Programming/React

React에서 상태관리하기 (feat. Context API, Redux, React Query)

mingule 2022. 2. 27. 16:33

최근에 프론트엔드 상태관리에 대해 같이 일하는 인턴분들과 잠깐 이야기 할 일이 생겼다! 

요즘 정말 많은 상태관리 도구들이 있는데, 이 중에서 무얼 어떻게 선택해야할 지 잘 모르겠다는 말에 정말 공감하기도 했고, 

또 막연하게 알고있던 내용들이 말하면서 제대로 정리가 되지 않는다는 느낌을 받아서 한번 조금 정리해보면 좋을 것 같다는 생각에 글을 쓰게 되었다. 그리고 요즘 본딩 개발을 하면서 빠른 개발을 위해 내가 가장 잘 사용하는 Context API를 택해서 개발을 하고 있는데, 추후에 Recoil이나 Redux 등의 상태관리 툴을 도입해보려고 한다.(언제가 될지는 모름^^...) 뭐.. 그래도 미리 한 번 정리해 둘 겸 해서 오랜만에 글 남겨본당. 호홋

맨날 글써야지 써야지 하고 끄적끄적 제목만 적어두고 못 쓴 내용이 많다. 반성해야지 흐규흐규!!

 

프론트엔드에서 상태란 무엇인가?

먼저, 상태의 뜻에 대해 알아보자. 상태란, 사물이나 현상이 처해있는 형편이나 모양을 뜻한다. 이것만 보면 사실 감이 잘 안온다.

우리가 일상생활에서 접하는 다양한 화면들을 떠올려보며 상태란 무엇인가에 대해 생각해보자. 

출처: google.com
https://reactjs.org/docs/faq-state.html

- 요약) State은 plain Javascript Object that influences output of render and is managed within the component.

- 상태는 컴포넌트 내부에서 관리되며 어플리케이션의 렌더에 영향을 미치는 플레인 자바스크립트 객체이다. 

 

공식 문서에 나와있는 상태에 대한 이야기인데, 축약하자면 "어플리케이션의 화면에 영향을 끼치는 자바스크립트 객체" 라고 보아도 무방하다.  보통은 이를 통칭해 "변화하는 데이터" 라고 일컫는다. 사용자와의 인터랙션을 통해 동적으로 계속해서 변화하는 데이터이다. 

 

내가 쓱닷컴에서 어떤 물건을 장바구니에 담고, + 버튼을 눌러 수량을 변경했을 때, 1개에서 2개로 변하는 것, 인스타그램에서 내가 누군가를 팔로우했을 때, 팔로워 수가 1이 증가하는것 모두 상태가 변화하는 것이라고 부를 수 있다.

 

이렇듯 우리는 정말 많은 곳들에서 이런 상태들을 알게모르게 변화시키고있다.

 

웹 사이트가 커질수록, 또 복잡해질수록 이런 상태들은 점점 많아지고, 또 서로 의존하게되며 관리하기 어려워진다.

단순히 인스타그램의 내 피드에만 들어가봐도, 정말 많은 상태들이 관리되고 있는 것을 알 수 있다.

내 인스타 아이디, 내가 팔로잉하는 사람들 수, 나의 팔로워 수, 내 피드에 공유된 포스트들, 스토리들, 알림 등.. 정말 많다.

이런 상태들은 일관적이어야한다. 어떤 말이냐면, 서로 다른 컴포넌트에서 동일한 상태를 다룬다면, 그 출처가 같아야한다는 것이다. 

예를 들어, 인스타 피드에 공유된 포스트들을 보여주는 컴포넌트가 있고, 그 포스트들의 개수를 나타내주는 컴포넌트가 있다고 하자.

두 가지 컴포넌트 모두 결국 "내가 올린 포스트"라는 데이터를 가져와 사용한다. 그런데 만약에 내가 새로운 포스트를 하나 올렸다고 하자. 그런데 피드에 보여지는 포스트는 11개인데, 그 개수를 나타내는 컴포넌트는 10개를 나타낸다면, 나는 혼란을 느낄 것이다.

"제대로 올라간게 맞나?!! 뭘 믿어야하지?!"

 

그렇기 때문에 상태의 일관성, 즉 데이터의 무결성은 정말 중요하다.

