Blazor에서 자식 컴퍼넌트의 int 값을 부모 컴퍼넌트로 가지고 오는 방법에 대해 질문드립니다

먼저 질문 글을 읽어주셔서 감사합니다

블레이저의 컴퍼넌트 간 데이터 연결을 공부하던 중
부모 컴퍼넌트에서 자식 컴퍼넌트에게로 값 이동은 파라미터 어트리뷰트로 쉽게 가능한데
자식 컴퍼넌트에서 부모 컴퍼넌트에게로 값 이동은 이벤트 헨들링을 이용해야 한다는 것을 알았습니다
제가 원하는 것은 '자식 컴퍼넌트의 int 값을 부모 컴퍼넌트로 가지고 오는 방법’인데요.
적당한 레퍼런스가 없어 전전긍긍하다
얼추 성공을 시킨듯 하였으나 마지막에 문제가 풀리지 않아 질문 드립니다

부모 컴퍼넌트

@page "/"
@rendermode InteractiveServer

<h3>Parent</h3>

<p class="mt-3 mb-3"> Child Comp에서 받은 숫자 : @GetChildNum </p>
<hr/>
<Child  OnSetNumber="GetChildNumMethod"></Child>
@code {
    
    public int? GetChildNum { get; set; }
    public void GetChildNumMethod(int? num)
    { 
        GetChildNum = num;
        Console.WriteLine(GetChildNum);
    }
}

자식 컴퍼넌트

<h3>Child</h3>

<form class="mt-3" @onsubmit="ChildNumCall">
    <input class="me-2" @bind="ChildNum" @bind:event="oninput" />
    <span>Child에서 Parent GetChildNumMethod 실행</span>
</form>
<p>Child Comp ChildNum의 현재 값 : @ChildNum </p>

@code {
    public int? ChildNum { get; set; }

    // Parent Comp에게 int 값을 넘겨주기 위한 액션 델리게이트
    [Parameter] public Action<int?> OnSetNumber { get; set; }
   
    private void ChildNumCall() => OnSetNumber.Invoke(ChildNum);
}


자식 컴퍼넌트에 숫자 38를 입력하면 부모 컴퍼넌트 빨간 박스 부분에 숫자 38이 나타나지 않는데요


디버깅을 해봐도 값은 잘 들어오고
콘솔 창에도 38이 잘 찍히는데
브라우져에서 렌더링이 안되는지 모르겠습니다 ㅜ

이상입니다.

미리 먼저 감사드립니다!

자신은 없지만, 혹시… 필요한 타이밍에 …?

StateHasChanged();

(정답은 아래 고수님께서 해주실 껍니다;; 미리 죄송합니다. )

1 Like

@경태_왕 헉… 됩니다 ㅜㅜㅜ
감사 또 감사드립니다!

1 Like

두개를 합친 것이 EventCallback입니다.

이 객체는 광범위하게 사용됩니다.
@bind-Value 에도 관여하구요

2 Likes

덧붙이고자 해서 추가적으로 설명드립니다 :slight_smile:

첫번째로 input의 값에 따라 바로 parent 값이 바뀌지 않는 이유는 form의 onSubmit에 Parent에 값을 전송하는 함수를 등록했기 때문에, input 요소의 onInput에는 Child 컴포넌트 안의 ChildNum값만 바뀌고, ChildNumCall이 호출되지 않습니다. 주신 코드에서 호출이 되게 하려면 input 요소에 포커스된 상태에서 enter를 입력하면 onSubmit이벤트가 발생하면서 ChildNumCall이 호출됩니다(input요소에서 enter가 입력되면 form에서 onSubmit이벤트가 발생하는 이유는 웹의 역사까지 흘러갑니다).

두번째로 form에 의존하지 않고 input 요소의 이벤트만으로 ChildNumCall을 호출하고자 하시면 @bind:after="함수"를 사용하시면 됩니다.(출처 - 공식문서). 혹은 부모에 전달하지 않아도 된다면 CascadingParameter를 사용하셔도 됩니다. StateHasChanged를 사용하게 되면 어플리케이션이 커지게 되면 불필요한 재렌더링을 만들 수도 있습니다.

2 Likes

양방향 데이터 바인딩을 허락하는 자식 요소를 정의하는 방법 중 가장 권고되는 방법은 아래와 같습니다.

