안녕하세요, 똑똑한개발자에서 프론트엔드 개발을 하는 이호균입니다.
리액트를 사용하여 어플리케이션을 만들다보면 자연스럽게 상태 관리를 하게 됩니다. 대개 redux, mobx와 같은 상태 관리 라이브러리를 이용하여 전역 스토어에서 모든 상태를 관리합니다. 예를 들면 이런 것들을 스토어에 저장합니다.
- ui 테마
- 폼 입력 데이터
- 사이드바 상태
- 그 외 서버로 부터 받아온 데이터
- 등등
이 모든 상태를 전역 스토에서 관리하는게 적절할까요? 상태를 관리하는 더 좋은 방법은 무엇일까요?
state란 무엇인가
상태 관리를 하는 방법에 대해서 이야기하기 전에, 우선 상태가 무엇인지 알아보겠습니다. React doc은 state를 다음과 같이 정의합니다.
state란 렌더링에 영향을 미치는 자바스크립트 오브젝트이다.
이 정의에 따라서 global state를 정의한다면 이렇게 해볼 수 있겠습니다.
global state란 어플리케이션 어디서든 접근할 수 있고, 그 변화에 따라 어플리케이션 전반의 렌더링에 영향을 미치는 자바스크립트 오브젝트이다.
전역 스토어에서 관리해야하는 상태가 얼마나 있을까요? 사실 생각해보면 어플리케이션 어디서든 접근해야하는 global state는 얼마되지 않습니다.
state의 유형
데이터에 대한 제어, 소유 여부에 따라서 server state와 client state로 구분할 수 있습니다.
server state
서버로 부터 불러오는 데이터를 말합니다. 클라이언트가 제어, 소유할 수 없기 때문에 서버로 부터 특정 시점의 데이터를 가져와 저장하여 사용합니다. 때문에 비동기적인 상태를 갖습니다.
client state
언어, ui 테마, 폼 입력, 사이드바 상태 등과 같이 클라이언트가 제어, 소유하는 데이터를 말합니다. 때문에 동기적인 상태를 갖습니다. client state는 다시 두 가지로 구분할 수 있습니다.
local client state
폼 입력, 사이드바 상태 등과 같이 하나 또는 인접한 컴포넌트들에서 이용되는 state입니다.
global client state
언어, ui 테마 등과 같이 어플리케이션의 여러 곳에서 사용되는 state입니다.
state 관리 방법
server state
server state를 전역 스토어에서 관리하는 것은 합리적으로 보입니다. 다음과 같은 이유를 생각해 볼 수 있습니다.
- state를 필요로하는 모든 컴포넌트에서 api 호출을 하는 것은 비용 낭비이며, ux 측면에서도 좋지 못하다.
- 여러 컴포넌트에서 state를 공유해야 한다면 prop drilling, composition을 이용해 해결하기는 무리가 있다.
전역 스토어에서 관리하면 불필요한 api 호출을 하지 않을 수 있고 접근도 훨씬 편리해 집니다. 여러모로 효율성이 향상됩니다. 전역 스토어에 저장된 server state를 마치 backend 상태의 cache 처럼 다루는 것입니다.
하지만 여기에도 몇 가지 문제점이 있습니다. 가장 큰 문제는 server state가 특정 시점의 backend(서버)의 상태일 뿐이라는 점입니다. server state와 상관없이 backend 상태는 얼마든지 변할 수 있습니다.
때문에 server state를 적절히 다루기 위해서는 다음과 같은 일들이 필요합니다.
- 캐싱
- 동일 데이터에 대한 중복 요청 제거
- 오래된 데이터 업데이트
- 데이터 변경 요청 이후 업데이트
- 등등
다행히도 이런 여러가지 사항을 직접 구현할 필요가 없습니다. react-query, swr과 같은 라이브러리를 사용할 수 있습니다. 두 라이브러리는 server state에 대한 패칭, 캐싱, 업데이트를 도와줍니다. 이외에도 요청 상태 처리, 요청 실패시 retry, window focus시 업데이트 등과 같은 비동기 요청과 관련된 여러가지 편리한 기능을 제공합니다.
개인적인 경험으로는 redux-saga를 이용하는 것보다 생산성이 향상되었을 뿐만 아니라, 코드량도 훨씬 줄어드는 효과를 봤습니다.
예를 들어 react-query를 이용하면 다음과 같이 작성할 수 있습니다.
import axios from "axios";
import { useQuery } from "react-query";
function Todos() {
const { isLoading, isError, data, error } = useQuery("todos", () =>
axios
.get("https://jsonplaceholder.typicode.com/todos")
.then((res) => res.data)
);
if (isLoading) {
return <span>Loading...</span>;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default Todos;
local client state
local state를 전역 스토어에서 관리하려면 boilerplate code가 필요합니다. 어플리케이션 이곳 저곳에서 사용되는 데이터도 아닌데, 이를 전역으로 관리하기 위한 번거로운 작업만 추가되는 셈입니다. 이 경우에는 React 내장 Hook인 useState와 useReducer를 쓰는 것으로 충분합니다.
또한 인접한 다른 컴포넌트에서 local state 접근이 필요하다면 prop drilling, lifting state up, composition을 이용해 간단히 해결할 수 있습니다.
global client state
global client state는 모두 Context를 이용해서 처리하면 됩니다. 대부분의 어플리케이션은 redux와 같은 전역 상태 관리 라이브러리가 필요하지 않다고 생각합니다.
마무리
리액트 어플리케이션의 상태는 server state와 client state로 구분할 수 있습니다. server state는 react-query, swr 같이 server state에 특화된 라이브러리를 이용합니다. local client state는 useState와 useReducer를 이용합니다. global client state는 Context를 이용합니다. redux, mobx와 같은 전역 상태 관리 라이브러리가 내 어플리케이션에 정말 필요한 것인지 고민해 봅니다.