brunch

매거진 개발개발

You can make anything
by writing

C.S.Lewis

by 워니 Aug 01. 2018

React.setState를 사용하지 않는 3가지 이유

[번역] 3 Reasons why I stopped using React

* 이 글은 Michel Weststrate 3 Reasons why I stopped using React.setState 를 번역한 글입니다.

** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!


저와 비슷한 고민을 하는 개발자 분들이 있을 수도 있다는 생각이 들어 오랜만에 번역을 해보았습니다. :)


평소에 setState 를 동기적으로 사용하기 위해 async/await 를 사용하는 것이 불편했고, MobX-state-tree 를 사용하면서 편의를 위해 별도의 로컬 컴포넌트 state 를 쓰는 것이 적합한 코드인지도 항상 의문이 있었는데 이 글을 통해 적절한 해결책을 찾았습니다.

실제로 트레바리 프로젝트에도 도입한지 2주 정도가 되었고 많은 부분에서 효율적으로 코딩이 가능한 것을 검증하여 더 좋은 방법을 알게 되기 전까지는 이를 쭉 활용해보려고 합니다.




몇 달 동안 새롭게 구현하는 모든 React 컴포넌트에 React 의 setState 를 사용하지 않았습니다. 아무런 문제도 없었고, 각 컴포넌트는 여전히 state 를 가지고 있었으며 바뀐 점이라고는 더 이상 React 가 state 를 관리하지 않는다는 것뿐입니다. 이는 매우 좋았습니다!


초심자가 setState 를 사용하는 것은 꽤 어렵습니다. 숙련된 React 개발자들도 React 의 state 메커니즘을 사용할 때 발견하기 어려운 버그를 종종 만나고는 합니다. 다음과 같이 말이죠:

React state 가 비동기적임을 잊어버리면 발생하는 버그; 계속 한 박자 늦은 아이템이 로그에 찍힌다.


React 문서setState 를 사용했을 때 잘못될 수 있는 모든 것을 요약해주고 있습니다:

Notes:
this.state 를 직접 조작하지 마세요. setState() 호출을 통해 필요한 작업을 수행할 수 있습니다. this.state 는 immutable 로 다루어야 합니다.

setState() 는 this.state 를 즉시 변경하지 않고 state 변화를 대기하도록 만듭니다. setState() 메서드를 호출한 직후에 this.state 에 접근하면 기존 값을 반환받을 수 있습니다.

setState 호출은 동기성을 보장하지 않으며 성능 향상을 위해 일괄적으로 처리될 수 있습니다.

조건부 랜더링 로직이 shouldComponentUpdate() 에서 실행되지 않는 한 setState() 는 항상 다시 랜더링하도록 만듭니다. mutable 객체가 사용되고 있고, 이 로직이 shouldComponentUpdate() 에서 실행될 수 없다면 state 값이 이전과 달라졌을 때만 setState() 를 호출하여 불필요하게 다시 랜더링 되는 상황을 피할 수 있습니다.


요약하자면 setState 를 사용했을 때 발생할 수 있는 3가지 이슈가 있습니다:


1. 비동기적인 setState

많은 개발자들은 처음에는 setState 가 비동기적인 것을 알지 못합니다. 어떤 state 를 설정하고 나서 그 state 를 보게 되면 state 가 여전히 기존에 가지고 있던 값을 가지고 있는 것을 발견하게 됩니다. 이것은 setState 에서 가장 헷갈리는 부분입니다. setState 호출은 비동기적으로 보이지 않고, 별생각 없이 호출하다 보면 찾아내기 어려운 버그들을 만나게 됩니다. 다음 코드는 이러한 부분을 제대로 보여줍니다:

얼핏 보기에는 괜찮은 코드처럼 보입니다. 두 가지 이벤트 핸들러와 제공된 onSelect 이벤트를 발생시키는 유틸리티 함수가 있습니다. 그러나 이 Select 컴포넌트는 위의 GIF 에서 보았던 버그를 가지고 있습니다. onSelect 는 항상 state.selection 의 이전 값을 가지고 실행됩니다. setState 는 fireOnSelect 헬퍼가 동작될 때 작업이 완료되지 않았기 때문입니다. 적어도 React 는 scheduleState 혹은 콜백이 필요한 메서드로 있어야 합니다.

이런 버그는 고치기 쉽지만, setState 때문에 이런 버그가 있을 수 있다는 것을 깨닫는 것 자체가 어렵습니다.


2018년 1월 25일 수정: Dan Abramov 는 왜 setState 가 비동기적으로 설계되었는지 설명해줍니다.


2. 불필요한 랜더링을 만드는 setState

setState 사용할 때 발생하는 두 번째 이슈는 항상 다시 랜더링 하도록 만든다는 것입니다. 대개는 다시 랜더링 할 필요가 없습니다. React 성능 도구에서 다시 랜더링 되는 것을 보기 위해 printWated 매서드를 사용할 수 있습니다. 대략적으로 다시 랜더링 할 필요가 없는 몇 가지 이유가 있습니다:

새로운 state 는 실제로 이전과 동일합니다. 이는 대부분 shouldComponenetUpdate 를 실행하여 해결할 수 있습니다. 여러분은 이 문제를 해결하기 위해 이미 (Pure render) 라이브러리를 사용하고 있을 수도 있습니다.

