[웹 프론트엔드 인터뷰] #3. React 상태관리는 어떻게 해야 하나요?
최근에 웹 프론트엔드 개발자로 취업을 준비하는 개발자 지망생 분들을 멘토링을 하면서 여러가지 질문들을 받는데, 그 중에서 나도 고민을 하고 답변을 해준 질문들이 있었던 것 같아 그러한 질문들도 조금씩 블로그에서 다루어 보려고 한다. 이번 주제는 리액트에서의 상태관리를 어떤 방식으로 하는 것이 소위 best practice인가에 대한 질문을 받았고 이에 대한 내 생각을 정리해 보려고 한다. 어디까지나 이 글이 정답은 절.대. 아니며 자유로운 의견은 댓글로 남겨주면 감사할 것 같다.
질문 : 리액트에서 상태관리는 어떻게 해야 하나요? 상태관리 라이브러리는 꼭 써야 하나요? 쓰게 된다면 어떤 것을 써야 하나요?
이 이야기를 하려면 먼저 Flux 패턴에 대해서 이야기를 할 수 밖에 없을 것 같다. Flux 패턴은 리액트를 만든 페이스북(이제 메타로 바뀜)이 이 2014년 개발자 컨퍼런스에서 발표를 하였고, 그 이후에 댄 아브라모브가 이 패턴을 가지고 Redux를 만들었다. Flux 패턴을 만든 이유는, 대규모 어플리케이션에서 데이터 관리를 일관된 방식으로 하기 위함이었다. MVC 패턴에서 관리해야 하는 Model과 View가 많아지면서 이 사이에서 수많은 N 대 N 인터렉션이 일어나게 되고 사이드 이펙트가 불가피하게 생길 수 밖에 없게 되었다.
페이스북은 이 문제를 Flux 패턴을 통해 해결했다. 양방향 데이터 흐름이 아닌 단방향 데이터 흐름을 가지도록 변경한 것이다. 이렇게 처리를 해 준 결과 데이터 흐름이 한 방향으로 강제되고, 모든 상태가 스토어에 모여 있어서 변경 사항을 컴포넌트에 전달하기도 쉬워졌다.
이러한 Flux 패턴을 바탕으로 Redux가 만들어졌고, 리덕스는 그 이후로 정말 많은 리액트 개발자들에게 상태관리 라이브러리로 선택받아 쓰이고 있다. 비동기를 처리하는 redux-saga 등과 같이 말이다. 하지만 리덕스는 2021년 현재 리액트 상태관리를 하기에 가장 좋은 옵션인가에 대해서는 나는 물음표다.
리덕스가 아주 만족스럽지 않은 이유는 리덕스가 flux 패턴을 그대로 구현한 것이 아니기 때문이다. 먼저 리덕스에는 위에 본 그림처럼 dispatcher 개념이 없다. 리덕스의 리듀서가 디스패처+스토어 의 역할을 담당한다. 리덕스의 철학은 이벤트 에미터(EventEmitter)로 작동하는 디스패처를 생략하고 리듀서가 각 액션 타입에 대한 메서드를 순수 함수 형태로 구현하는 것이 더 낫다는 주의이다. 게다가 리덕스는 모든 상태를 immutable하게 설정해서 업데이트 된 값은 기존 상태를 재작성하는 것이 아니라 새로운 객체로 복사가 되는 개념이다. 따라서 각 리듀서는 서로를 의존할 수 없고 완전히 고립되어 있다.
또 리덕스가 만족스럽지 않은 두 번째 이유는 리덕스 스토어는 싱글톤 패턴을 따르기 때문이다. 위의 Flux의 스토어가 여러 개 존재한 것과 다르게 리덕스의 스토어는 상태를 지니는 리듀서들이 모두 하나의 스토어에 묶이도록 설계했다. 원래 Flux 패턴은 각 뷰가 자신이 필요한 스토어(Model)를 접근할 수 있다는 것이었는데 리덕스는 무조건 싱글 스토어를 통해 각 리듀서에 접근을 해야 했다.
물론 리덕스에도 장점이 있다. immutable을 유지하며 무결성을 보장하고 사이드 이펙트 없이 상태를 관리할 수 있도록 설계했다는 점이 바로 그것이다. 뭐든지 trade-off가 있는 것이고 모든 조건을 만족하는 이상적인 silver bullet은 없다. 그리고 참고로 나는 MobX를 사용해 본 적은 없다. 따라서 이에 대해서는 별로 할 말이 없다. 써보신 분 계시다면 댓글로 장단점 알려주시면 감사하겠습니다 ㅎㅎ
내가 실무에서 경험하고, 주변의 웹 프론트엔드 개발자들을 조사해 본 바로 현 시점에서 리덕스가 가장 좋은 옵션은 아니다 라는 생각에는 전체적으로 동의하는 듯 했다. 나의 경우 실무에서는 리액트 내부 상태관리 도구인 context api를 주로 사용하고 있다. context api는 새로운 컨텍스트를 React.createContext() 라는 함수로 만들어서 해당 컨텍스트의 Provider 컴포넌트를 가지고 우리가 원하는 범위의 컴포넌트를 감싸줄 수 있게 되며, 이 안에서 필요한 값들을 value로 넣어서 상태관리를 하는 도구이다. 전역 상태를 관리할 때 사용할 수도 있고, 특정 원하는 지역의 상태를 관리할 때도 사용할 수 있어서 편리하다. 복잡한 상태관리를 할 때는 조금 이야기가 달라지겠지만, 일정 규모까지는 굉장히 심플하게 사용할 수 있어서 현업에서도 편리하게 잘 사용하고 있다.
그렇다면 Redux에 비해서 Context api가 가진 장점들은 알겠고.. context api의 한계는 과연 무엇일까? 만약 규모가 커지고 복잡도가 증가하면 context로 비효율 적이다라고 페이스북 엔지니어 세바스찬 마크베이지는 이야기한다. 낮은 빈도의 업데이트는 context를 써도 문제가 없지만, 결과적으로 context는 flux와 같은 상태 관리 시스템을 대체할 수는 없을 것이다 라고 말이다.
Provider 하위의 모든 consumer들은 Provider의 속성이 변경될 때 마다 다시 렌더링이 일어난다. Provider에서 복잡한 값을 들고 있다면 그 context를 구독하고 있는 모든 하위의 컴포넌트들이 다시 리렌더링이 일어나게 될 것이다. 아래의 그림에서 왼쪽 상태에서 Provider 3을 감싸주면 오른쪽에 파란색으로 칠한 하위 컴포넌트가 모두 리렌더링이 발생하고 이는 복잡도가 증가할수록 비효율적으로 이루어진다. 성능도 안 좋고, Provider와 컴포넌트 트리 사이에 커플링이 생긴다.
이를 해결하기 위해 2020년 페이스북 팀에서 Recoil 이라는 상태관리 라이브러리를 만들었다. 리코일 공식 문서를 가보면 Minimal and Reactish 한 라이브러리로 리코일을 소개한다. 컴포넌트가 구독할 수 있는 React state를 atom으로 지정하고, 이 atom 값이 바뀌면 이 atom을 구독하고 있는 컴포넌트들만 리렌더링이 일어나는 식으로 설계되어 있다. 그리고 selecter는 상태에서 파생된 데이터로 다른 atom에 의존하는 동적인 데이터를 만들 수 있게 해준다. 리액트의 동시성 모드(Concurrent Mode)를 지원하며 비동기 함수들도 selector 데이터 플로우 그래프에서 균일하게 혼잡하게 해준다. 또한 React Suspense와 함께 동작할 수 있게 설계되어 보류중인 데이터(Promise resolve 이전)를 다룰 수도 있게 해준다. 이 외에도 많은 기능들을 지원하는데 이 부분은 리코일 공식 문서를 참고하기를 바란다.
한 줄로 결론을 내면, 덜 복잡한 건 그냥 리액트 내부의 context를 쓰고, 어플리케이션이 많이 복잡한 건 recoil을 써보도록 하자!
참고자료
https://www.huskyhoochu.com/flux-architecture/
https://react.vlpt.us/basic/22-context-dispatch.html
https://ui.toast.com/weekly-pick/ko_20200616