<h3>Child</h3>

<input class="me-2" 
    @bind-value:get="Value" 
    @bind-value:set="OnInputChange"
    @bind-value:event="oninput" 
    />
<span>Child에서 Parent GetChildNumMethod 실행</span>

<p>Child Comp ChildNum의 현재 값 : @Value </p>

@code {

    [Parameter]public int? Value { get; set; }
    [Parameter]public EventCallback<int?> ValueChanged { get; set; }

    private async void OnInputChange(int? value)
    {
        if (value == Value) return;

        await ValueChanged.InvokeAsync(Value = value);
    }
}

여기에서 주목해야 할 부분은 아래의 파라미터 쌍입니다.

    [Parameter]public int? Value { get; set; }
    [Parameter]public EventCallback<int?> ValueChanged { get; set; }

블레이저의 관습 중 하나는 아래와 같이 쌍을 이루는 두 파리미터를 부모와 양방향 데이터 바인딩을 위한 것으로 보는 것입니다.

    [Parameter]public TName Name { get; set; }
    [Parameter]public EventCallback<TName> NameChanged { get; set; }

양방향 바인딩

자식 요소가 위 파라미터 쌍을 보유하고 있으면, 부모 요소는 아래와 같이 @bind-{Name} 태그 속성을 통해 자신의 상태와 자식 요소의 파라미터 사이에 양방향 데이터 바인딩을 걸 수 있습니다.

@attribute [Route(Path)]
@rendermode InteractiveServer

<h3>Parent</h3>

<p class="mt-3 mb-3"> Child Comp에서 받은 숫자 : @_childNum </p>
<hr/>
<Child @bind-Value="_childNum"></Child>

@code {
    public const string Path = "/parent";
    private int? _childNum;
}

블레이저 프레임워크가 제공하는 요소들은 Name 에 보통 “Value” 를 쓰는데, 커스텀 요소인 경우 보다 적절한 의미를 나타내는 이름으로 변경할 수 있습니다.

    [Parameter]public int? Number { get; set; }
    [Parameter]public EventCallback<int?> NumberChanged { get; set; }
// ...
<Child @bind-Number="_childNum"></Child>
// ..

자식 => 부모 단방향 바인딩

부모 요소는 아래와 같이 자식으로부터 값을 읽기만 할 수 있습니다.

// ...
<Child ValueChanged="OnChildValueChanged" ></Child>

@code {
    // ...

    private void OnChildValueChanged(int? value)
    {
        if (_childNum == value) return;

        _childNum = value;
    } 
}

양방향 방인딩이든, 단방향 바인딩이든, 자식 요소의 이벤트에 의해 부모 요소가 리렌더링되기 때문에 StateHasChanged 를 수동으로 호출하면 이중 렌더링이 됩니다. (호출하지 말아야 합니다)

파라미터에 쓰지 마라

보통은 파라미터로 노출한 속성에 직접적으로 쓰는 것을 피하는 것이 권고됩니다.

이 원칙을 Child 요소에 적용해 보면,

<h3>Child</h3>

<input class="me-2" 
    @bind-value:get ="_inputValue" 
    @bind-value:set="OnInputChange"
    @bind-value:event="oninput" 
    />
<span>Child에서 Parent GetChildNumMethod 실행</span>

<p>Child Comp ChildNum의 현재 값 : @_inputValue </p>

@code {

    [Parameter]public int? Value { get; set; }
    [Parameter]public EventCallback<int?> ValueChanged { get; set; }

   // 상태 관리를 내재화
    private int? _inputValue;

    protected override void OnParametersSet()
    {
        if (Value != _inputValue) _inputValue = Value;
    }

    private async void OnInputChange(int? value)
    {
        if (value == _inputValue) return;

        await ValueChanged.InvokeAsync(_inputValue = value);
    }
}

리렌더링의 억제

레이저 (부모)요소를 작성하다 보면, 많은 자식 요소들을 포함할 수 있고, 자식 요소들 때문에 부모의 요소의 리렌더링 횟수가 기하 급수적으로 늘어날 수 있습니다.

보통 이러한 렌더링이 부담스러워 리렝더링을 억제하고자 하는 욕구가 생깁니다.
그런데, 사실 억제하면 안되는 경우가 더 많습니다.