만약 서로 다른 여러개의 컴포넌트들이 어떤 동일한 상태를 다루면, 그 데이터의 정확성을 보장하기 위해 데이터의 변경을 제한해 데이터의 상태를 항상 같게 유지해야한다. 이런 데이터의 무결성을 위해 Single source of Truth(신뢰할 수 있는 단일 출처, 즉 동일한 데이터는 항상 같은 곳에서 데이터를 가지고 온다)라는 방법론이 생기기도 했고, React에서도 이 방법론을 택해서 사용하고 있다. 우리가 useState를 통해서만 state를 변경시켜야하는 이유를 여기에서 추측할 수 있다.

 

상태의 종류

상태는 크게 범위의 개념에서 전역상태(Global State)와 컴포넌트 간 상태(Cross Component State), 그리고 지역상태(Local State), 세가지로 나눌 수 있다.

 

지역상태는 특정 컴포넌트 안에서만 관리되는 상태를 뜻한다. 다른 컴포넌트들과 데이터를 공유하지 않는다. 예를 들면, input, selectbox 등에서 사용자의 입력값을 받는 경우가 있다. (보통 Form 데이터들이 지역상태에 속한다.)

 

컴포넌트 간 상태는 여러가지 컴포넌트에서 관리되는 상태를 나타낸다. 다수의 컴포넌트에서 쓰이고, 또 영향을 미치는 상태를 뜻한다. 프로젝트 곳곳에서 쓰이는 모달을 예로 들 수 있다. 보통 상위 컴포넌트에서 하위 컴포넌트로 prop을 넘겨 해당 컴포넌트까지 전달되도록 하는 Prop Drilling 방식을 필요로 한다.

 

전역 상태는 프로젝트 전체에 영향을 끼치는 상태이다. 예를 들면, 유저 기능을 생각하면 된다. 이 또한 Prop Drilling 방식을 활용해서 부모에서 자식으로 데이터를 전달한다.

 

Prop Drilling이란, props를 오직 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트를 거치며 컴포넌트에서 다른 컴포넌트로 데이터를 전달하는 과정이다.

[상위 컴포넌트 > 중간 컴포넌트 > 중간 컴포넌트 > ... > 타겟 컴포넌트] 이런 식이다.

이런 prop 전달 과정이 많지 않다면 큰 걱정은 없지만, 만약 어플리케이션이 커져서 정말 많은 과정을 거치게 되어야 한다면, 그 코드의 prop을 추적하기는 정말 힘들 것이다. 

 

상태관리는 왜 필요한가?

서로 다른 두 컴포넌트에 같은 데이터가 필요하다고 할 때, 각 컴포넌트가 부모자식 관계로 되어있지 않은 이상, 각 컴포넌트 간의 직접적인 데이터 전달은 어렵다. 데이터를 부모 컴포넌트로 보내고, 다시 그 상데이터 필요한 컴포넌트로 전달해야한다. 하지만 이렇게 Prop Drilling이 많아지면 이 prop이 어디에서 왔는지 확인하기 정말 어려워진다. 

물론 상태가 어디에서 왔고, 어디를 거쳐서 어디까지 갔고.. 이런걸 다 기억하고 있을 수 있다면 정말 좋겠지만, 상태가 많아지고 어플리케이션이 복잡해질수록 prop을 추적하기는 점점 힘들어질 것이다. (나는 어제 쓴 코드도 까먹는데 하하)

그렇기때문에 각 어플리케이션에 알맞은 상태관리 툴을 선택해 상태를 잘 관리하는 것이 중요하다.

 

상태관리를 위한 툴

그럼 이렇게 상태를 관리할 수 있는 Tool에는 무엇이 있을까? 

상태관리를 위한 도구는 정말 다양하다. 

나는 그 중에 가장 대표적이고 또 많이 쓰이는 Context API, Redux, React Query에 대해 소개해보겠다.

 

Context API

https://ko.reactjs.org/docs/context.html

 

Context – React

A JavaScript library for building user interfaces

ko.reactjs.org

Context API는 리액트에서 만든, React 컴포넌트 트리 안에서 전역 상태를 공유할 수 있도록 만들어진 방법이다. 

"전역 상태관리 도구"라고 말하기는 살짝 애매한 감이 있는데, 이는 Context API는 아무것도 관리하지 않기 때문이다. 

Context API는 종속성을 주입하기위한 도구일 뿐이다. React에서의 실질적인 상태관리는 useState, useReducer를 통해 일어난다.

보통 "상태관리" 라고 함은, 변화하는 데이터들을 관리하는 것인데, 상태의 초기 값을 저장하거나, 현재 상태의 값을 읽거나, 새로운 데이터로 상태를 업데이트 하는 등의 행위를 뜻한다. 하지만 Context는 이런 상태들을 직접적으로 관리해주지 않고, 단순히 이미 존재하는 상태를 다른 컴포넌트들과 쉽게 공유할 수 있게 해주는 역할을 한다. 

 

이런 Context API는 중간에 있는 Element들에게 props를 넘겨주지 않고도 데이터를 가져다가 사용할 수 있기 때문에 (Prop Drilling을 피할 수 있다.) 테마나 언어 등 전역적으로 쓰이는 데이터들을 사용할 때 자주 쓰인다.

다만 Context를 사용하게되면 컴포넌트를 재사용하기가 매우 힘들어지기때문에, 마구 사용하는 건 지양해야한다. 

 

Context API는 Context, Provider, Consumer로 설명할 수 있다.

Context는 전역 상태를 저장하는 곳이다. Context 내부에 Provider와 Consumer가 정의되어있고, Consumer는 Context를 통해서 상태에 접근이 가능하다. 

 

Provider는 전역 상태를 제공하는 역할을 한다. Context에 상태를 제공해서 다른 컴포넌트가 상태에 접근할 수 있도록 도와준다. 제공된 상태에 접근하기 위해서는 Provider 하위에 컴포넌트가 포함되어있어야한다. 보통 모든 컴포넌트에 접근해야하는 상태를 제공하기 위해서는 Root Component (index.js / app.js)에서 Provider를 정의한다.

 

Consumer는 제공받은 전역 상태를 받아서 사용하는 역할을 한다. Context는 Consumer 사이에 있는 첫 객체를 Context에 인자로 전달하기 때문에, 바로 JSX를 작성하면 안되고, 빈 객체를 작성하고나서 JSX를 작성해야한다.

 

이런 Context API는, 그 범위를 제대로 지정하지 않는다면, 불필요한 rerender를 일으키기도 한다. 각 Context를 잘 나누고 또 필요하다면 useMemo 등을 사용해 코드를 작성하는 것이 좋다. 하지만 useMemo 또한 연산이 들어가는 작업이기 때문에, 정말 필요한 곳에 잘 사용하는 것이 중요하다. 곧 살펴볼 Redux도 코드를 뜯어보면 Context API를 기반으로 만들어져있다고 한다. (Context API를 사용하면서 이것저것 신경쓰느니 차라리 Redux를 사용하는 편이 나을지도..?!)

 

 

 

Redux

https://ko.redux.js.org/introduction/getting-started/

 

Redux 시작하기 | Redux

소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

ko.redux.js.org

Redux는 전역 상태관리를 위한 도구이다. 어플리케이션 전체에 대한 중앙 저장소 역할을 한다. 

Redux 공식 페이지에보면, Redux는 "자바스크립트 앱을 위한 예측 가능한 상태 컨테이너"

"액션"이라는 이벤트를 사용해서 어플리케이션의 상태를 우리가 예측 가능한 방식으로 업데이트 할 수 있게 도와준다. 

Redux는 라이브러리이기 때문에 다양한 UI Framework / Library들과 함께 사용이 가능하다.

비단 React 뿐만아니라 Vue나 Vanilla JS 등에서도 사용이 가능하다는 뜻이다. 

처음에는 Redux가 React와 어떤 연관성이 있는 라이브러리라고 생각했는데, 별 상관 없다. 그냥 Redux는 모든 자바스크립트 앱을 위한 상태관리 도구일 뿐이다. 헷갈리지 말자!

 

React에서 Redux를 사용할 때, 보통 React-Redux라는 라이브러리를 함께 설치해서 사용한다.

Redux도 설치하면서, React-Redux는 왜 또 설치해야할까? 쉽게 설명하자면, React와 Redux를 연결해주는 라이브러리라고 보면 된다. 

아래 공식 문서에 자세히 나와있지만, 요약하자면 React-Redux는 UI 바인딩을 도와주는 라이브러리라고 한다. 쉽게 말하면 그냥 Redux와 React를 연결해준다는거다. 

https://react-redux.js.org/introduction/why-use-react-redux

 

Why Use React Redux? | React Redux

Introduction > Why Use React Redux: benefits of using React Redux in a React app

react-redux.js.org

 

어쨌든, Redux는 크게 Store, Reducer, Action으로 설명할 수 있다.

Store는 저장소, 즉 전역상태를 저장하는 공간이다. 자바스크립트 객체 형태로 저장되어 있으며, 오직 Reducer를 통해서만 접근할 수 있다. 보통은 최상단의 index.js에 정의한다. 여러개의 Context를 만들 수 있는 Context API와는 다르게 Redux에서 Store는 1개만 존재할 수 있다. 

 

