createBrowserRouter로 라우팅을 하면 loader라는 속성을 제공한다. 해당 속성을 사용하면 페이지가 라우팅 되는 시점에 곧바로 loader에 등록해 둔 메서드가 실행돼서 서버의 데이터를 받아올 수 있다. 데이터는 컴포넌트에서 useLoaderData 훅을 사용해 받아올 수 있다.
useEffect의 의존성 배열로 빈 배열을 넘겨서 처음 컴포넌트가 렌더링 될 시 서버 데이터를 패치할 수도 있지만, 그렇게 되면 useEffect 실행 전에 한 번, useEffect 실행 후 서버 데이터로 상태를 변경한 이후에 또 한 번, 이렇게 총 두 번 렌더링이 발생한다. 하지만 loader와 useLoaderData 훅을 사용하면 라우팅 시점에 페이지의 컴포넌트를 렌더링하기 전에 데이터를 먼저 서버에서 받아오고, 서버 요청 처리가 끝나면 받아온 데이터를 바로 사용할 수 있기 때문에 렌더링이 한 번만 일어난다. 마치 컴포넌트에 Props로 데이터를 내려준 것과 비슷하게 동작한다.
사용자 이벤트에 따른 데이터 요청이라면 useEffect를 사용해야겠지만, 페이지에 처음 라우팅되는 시점에 데이터를 받아오는 건 loader와 useLoaderData를 사용하는 게 훨씬 유리하다. 잘 활용하자.
전역으로 관리하는 Context를 제공해줄 Provider 컴포넌트를 만들었다면 해당 위치에서 useLoaderData로 서버 데이터를 미리 받아오고, 감싸진 하위 컴포넌트들에는 Context로 데이터를 제공해 줄 수 있다.
공통 컴포넌트더라도 어디서 사용하든 필수적으로 사용해야 하는 데이터라면 Props가 아니라 직접 컴포넌트 내부에서 데이터를 받아오는 것도 나쁘지 않은 것 같다. 해당 컴포넌트가 특정 컨텍스트에 의존된다는 단점이 있고, 또 상위 컴포넌트에서 직관적으로 '여기에 어떤 데이터가 뿌려지겠다'라는 게 표현되지 않는다는 단점이 있지만, 그래도 필수적인 데이터를 하위 컴포넌트에서 다루면 코드가 좀 더 깔끔해지고 관리가 편하다는 장점이 있다고 생각한다.
라우팅 경로에 따라 위치할 컴포넌트를 표시할 때 react-router-dom의 Outlet을 사용하면 된다. createBrowserRouter에서 children을 사용한 경우 자식 요소에 경로에 따라 다른 컴포넌트를 보여줄 수 있으며, '다른 컴포넌트'의 위계를 표현할 때 Outlet을 사용한다.
Outlet은 자체적으로 context를 만들 수 있고, 위치할 컴포넌트들 중 상위에서 Props를 받고 싶은 위치에서 useOutletContext를 사용해 데이터를 받아올 수 있다. useOutletContext는 react-router-dom에서 제공되는 훅이다.
클라이언트와 서버의 상태를 언제 어떻게 동기화할지가 고민이다. 일단은 클라이언트에서도 상태를 계속 추적하면서 관리하고, 변경될 시 즉각 서버에 요청을 보내 동기화하는 방식으로 구현했는데, 몇 가지 의문이 남아있긴 하다. 즉각 동기화를 하는 거면 클라이언트에서 굳이 상태를 들고 관리를 같이 해줘야 하는 건지(필요할 때마다 서버에서 전부 요청하면 되는 게 아닌지), 즉각 동기화가 아니라 어느 정도 쌓아뒀다가 동기화 요청을 보내서 요청 수를 줄이는 게 좋은지, 등등.
경험이 부족하니 뭐가 맞는지 모르겠다. 이것 저것 자료들 더 찾아보면서 공부해 봐야겠다.
모던 리액트 딥다이브
상태란 어떠한 의미를 지닌 값이며, 애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값을 의미한다.
클라이언트가 무거워지면서 관리해야 하는 상태가 많고 복잡해졌다. 상태를 어디에 저장할지, 접근 범위를 어디로 할지, 상태 변화에 따른 리렌더링은 어떻게 제어할지 등 상태를 잘 관리하는 것은 어렵지만 꼭 해야만 하는 미션이 됐다.
리액트는 데이터를 단방향으로 바인딩한다는 특성이 있고(부모에서 자식으로만 props 전달이 가능하다), 이러한 특징과 잘 어울리는 상태 관리 패턴이 Flux 패턴이다. Flux의 Action은 단방향으로만 동작하여 상태를 Dispatch 하고 관리한다.
리덕스(Redux)는 Flux의 컨셉을 차용하면서도 데이터의 흐름을 모델, 뷰, 업데이트 세 가지로 분류하고, 데이터 흐름을 단방향으로 강제해 애플리케이션 상태를 안정적으로 관리하려고 노력한 도구이다. 꽤 긴 기간 상태 관리의 표준 라이브러리처럼 많이 사용되었다.
Context API는 엄밀히 말하면 '상태 관리'가 아니라 '상태 주입'을 위한 도구이다. 상위 컴포넌트에서 관리되는 상태를 하위 컴포넌트에 전달할 때 Props Drilling이 발생할 수 있다는 문제가 있기 때문에, 상위 컴포넌트의 상태를 효과적으로 하위에 내려주는 것이 Context API의 목적이었다.
React Query와 SWR은 http 요청에 특화된 상태관리 라이브러리다. http 요청을 통해 받아온 상태 데이터들을 캐싱해주고, 데이터 사용이 필요한 시점에 API 요청을 다시 날리는 게 아니라 캐싱된 데이터를 사용할 수 있게 해 준다.
함수형 컴포넌트 진영에서 훅이라는 패러다임이 등장함에 따라, 훅을 사용해서 상태를 가져올 수 있는 다양한 라이브러리들이 등장했다. Recoil, Jotai, Zustand, Valtio 등 다양하다. 훅에 기반한 상태 관리 도구들은 전역 상태 관리 패러다임(Redux의 Store)에서 벗어나 원하는 만큼의 상태를 지역적으로 관리하는 데 유용하다.
useState의 등장으로 컴포넌트마다 관리돼야 하는 지역적인 상태를 쉽게 정의하고 관리할 수 있게 됐다. 특별히, 커스텀 훅까지 함께 사용하면 여러 곳에서 사용되는 동일한 상태의 인터페이스를 미리 정의해 두고 가져다 쓸 수 있게 돼서 편리하다.
지역 상태는 전역 상태와 달리 독립적이라는 장점이 있지만, 한편으론 지역 상태라는 것이 단점이 되기도 한다. 경우에 따라선 여러 컴포넌트에서 각자에게 필요한 지역 상태를 많이 만들수록 상태를 동기화하는 게 어렵고, 다양한 상태가 파편화될 수 있다는 단점도 존재한다.
두 개 이상의 컴포넌트에서 같은 상태를 참조하도록 하는 가장 단순한 방법은 두 컴포넌트의 공통 조상 컴포넌트에 상태를 정의하고 Props로 값을 내려주는 것이다. 그러나 이러한 방식은 컴포넌트의 깊이가 깊어질수록 Prop을 너무 많이 전달해야 하고, 유지보수도 어렵다는 큰 단점이 있다.
상태를 관리한다는 것은 useState나 useReducer 등으로 생성한 지역 변수가 좀 더 전역적으로 공유될 수 있게 하는 것, 상태의 변경 시 해당 상태를 참조하는 컴포넌트를 성공적으로 리렌더링 해주는 것, 그리고 이 과정을 좀 더 수월하게 할 수 있도록 해주는 것을 의미한다. 여러 상태 관리 도구들이 풀고 싶은 문제도 결국은 어떻게 하면 이 과정들을 더 효과적이고 안정적으로 할 수 있을지에 대한 부분이다.