질문의 코드가 그렇습니다.
부모 요소는 자신의 상태를 UI에 (즉시) 반영하도록 설계되었기 때문에, 상태의 변화는 반드시 리렌더링을 통해 UI에 반영돼야 합니다.

단순히 UI에 반영해야 할 상태가 많아서 생기는 리렌더링의 증가는 억제하면 안되겠죠. 반대로, 억제하려는 시도가 결국은 오작동으로 귀결되기도 합니다.

리렌더링 증가로 인한 비효율은 내 코드 보다는 프레임워크에서 해결하는 것이 더 바람직합니다.
실제로 리렌더링이 많아도 큰 성능 저하가 발견되지 않는 것으로 보아, 블레이저의 리렌더링 로직이 대체로 최적화된 느낌이 있습니다.

7 Likes

@hjnn 자세한 설명 감사합니다.
덕분에 CascadingParameter에 대해서도 공부해봐야겠네요.
혹시 작성 글 중 ‘StateHasChanged’ 메서드를 호출하면 어플리케이션이 커지게 된다고 하셨는데
커진다는게 어떤 건가요? 새로 서버와 통신이 이뤄진다는 말씀이신지요?

1 Like

어플리케이션이 커지는 경우라고 뭉뚱그려서 말했는데, 간단하게 웹 서비스가 커지는 경우를 생각한 것입니다. 만약 Blazor로 만들어진 UI가 간단하면 추적이 쉽지만, UI가 복잡해지고, 다른 JS가 Interop하는 경우까지 가게 된다면(극단적인 가정이지만, 약간의 복잡성을 가져도 같다고 생각합니다. ex. 캘린더, 테이블) StateHasChanged로 트리거된 재렌더링으로 만든 오류를 찾기 힘들다고 생각해서 StateHasChanged를 사용해서 강제 재렌더링을 만드는 것 보다, Blazor의 자체 시스템을 사용해서 추적하기 쉽도록 코드를 만드는 것이 좋다고 생각했습니다.

덤. 비슷한 UI를 만드는 라이브러리인 React에서도 forceUpdate도 권장되지 않습니다. 출처

3 Likes

@hjnn 자세한 설명 감사합니다!

1 Like

여러 가지 관점을 담지는 못했는데, 잘 설명해주셔서 제 주관과 함께 부가 설명을 드리려고 합니다!

파라미터에 쓰지 마라

Blazor의 데이터 바인딩은 Angular와 Vue의 양방향 데이터 바인딩을 따르고 있습니다. 비슷하게 Parameter를 사용할 때 비슷한 문제를 겪고 있는데, Paramter로 할당된 값을 읽기 전용을 사용하지 않아도 재렌더링을 할 수 있다는 문제점이죠(출처).

보통은 파라미터로 노출한 속성에 직접적으로 쓰는 것을 피하는 것이 권고됩니다.

이 문장도 비슷한 문제에서 나왔다고 생각합니다공식 문서.

매개 변수는 예기치 않게 덮어쓰지 않습니다.

예기치 않게 덮어쓸 수 있고, 이는 예상치 못한 부작용이 나오기 때문에 매개변수를 예기치 못하게 덮어쓰는 것을 방지하기 위해 예시가 나온것이죠.

리렌더링 억제?

레이저 (부모)요소를 작성하다 보면, 많은 자식 요소들을 포함할 수 있고, 자식 요소들 때문에 부모의 요소의 리렌더링 횟수가 기하 급수적으로 늘어날 수 있습니다.

기하 급수적으로 늘어나면 안된다고 생각합니다. 만약 버그가 발생하는 재렌더링이 여러번 일어나고 그 중 하나만이 버그의 원인이라고 하면 복잡한 UI에서는 찾기 어려울겁니다. 혹은 리렌더링을 모두 계산하는 비용이 커서 사용자에게 응답이 오는데 오랜 시간이 걸릴 수도 있습니다.

React에서는 버그의 원인을 추적하기 쉽게 하기 위해서 여러가지 대안이 나왔습니다.

  • Props(Blazor에서는 Parameter) drilling 현상을 피하기 위해서 Context API(Blazor에서는 CascadingParameter)가 나왔습니다.
  • 파생된 상태를 직접 관리해서 재렌더링 현상을 피하고 추적이 용이하기 쉽도록 Single source of truth를 강조합니다. Blazor Talk, 관련 Slide.