때로는 변경된 state 가 랜더링과 관련이 있을 수 있지만 모든 경우에 그렇지는 않습니다. 예를 들어 일부 데이터가 특정 조건에서만 볼 수 있는 경우가 있을 수 있습니다.

세 번째로 Aria Buckles 가  React Europe 2015 에서 지적한 것처럼 때로는 인스턴스 state 가 랜더링과 전혀 관련이 없습니다. 대부분 이벤트 리스너나 타이머 ID 등을 관리하기 위해 관련된 state 입니다.


3. 모든 컴포넌트의 state 를 포착하기에는 충분하지 않은 setState

위의 마지막 포인트에 따르면 모든 컴포넌트의 state 는 setState 를 사용하여 저장되고 업데이트되어야 하지 않습니다. 더 복잡한 컴포넌트는 대부분 타이머, 네트워크 요청, 이벤트 등을 관리하기 위한 라이프사이클 훅이 필요한 로직이 있습니다. setState 를 사용하여 이것들을 관리하면 불필요한 랜더링을 발생시킬 뿐만 아니라 관련된 라이프사이클 훅을 다시 발동시켜 이상한 상황을 만듭니다.


MobX 를 사용하여 로컬 컴포넌트 state 관리하기

(짱이고 짱임) Mendix 에서는 이미 모든 store 를 MobX 를 사용하여 관리하고 있습니다. 그러나 여전히 로컬 컴포넌트 state 를 사용할 때는 React 의 state 메커니즘을 사용하고 있었습니다. 최근에서야 MobX 를 사용하여 로컬 컴포넌트의 state 를 관리하도록 바꿨습니다. 다음과 같이 말이죠:

완성된 모습입니다:

동기적인 state 매커니즘을 사용하면 버그가 발생하지 않습니다


위의 코드 스니펫은 간결할 뿐만 아니라 setState 와 관련된 모든 이슈들을 해결합니다:


state 에 대한 변경은 로컬 컴포넌트 state 에 즉시 반영됩니다. 이는 우리의 로직을 간단하게 만들고 코드 재사용을 용이하게 해줍니다. state 가 아직 업데이트되지 않았을 수도 있다는 것을 대비하지 않아도 됩니다.


MobX 는 어떤 observable 이 랜더링과 관련됐는지 런타임 때 결정합니다. 그래서 일시적으로 랜더링과 관련이 없는 observable 은 다시 랜더링 하도록 만들지 않습니다. 다시 관련이 있기 전까지는 말이죠. 이런 이유로 필드에 @observable 가 표시될 때 랜더링 하는 것과 관련이 없다면 랜더링 페널티 (혹은 라이프사이클 문제)가 전혀 없습니다.


그리고 랜더링 가능하고 랜더링이 불가능한 state 는 통일성 있게 처리됩니다. 게다가 컴포넌트에 저장된 state 는 store 에 저장된 다른 state 와 동일하게 동작합니다. 이는 컴포넌트를 리팩토링 하기 쉽게 만들고 로컬 컴포넌트 state 를 별개의 store 옮기거나 그 반대의 경우도 쉬워지도록 만듭니다. 이는 egghead 튜토리얼에서 설명하였습니다.

MobX 는 컴포넌트를 효과적으로 작은 store 로 전환합니다.


게다가 state 를 위해 observable 를 사용하면 신입 개발자들은 state 객체에 값을 직접 할당하는 것과 같은 실수를 하지 않을 수 있습니다. 아참 그리고 shouldComponentUpdate 혹은 PureRenderMixin 실행에 대해 걱정하지 마세요. MobX 는 이미 이것들을 잘 다루고 있습니다. 마지막으로 setState 가 끝날 때까지 기다리고 싶으면 어떻게 해야 하는지 궁금한가요? 여전히 compentDidUpdate 라이프사이클 훅을 사용할 수 있습니다.


짱짱! MobX 를 시작하려면 어떻게 해야 하나요?

간단합니다. 10 minute interactive introduction 을 따라 해 보거나 앞서 말한 비디오를 보세요. 간단하게 코드로부터 하나의 컴포넌트를 가져와서 @observer 를 넣고, @observable 속성을 도입할 수 있습니다. 이미 사용하고 있는 setState 호출을 바꾸지 않아도 됩니다. MobX 를 사용하는 동안에도 계속 동작합니다. 잠시 동안 엄청 복잡하게 보이겠지만 곧 바뀔 겁니다. :) (오 그리고 만약 decorator 를 좋아하지 않아도 걱정하지 마세요. ES5 에서도 잘 동작합니다.)


TL;DR:

로컬 컴포넌트 state 를 관리할 때 React 를 사용하지 않기로 하였습니다. 대신 MobX 를 사용합니다. 이제는 React 가 진짜 "view" 로만 쓰입니다. :) MobX 는 로컬 컴포넌트의 state 와 store 의 state 를 둘 다 관리합니다. 이는 간결하고, 동기적이고, 효율적이며 통일성이 있습니다. 경험상 MobX 가 React 의 setState 보다 초심자들에게 설명하기 더 쉽습니다. 덕분에 컴포넌트가 깔끔하고 간단하게 유지됩니다.


state 관리를 위해 setState 를 사용한 JSBin

state 관리를 위해 MobX observables 를 사용한 JSBin

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari