알잘딱을 싫어하지만 ‘상태 관리’만큼 알잘딱의 영역에 있는 것이 있을까. 도구를 선택하는 것도, 상태를 모아두고, 나누고, 관리하는 것도. 쉽게 수치화되지 않고 상황에 따라 경우가 많아지고 복잡해진다.
그러다 얼마 전 복잡하고 큰 데이터와 타입을 다루는 에디터를 만들게 되었다며 상태 관리 라이브러리를 추천해달라며 친구에게 부탁을 받았었다. 그 말을 듣고 처음으로 든 생각은, ‘데이터의 크기나 복잡도로 상태 관리 라이브러리를 고를 수 있을까?’라는 생각이 들었다. 비슷한 주제로 몇 가지 생각의 가지를 뻗어나가며 허접하게나마 ‘상태 관리’에 관한 내 생각 몇 가지를 정리할 수 있었다.
데이터의 크기와 복잡도 외에 고려할 점이 있을까?
앞서 데이터의 크기나 복잡도만으로는 사용할 도구를 선택하기 어렵다고 이야기했다. 데이터 크기를 생각해보자. 하나의 웹서비스에 있는 모든 상태를 사용한다고 했을 때 어느 정도의 크기를 가지게 될까?
Redux를 적용한 웹사이트에 Redux Persist 같은 미들웨어를 연동해 LocalStorage와 동기화시켜 용량을 측정해본 적이 있다. 파일의 바이너리 등을 다룬다면 이야기는 달라지겠지만, 이전에 측정해봤을 때 3MB를 넘지 않았다. 평균적으로 브라우저의 LocalStorage가 5~10MB 정도를 담을 수 있는 것을 생각해보면 그렇게 크지는 않을 수 있다.
개인적으로 상태 관리 도구를 선택할 때에는 만들 제품의 특성, 함께 작업하는 사람들의 수와 함께 아래의 조건을 적용해볼 수 있을 것 같다.
작게 쪼갤 수 있는 데이터인가? (혹은 정말로 작게 쪼개야하는 데이터인가?)
얼마나 많은 곳에서 그 데이터를 참조하게 되는가?
해당 데이터는 단발적 / 휘발적인가?
작게 쪼갤 수 있는가, 정말 쪼개야 하는가?
탁구공은 눈에 띄지만 공기 분자가 눈에 들어오지는 않는다. 데이터 역시 적당한 크기로 쪼갠다면 ‘관심사의 분리’가 되지만, 너무 작게 쪼개면 눈에 띄지 않아 놓치게 된다. 자주 하는 실수 중… ‘한 곳만 업데이트해서 화면이 깨진다’거나 ‘비슷한 역할의 Dispatch 액션을 여러 개 만든다’거나…
그렇기에 오히려 쪼개는 것이 어렵다면 ‘지금은’ 커다랗게 모아두어도 괜찮지 않을까. 생각해보면 Redux도 하나의 거대한 컨텍스트(Context)이자 단일 스토어(Store)니까.
오히려 중요한 것은 ‘어떻게 상태의 업데이트 패턴과 함수를 가져갈 것인가’이다. 비즈니스 로직을 구현하는 과정에서 어떤 상태를 어떻게 업데이트할지 감을 잡을 수 있어야, 해당 경험을 토대로 커다란 상태를 작게 쪼개거나 비슷한 상태끼리 묶을 수 있다. (개인적으로는 XState를 사용해서 Machine을 설계해보는 것이 도움이 되었다)
얼마나 많은 곳에서 데이터를 참조하는가?
데이터의 참조만큼 곤란한 것이 없다. 상태를 정리하거나 없애려고 하면 저멀리 있는 다른 페이지, 다른 기능에서 그 상태를 참조하고 있어 곤란했던 적이 많다. 단순히 데이터를 참조만 한다면 다행이지만, 상태의 업데이트까지 여러 곳에서 하게 된다면 지옥이 따로 없다.
리포트를 만들면서 가장 고통받았던 것 중 하나인데, 리퀘스트를 바꿀 수 있는 컴포넌트와 기능이 정말 다양하게 흩뿌려져 있다. 사용자들은 편하지만 우리는 고통스럽다. 사실 참조와 업데이트가 여러 곳에서 가능하다면, 대부분은 컴포넌트와 훅의 설계 과정에서 최대한 해결할 수 있다고 믿고 있다. React의 경우 children
을 활용하여 의도적으로 컴포넌트를 바깥으로 꺼내 제어를 역전시킬 수도 있을 것이다.
이걸로 해결되지 않는다면 그때는 Atomic한 상태 관리 방법(Jotai, Recoil…)을 고려할 수 있다. 가장 가까운 루트의 상태값을 따라가게끔 만들 수도 있고, 역할이 끝나면 노드 자체를 날려버릴 수도 있다. 하지만, 이로 인해 상태 관리의 흐름이 직관적으로 보이지 않아 사이드 이펙트가 잔뜩 생길 수 있다는 점은… 나도 알고 싶지 않았다.
얼마나 단발적 / 휘발적인가? (정말 필요한가?)
잠깐 쓰이고 말 데이터에는 특별한 이유가 없는 한 전역으로 넘기지 않아야 한다. 대부분의 상태 관리는 해당 뷰 프레임워크 혹은 라이브러리가 제공하는 기본 값으로 충분한 경우가 많다.
예를 들어 useState
는 작은 컴포넌트에서 단발적이고 휘발적인 상태 혹은 데이터를 다룰 때 사용한다. 업데이트할 상태가 많아지면 useReducer
를 쓰다가, 보다 여러 곳에서 상태를 쓰게 된다면 Context로 옮기고. 더 전역에서 쓰게 된다면 Context Provider의 위치를 점점 상위로 올려보고… 컴포넌트가 언마운트되며 값이 초기화된다는 것은, 전역 상태의 어딘가에 기록을 남기지 않는다는 점에서 다른 사이드 이펙트를 막을 수 있다.
간단한 Form의 상태를 관리하기 위해 전역 상태 관리 도구를 사용할 필요는 없을 것이다.백엔드에는 같은 값을 보내주어야 하는데 프론트엔드에서는 다르게 보여주어야 하는 경우는 상태를 위한 여러 플래그를 두지 않고 관리해볼 수도 있을 것이다.
가장 좋은 상태 관리는 관리할 상태가 없는 것
궁극적으로 가장 좋은 상태 관리는 ‘관리할 상태가 없는 것’이다.
‘상태 = 사이드 이펙트’라면, 사이드 이펙트의 수가 적을 수록 개발의 복잡도가 내려가게 되니까. 예전에 작업한 코드를 다시 돌아보면 불필요하게 상태를 선언해서 useEffect
등으로 동기화하거나, 상태에 들어갈 수 있는 값을 너무 많이 선언해 복잡해진 코드가 많았다.
상태를 줄이기 위한 노력 역시 많아지고 있다. 적절한지는 모르겠으나 React의 Server Component가 하나의 예시가 될 것 같다. 보통 많이 보이는 패턴 중 하나로, 백엔드에서 데이터를 불러온 뒤에 클라이언트에 다시 상태를 저장하는 패턴을 볼 수 있다.
지금까지는 useEffect
에서 API를 호출한 뒤 받아온 데이터를 state에 넣어주거나… React Query 같은 서버 상태 라이브러리를 썼다. Server Component를 사용하면 이러한 라이브러리를 사용하지 않고 async하게 컴포넌트를 선언해서 API 호출을 그 컴포넌트 안에서 실행하면 된다. Suspense 등의 이점 역시 그대로 가져갈 수 있다.
여전히 상태 관리는 내게 복잡하고 어렵다. Bad Case를 많이 작성하고 많이 맞아보면서 조금씩 늘어가고 있을 뿐이다.