단순히 UI에 반영해야 할 상태가 많아서 생기는 리렌더링의 증가는 억제하면 안되겠죠. 반대로, 억제하려는 시도가 결국은 오작동으로 귀결되기도 합니다.

저는 반대라고 생각합니다. UI에 반영해야 할 상태의 원천은 결국 소수이고, 이 상태가 원천인지, 파생된 상태인지 판단하는 것은 중요합니다. 이전에서 말했듯이 버그가 발생하면 추적하기 쉽기 때문이죠.

말씀 주신대로 성능 최적화는 상황마다 다릅니다. 제가 말한 것이 틀릴 정도로 미미한 정도로 최적화가 적을 수도 있고, 혹은 제 말대로 버그가 추적하기 어려워서 버그를 해결하는데에 며칠을 소모할 수도 있습니다. 물론 원 질문에서는 source of truth를 페이지에 두는 것이 많지만 보통의 웹 서비스의 경우에는 부모 - 자식 한 depth만 생성되는 것이 아니라 여러 depth가 생성됩니다. 그래서 여러 방법을 설명하고, 리렌더링을 막는 것이 중요하다고 했지만, 제 부연설명이 부족했던 것 같습니다.

프레임워크에서 해결하는 것이 더 바람직합니다.에는 동의합니다. 그 프레임워크만의 최적 코드가 있을 수 있다고도 생각합니다. 하지만 프론트엔드를 다루는 기술은 크게 다르지 않아서, 재렌더링을 최소화 하기 위해 Single source of truth를 강조하거나, Props drilling을 피하려는 기술은 Blazor에서도 적용된다고 생각합니다.

1 Like

링크의 문서에서 덮어쓰는 주체는 부모 요소입니다.
제가 말씀드린 것은 자식 요소 내부에서 파라미터에 쓰지 말라는 의미입니다.

부모 요소가 자식 요소의 파라미터를 덮어 쓰면, 대부분의 경우 자식 요소는 리렌더링되는 게 정상적입니다. 왜냐하면 파라미터에 할당된 값은 자식 요소의 UI에 어떤 식으로든 반영되는 경우가 대부분이기 때문입니다.

문제 상황은 파라미터를 자식 요소가 상태 저장소로 사용하는 경우입니다.
부모와 자식이 동시에 상태를 변경할 수 있기 때문에 예기치 않은 상황이 발생하는 것이죠.

이러한 문제는 자식이 별도의 상태 변수를 통해 자신의 상태를 관리하게 만들어 피할 수 있습니다. (부모가 파라미터에 쓰는 것을 막을 수는 없으니까요)

저의 예제의 경우, 부모가 파라미터에 쓴 값을 OnParameterSet 을 통해 간접적으로 상태 저장소에 반영시키고 있는데, 링크의 문서에서는 경우에 따라 OnInitialized 와 OnParameterSet을 선택할 수 있다고 설명하고 있습니다.

버그의 원인은 소스코드에 있는 것이지 리렌더링 자체에 있지는 않습니다.

실험을 한 적이 있는데, 부모 페이지 객체의 리렌더링을 원천적으로 봉쇄하고

protected override bool ShouldRender() => false;

그 페이지의 모든 자식 요소는 닷넷 이벤트 시스템을 통해 개별적으로 리렌더링을 결정하도록 구성해 본 적이 있습니다.

이 경우 이벤트 발행자는 캐스케이드 파라미터를 통해 자식 요소로 주입했습니다.

이 페이지는 동일한 요소 인스턴스를 대략 1000 개 정도(더 적을 수도 많을 수도)를 보유했는데, 모든 인스턴스가 하나의 이벤트를 구독하다보니, 이벤트가 호출되었을 때 문제의 요소를 찾으려면 조건부 중단점 외에는 달리 방도가 없더라구요.

근데, 이러한 문제 해결 방식은 블레이저 이벤트 시스템에 기반한 경우도 여전히 유효한 것이라, 별도의 이벤트 관리 객체 도입 비용만 증가한 형국이 된 것이죠.

특히 요소가 그 이벤트 관리 객체에 강하게 종속되어 경직되고, 재사용성이 급감하는 문제도 무시할 수 없었습니다.