Action은 Reducer에게 보내는 Store에 대한 행동을 정의하는 자바스크립트 객체이다. 우리는 상태에 어떤 변화가 필요할 때, 액션을 발생시킨다. "이렇게 상태를 변경해줘~" 라고 하는 어떤 주문서의 역할을 한다고 보면 이해가 쉽다.

Action을 Reducer에게 전달하기 위해서는 dispatch 메소드를 사용해야한다. dispatch는 Store의 내장 함수 중 하나인데, 액션 객체를 넘겨줘서 상태를 업데이트한다. 이벤트를 일어나게하는 이벤트 트리거의 역할을 한다. 

 

Reducer는 이전 상태와 액션을 받아, 다음 상태를 반환하는 역할을 하는 순수 함수이다. Reducer를 통해서만 전역 상태를 변경하고 업데이트할 수 있다. 어떤 액션이 들어오는지 그 유형에 따라 이벤트를 처리하는 이벤트 리스너라고 볼 수 있다. 여기서 중요한 점은, 이전 상태를 변경한다는 점이 아니라, 새로운 상태 객체를 생성해서 반환해야한다는 사실이다. 

*순수함수란, 다른 외부의 상태를 변경하지 않으면서도, 어떤 동일한 인자에 대해 항상 동일한 값을 리턴하는 함수이다.

 

사실 요 세가지는 Redux의 이해를 돕는, 전반적인 부분에 대한 내용일 뿐이다. 이 포스트의 중심은 상태관리 도구들의 비교에 중심을 맞추어 작성했으니, Redux의 더 자세한 내용을 알고싶으신 분들이라면, 요 미디엄 글을 읽어보시는 것도 좋을 것 같다. Redux가 어떻게 상태를 관리하는 지에 대한 큰 흐름, 그리고 코드단계에서 Redux가 어떻게 쓰이는지까지 잘 작성되어있다.

 

Redux는 Context API와 비교했을 때 보다 더 폭넓은 기능을 제공한다. 

아래 Dan Abramov의 글인 미디엄에 자세히 나와있다. 해당 글에서 Redux가 Context API에 비해 가지는 8가지 강점들에 대해 발췌해왔다. 

1. 로컬스토리지에 상태를 영속적으로 저장하고, 시작할 때 다시 불러오는 것에 뛰어나다.

2. 상태를 Server에서 미리 채워서, HTML에 담아 Client로 보내고, 앱을 시작할 때 다시 불러오는 것에 뛰어나다.

3. 사용자의 액선을 직렬화(Serialize)해서, 상태와 함께 자동으로 버그 리포트에 첨부할 수 있고, 개발자들은 이를 통해 에러를 재현할 수 있다. 

4. 액션 객체를 네트워크를 통해 보내면, 코드를 크게 바꾸지 않고도 협업 환경을 구현할 수 있다.

5. 코드를 크게 바꾸지 않고도 실행 취소 내역의 관리나 Optimistic Mutations(낙관적인 변경)을 구현할 수 있다.

6. 개발할 때, 상태 내역 사이를 오가고 액션 내역에서 현재 상태를 다시 계산하는 일을 TDD 스타일로 할 수 있다.

7. 개발자 도구가 완전한 조사, 제어를 할 수 있게 함으로써 개발자들이 자신의 앱을 위한 도구를 직접 만들 수 있게 해준다.

8. 비즈니스 로직을 대부분 재사용하면서 UI를 변경할 수 있게 한다. 

https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367

 

You Might Not Need Redux

People often choose Redux before they need it. “What if our app doesn’t scale without it?” Later, developers frown at the indirection Redux…

medium.com

Redux는 이와 같이 다양한 기능을 제공한다. 하지만 Context API에 비해 작성해야 하는 코드의 길이도 많고, 또 복잡하기 때문에 본인의 어플리케이션에 알맞은 상태관리 방법을 잘 고심해보고 선택해야 한다. 

 

아래는 npm trends 사이트에서 Hot한 상태관리 툴들의 다운로드 수를 비교해 본 것이다. 다양한 상태관리 툴들의 등장으로 Redux의 인기가 떨어지고 있다고들 하기는 하지만, 사실 뭐 아직까지 리덕스.. 가장 많이 쓰이긴 한다. ㅋㅋㅋㅋ 프론트엔드 개발자라면 꼭 한번 배워보는 것이 좋을 것 같다.

 

 

 

React Query

https://react-query.tanstack.com/

 

React Query

