React) Context API는 상태관리 도구가 아니다.

2ast·2024년 5월 6일
7

Context API는 상태관리 도구가 아니다.

많은 사람들이 redux, zustand, recoil 등 상태관리 툴과 react의 context를 동일 선상에 두고 고려하는 경향이 있다. 하지만 엄밀히 말하면 이들은 실제 수행하는 작업이 다른 별개의 개념이다. 바로 이 지점에서 많은 오해가 발생하고 있다고 느껴졌다.

흔히 "상태관리 라이브러리" 라고하면 떠오르는 redux, zustand, recoil, mobx 등을 떠올려 보자. 이들은 말 그대로 상태 "관리"에 그 방점이 찍혀 있음을 알 수 있다. 라이브러리 철학에 따라 성능을 최적화하기도하며, selector, persist, subscribe, reducer 등 부가적인 기능들도 폭넓게 제공한다. 마치 react에서 상태를 관리하는데 필요한 기능들을 몰아넣은 종합 공구 세트 같은 느낌이다. 이처럼 상태관리 라이브러리들에게 "전역으로 상태에 접근하게 해준다" 라는 점은 상태 관리를 위한 수많은 부가기능 중 하나에 불과하다.

반면 Context를 떠올려보자. Context는 상태를 관리하기 위한 어떠한 도구도 제공해주지 않는다. 단지 Provider 하위 스코프에서 value에 접근 가능하도록 백도어를 열어줄 뿐이다. 이런 배경에서 우리는 Context를 "상태 관리 도구"가 아니라 "상태 주입 도구"라고 정의하고 받아들여야만 한다.

누군가는 위 문단을 읽고 이렇게 생각할 수도 있다.

Context가 상태관리 도구가 아니라 상태 주입 도구라고 말한 부분은 이해했어! 하지만 그게 뭐 어쨌는데? 분류상 그런 것일 뿐 현실은 달라! 어쨌든 우리는 Context로 상태 관리를 하고 있고, 그게 잘못된 건 아니잖아?

맞다. 이 의견에는 백분 동의한다. 미역이 사실은 식물이 아니라는 둥의 분류학상 이야기를 해봤자 우리는 생일날 미역국을 맛있게 먹을 것이다. 그것이 우리 현실에 어떠한 영향을 끼치지는 못한다. 하지만 그렇다고 완전히 무시할만한 이야기는 아니다. Context는 왜 상태 관리 도구가 아니며, 그럼에도 Context가 가지는 차별점은 무엇인지를 이해하는 것은 앞으로 코드를 설계하는데 어떤 식으로든 도움이 될 수 있기 때문이다.

만약 이런 부분에 대해서 깊은 이해와 고민 없이 코드를 작성한다면 이미 발명된 바퀴를 굳이 다시 만들어 삐걱거리는 마차를 타고 여정을 떠나는 사람과 같이 어리석은 선택을 할 수 있으며, 반대로 Context를 써야할 곳에 상태관리 도구를 적용하면서 과도한 확장성과 접근성으로인해 프로젝트에 잠재적 위험성을 더하는 결과를 초래할 우려가 있다.

그럼 Context API는 필요 없는가.

이야기가 잠깐 샜는데, 다시 본론으로 돌아와보자. Context가 단순히 상태를 주입하는 역할밖에 하지 않는다면 이런 질문이 생길 수 있다.

어쨌든 Context는 하위 컴포넌트에서 상태에 접근할 수 있게 도와주는 기능밖에 없다는거 아니야? 다른 라이브러리를 쓰면 그 기능도 당연히 제공하는데 그럼 Context는 도대체 무슨 쓸모가 있는거야?

Context는 상태 주입 밖에 못하는데, zustand는 상태 주입 + 온갖 기능을 다 제공한다. 대충만 생각해봐도 Context는 더이상 설자리를 잃은 것처럼 보인다. 하지만 현실은 다르다. 우리는 생각보다 많은 곳에서 Context를 사용하고 있고, 사용할 여지가 있다. 이는 Context만이 가지는 두 가지 특징 덕분이다.

특징 1. 의존성이 없다.

emotion을 사용한다면 useTheme이라는 hook에 대해 알고 있을 것이다. useTheme은 컴포넌트에서 theme 정보에 접근할 수 있게 해주는 역할을 하며, 내부적으로 context를 사용해 구현되어 있다. react-navigation의 useNavigation도, react-query의 useQueryClient도 모두 Context를 사용한다. 왜 이들은 zustand나 recoil을 쓰지 않고 Context를 사용했을까?

이는 반대로 생각하면 이해하기 쉽다. emotion의 개발자가 zustand를 좋아해서 useTheme을 zustand로 구현했다고 해보자, 그럼 emotion을 사용하는 사람은 필연적으로 zustand도 함께 설치해야만 한다. 문제는 모든 라이브러리 개발자가 선호하는 상태관리 도구가 다를 것이라는 점이다. 결과적으로 내 프로젝트는 온갖 상태관리 라이브러리를 모두 설치해야하고, 필요 이상으로 무거워지고 의존성 관리도 굉장히 힘들어 질 것은 불보듯 뻔하다.

여기서 Context가 빛을 발한다. Context는 react core에 기본 탑재되어 있다. 이 말은 react를 사용하고 있다면 Context를 사용하기 위해 추가적인 패키지를 설치할 필요가 없음을 의미한다. 이런 이유에서 라이브러리 개발 등 다른 환경에서 구동될 필요가 있는 로직의 경우 의존성 이슈가 없는 Context API를 선호하는 것이다.

특징 2. 상태를 공유할 Scope를 정의할 수 있다.

상품등록 기능을 구현한다고 가정해보자. 요구사항이 꽤나 복잡해서 거대한 상태와 수많은 액션이 다양한 컴포넌트와 결합되어 동작해야하는 상황이다. 대충 생각해보다가 머리가 아파와 상태를 전역으로 관리하고 각 컴포넌트에서 접근해 사용하기로 결정했다. 이 때 예상되는 문제가 몇가지 있다.

  1. 상품 등록 페이지 진입 또는 이탈 시 전역 상태 초기화 로직을 추가해야 한다. 만약 클린업이 제대로 이루어지지 않을 시 상태가 남아 어떤 영향을 끼칠지 모른다.
  2. 상품등록 페이지 내부 뿐만 아니라 프로젝트 어디서든 이 전역 상태에 접근하는게 가능해지며, 이는 예상하지 못한 사이드 이펙트를 초래할 가능성이 있다.
  3. 국소적으로 쓰이는 상태까지 전역상태로 뺄 경우, 무엇이 실제로 전역에서 쓰이는 데이터이며, 무엇이 특정 범위에서만 사용하는 데이터인지 분간이 잘 되지 않아 컨텍스트를 모르는 제 3자가 코드를 파악하기 어려워진다.
  4. 만약 여러개의 상품등록을 병렬적으로 작성해야하는 요구사항이 발생할 경우 대응하기 까다로워진다.

이런 상황에서 대안으로 고려해볼 수 있는 것이 Context다. Context는 Prodiver 하위 스코프에서만 상태를 공유하며, 상품 등록 페이지 진입시 초기화한 상태를 Provider로 넘겨주기 때문에 상기 언급한 이슈에서 모두 자유롭다.

사실 이런 지점 때문에 전역 상태 관리 라이브러리와 Context API를 함께 사용하는 케이스도 종종 권장되고는 한다. 대표적으로 zustand 공식 문서에서 소개하고 있는 createContext가 그러하다. 문서에서 설명하는 것과 같이 zustand와 context를 함께 사용하면 상태관리 도구의 이점과 스코프기반 context 이점을 모두 취할 수 있다. (코드중복을 이유로 zustand에서 직접 제공하는 createContext는 다음 버전에서 제거 예정이지만 react context를 이용하여 사용자가 쉽게 구현할 수 있는 형태이며, 그게 귀찮다면 서드파티 형태로도 제공된다.)

결론

Context API와 전역 상태 관리 도구는 적재적소에!

지금까지 Context API와 상태관리 라이브러리에 대해서 알아봤다. 이처럼 두 범주는 서로 비슷한듯 다르기 때문에, 적재적소에 적절한 도구를 선택해 적용하는 것이 중요하다. 내가 작성중인 코드가 다른 환경에서 구동될 여지가 있다면 되도록 의존성 문제에서 자유로운 Conext API를 적용하고, 그럴 여지가 없는 프로젝트라면 이미 모든 것이 잘 구축된 상태관리 라이브러리를 사용하는 것을 권장한다. 특정 스코프에만 상태를 주입해야하는 경우에는 Context API를 쓰되, 경우에 따라 두가지를 함께 씀으로써 장점만을 취할 수도 있다.

상태관리 라이브러리 도입을 적극 고려해봐도 좋다.

간혹 Context API만으로 프로젝트를 구성하시는 분들을 봤는데, 소신 발언을 해보자면 사실 의존성 문제만 아니라면 외부 라이브러리 도입은 안할 이유가 없다고 생각한다.

어떤게 좋은지 몰라서 도입을 못하겠다는 분들은 보통 미래에 라이브러리 교체에 들어가는 유지보수 비용을 생각하시지만, 어차피 Context API를 사용하든 다른 도구를 사용하든 그 교체에 들어가는 공수는 크게 다르지 않다고 생각한다. 뿐만 아니라 최근 나온 도구들은 러닝커브가 굉장히 낮기 때문에 코드를 파악하고 새로운 기술로 교체하는 작업이 그렇게 어렵지만도 않다. react query 등 서버 상태 관리 도구의 대중화로 로컬 상태 관리 소요가 크게 줄어든 것도 난이도를 낮추는데 큰 기여를 했다.

일부에선 Context API만으로 충분해서 도입을 안하신다는 분들이 있는데, 그것이 정확한 판단이라면 이 선택은 유효하다. 하지만 많은 경우 프로젝트 사이즈를 잘못 판단하거나, Context를 사용할 때 무의식적으로 많은 로직을 작성해서 상태를 주입하기도 한다는 점을 생각해보면 한번 더 진지하게 고민해볼 필요가 있다. 우리가 Context와 함께 한땀한땀 짜넣는 기능 중 많은 부분을 상태관리 라이브러리는 최적화까지해서 제공하고 있을 확률이 높기 때문이다.

물론 현재 나와있는 상태관리 라이브러리의 철학이 모두 마음에 들지 않아 어떤 것도 쓰고 싶지 않은 거라면 도리가 없다. 또는 진짜로 Context만으로 충분하다거나 Context만을 사용해서 프로젝트를 구성하는 자신만의 도전과제가 있다면 라이브러리 없이 Context만으로 프로젝트를 만들어 나가시면 된다. 하지만 그런게 아니라면 전역 상태관리 도구 도입을 조금 더 적극적으로 고려해봐도 좋다고 생각한다.

상태 공유는 소극적으로! 가능하면 props만으로 구현하자!

이쯤에서 개인적인 의견을 조금 더 어필해보자면, 내가 생각하는 베스트는 어떤 도구도 쓰지않고 깔끔한 구성으로 구현될 수 있도록 프로젝트를 설계하는 것이다. 위에서 소개했던 상품등록 기능 예시만봐도 아마 설계에 조금만 신경썼다면 충분히 props만으로 구현이 가능했을 확률이 높다.

내가 느끼기에 안타까운 오해중 하나가 바로 props drilling은 악이라는 인식이다. 전역상태 분리의 기준이 props drilling이라고 주장하며, depth가 3단계 이상이라면 무조건 전역상태로 뺀다는 분까지 본 적이 있는데, props drilling 자체가 문제도 아니거니와, 그 문제를 해결하는 방법도 어긋나 있다고 생각한다. 물론 드릴링이 과도하면 코드가 어지럽고 읽기 불편하지만 props는 직관적으로 데이터의 흐름을 파악할 수 있다는 장점도 있다. 실제로 코드를 짜다보면 어떠한 맥락도 없이 마법처럼 상태를 전해주는 전역상태들이 오히려 악으로 보이는 경험을 하기도 한다. 그런 의미에서 내가 생각하는 이상적인 모습은 먼저 적절한 수준에서 props만으로 구현 가능한 구조를 추구하는 것이다.

만약 전역 상태를 쓰고 싶은 유혹이 든다면 다음과 같은 사고 흐름을 시도해보는 것을 추천한다.

  1. 반드시 상태 주입이 필요한 케이스인지 판단한다. 컴포넌트 구조 설계를 다시 함으로써 과도한 props drilling 없이 props만으로 충분히 구현 가능하다는 판단이 선다면 구조를 변경한다.

  2. 전역 상태 도입이 필요하다는 판단을 내렸다면, Context API가 더 적절한 케이스인지 판단한다. 만약 국소적인 컴포넌트가 복잡하게 얽혀 있어서 해당 스코프에서만 사용하는 상태를 공유하는게 목적이라면 Context API를 사용하는 것이 적절하다.(추가적인 관리상 이점이 필요하다면 상태관리 라이브러리와 함께 사용할 수도 있다.)

  3. 프로젝트 전반에 걸쳐서 접근해야하는 상태라면 전역상태관리 도구를 적용한다.

상태 관리 도구 도입은 적극적으로! 상태 공유는 소극적으로! 필요하다면 적재적소에!

profile
React-Native 개발블로그

4개의 댓글

comment-user-thumbnail
2024년 9월 5일

저는 앱과는 분리된 패키지가 react 의존성을 가지고, dynamic하게 변하지 않는 global state주입이 필요한 모듈개발할때 몇번 사용했습니다. (config, api key 같은 성격의 값을 주로 의존성 주입을 했던것 같아요.)
값을 dynamic하게 변할 시나리오가 없어서 굳이 상태관리라이브러리 의존성을 추가할 필요가 없다고 판단을 했었어요.
정답은 없는 문제이지만 "depth가 3단계 이상이라면 무조건 전역상태로 뺀다"라는 말이 저는 공감이 가긴합니다.
보통 depth 3단계 이상이라면 수직뿐만아니라 수평적으로 여러 곳에 쓰이고 있을 확율이 컷던 것 같아 global state로 관리하고, 자주쓰인다면 별도 hook으로 만들어두긴했습니다. 직관성이 떨어지는 것도 이해는 되지만 comment나 readme를 꼼꼼하게 써서 코드를 읽는 사람에게 조금이나마 부담을 덜어드리려고 했습니다

1개의 답글