그래서 걷어 내고, 블레이저 이벤트 시스템에 기반하도록 변경했는데, 렌더링이 느려졌다고 느껴지는 못했습니다. (블레이저 와즘 기준)

저도 한때 블레이저의 리렝더링 비용이 바쌀 것이라는 막연한 우려를 가지고 있었는데, 아직까지 수치로 증명된 것을 본 적은 없는 것 같습니다. (경험은 비싸지 않다고 말하고 있구요)

"Linq 는 느리다"는 막연한 낭설과 같은 것은 아닐지.

2 Likes

네, 맞습니다. 제가 쓴 글은 추가 설명이라고 생각합니다.

저도 버그의 원인은 리렌더링에 있다고 생각하지 않습니다. 하지만 리렌더링이 많이 이루어지면 그 리렌더링 중에 버그를 찾는데 어려움이 있을 것이고, 리렌더링을 트리거하는 것을 줄이자가 주 내용입니다. 그 과정에서 불필요한 리렌더링(혹은 부작용, 사이드 이펙트)을 줄일 수 있을것이구요.

간단한 수치는 js-framework-benchmark에서 볼 수 있습니다(물론 실 사용사례에서는 다를 수 있다고 생각합니다). 프론트엔드에서 느리다고 욕 먹고 있는 ReactJS보다 전체적으로 성능이 떨어지는 것을 볼 수 있습니다. Blazor의 경우에는 어쩔 수 없는 측면이 없지않아 있지만(WASM에서 DOM/BOM API를 직접 조작하는 것은 힘들기 때문입니다), 그만큼 렌더링의 비용이 JS에 비해 비싸다는 것은 사실입니다. 그러면 React보다 느린만큼, 중간 규모의 어플리케이션을 짤 때 더 많은 노력이 들어가야 한다고 생각합니다(낮은 성능의 모바일 기기를 사용하시는 분까지 생각하면요). 물론 말씀하신 섣부른 추상화도 문제이고, 그만큼 많이 렌더링하고 있지 않을 뿐더러, 저것을 렌더링한다고 프레임 드랍이 발생할 확률도 적을 정도로 우리의 기기가 발전한 것도 사실입니다.

하지만 우리가 어떤 어플리케이션을 만들지는 모릅니다. Blazor를 facebook과 같은 소셜미디어를 만들 때 쓸 수 있는 것이고, 아니면 간단한 todo 앱을 만들 때 사용할 수도 있습니다. 하지만 두 어플리케이션을 만들 때 적용되는 개념은 비슷하다고 생각합니다. 코드를 파악하기 쉽게, 버그에 잘 대응하도록 하는것이죠. 그를 위해서 여러 개념을 사용할 것을 고려해서 말씀드렸습니다(제가 생각하기에는).

사실 @BigSquare 님이 말하시는 바와 크게 다르지 않다고 생각합니다(처음에는 표현때문에 잘못 이해했었습니다. 죄송합니다.). 저는 때에 따라 CascadingParameter나 그 이외의 것들을 사용하거나 Single source of truth원칙에 따라서 만들면 버그를 추적하기 쉬울 것이고, 그러면 자연히 불필요한 재렌더링은 제거된다고 생각합니다. @BigSquare 님은 과도하게 재렌더링을 막는 것은 그것 또한 비용이며, 그러면 코드가 보기 어려워진다고 주장하신다(라고 저는 해석했습니다). 둘이 사실 상충하는 개념은 아닙니다. 때에 따라 CascadingParameter나 다른 메소드를 사용하여 편의성을 증대하되, Single source of truth을 지키면서 과도한 최적화를 경계하면서 코드를 만들면 되겠죠.

4 Likes

@BigSquare 정말 감사합니다.
이벤트 콜백 구조체 에 대해서 알게 되었고
사용방법도 얼추 눈에 들어왔습니다

블레이저는 이렇게 사용해야 한다 라는 느낌을 처음으로 맛본듯 합니다.
소중한 시간 내주셔서 다시 한번 감사드립니다

2 Likes

좋은 자료 감사합니다.

블레이저가 다른 js 프레임워크보다 많이 느릴 것이라 생각은 했는데, 느리기는 많이 느리네요.

2 Likes