Hooks for fetching, caching and updating asynchronous data in React

react-query.tanstack.com

React Query, 요즘 정말 핫한(?) 라이브러리다. 전역 상태관리 라이브러리라고 보기보다는, 서버와 클라이언트 간 비동기 작업을 쉽게 다룰 수 있게 도와주는 라이브러리다. 서버상태를 관리하는 라이브러리라고 생각하면 된다. 프론트엔드에서 어려운 부분 중 하나가, 이렇게 비동기를 통해 서버 상태를 가져오고 관리하고 업데이트 하는 부분인데, 고 부분을 쉽게 해준다. 

보통 Redux와 같은 전역상태관리 라이브러리들이 클라이언트의 상태에 대해서는 잘 동작하지만, 서버 상태에 대해서는 그렇게 동작하지 못하기 때문에(명시적으로 fetch를 해야지 서버의 데이터를 최신으로 반영한다거나, 많은 컴포넌트에서 최신 데이터를 받아오기 위해 여러번 fetch를 한다던가.. 하는 등의 최적화가 어려워서), 이와 같은 서버상태관리 라이브러리가 나타나게 되었다고 볼 수 있다.

 

 

공식 문서에 따르면, 서버 상태는 다음과 같은 특징을 가진다고 설명한다. (백엔드 Database에 저장된 데이터라고 생각하면 편하다.)

- 어플리케이션 내에 속하지 않고, 그러므로 제어하지도 못한다. 보통은 원격에 위치한 곳에 저장되어있다. 

- 데이터를 가져오거나 업데이트를 하기 위해서는 비동기 API가 필요하다. 

- 나만 사용하는 것이 아니라, 다른 사람들과 함께 사용하기 때문에 언제 어떻게 Update 될 지 모른다.

- 그러므로 내가 당장 어플리케이션에서 사용하는 데이터가 Outdated 상태가 될 수 있다. 

  (클라이언트는 서버 데이터의 snapshot만을 사용하기 때문에, 클라이언트에서 보이는 서버 데이터는 항상 최신이라고 보장할 수 없다.)

 

서버 상태는 내가 직접 관리할 수 있는 것이 아니기 때문에, 데이터의 만료에 따른 업데이트나 캐싱, 서버 데이터를 호출하는 과정 등에 특별히 신경을 써가며 관리를 해야한다. 물론 신경을 쓰지 않아도 되지만 완성도 높은 어플리케이션을 만들기 위해서는 필요한 작업이라고 할 수 있겠다. 

 

React Query는 useQuery hook의 파라미터를 통해 다양한 기능을 제어할 수 있다.

- API 데이터의 만료시간

- 데이터를 캐시에서 유지할 시간

- 브라우저를 focus할 때, 데이터를 리프레시 할 것인지에 대한 여부

- 리프레시 간격

- 데이터 가져오는 것에 대한 성공, 로딩, 에러 콜백 등..

몇가지 기능들을 주절주절 적어봤지만, 뭐.. 정리하자면 데이터 Fetch, Load, Caching 등의 귀찮은 기능들을 모두 제공한다는거다. 

정말 간단한 코드 몇줄로 이게 모두 가능하다. 

 

이외에도 useMutation hook을 사용해 원격 데이터를 생성하거나 업데이트, 삭제하는 등의 기능도 편하게 사용할 수 있다. 정말 다양한 기능이 있으니 공식 문서를 꼭 살펴보면 좋을 것 같다. 

사용 방법에 대해서는, 아래 블로그들을 참고해보시는 것도 좋을 것 같고, 실제 서비스 단에서 어떻게 사용하는지에 대해서는 우아한형제들 기술블로그에 있는 Store에서 비동기통신 분리하기 포스팅을 참고해보아도 좋을 것 같다. 

 

https://maxkim-j.github.io/posts/react-query-preview

 

React-Query 살펴보기

React의 Server State 관리 라이브러리 React Query를 살펴봅니다.

maxkim-j.github.io

https://blog.rhostem.com/posts/2021-02-01T00:00:00.000Z

 

blog.rhostem.com

프론트엔드 웹 개발 기술 블로그

blog.rhostem.com

 

 

프론트엔드의 상태와, 그 상태를 관리하기 위한 세가지 상태관리 툴들에 대해 정리해봤다. 지금까지 정리되지않아서 애매모호했던 것들이 조금이나마 정리된 느낌이다! 호호호

나중에 본딩에 적절한 상태관리 툴을.. 적용하게되면 그때 또 관련된 내용을 포슷팅해야겠따. ㅎㅎㅎ