Intro
안녕하세요! Playce Dev 팀 Frontend Engineer 강동희 입니다. 매주 팀 내에서 주최하는 Frontend Tech-day 에서 새로운 Frontend Architecture 설계를 준비하고 있습니다. 이번주 주제는 상태 관리 매니저 선정이었습니다. 현재 Playce Dev 팀 Frontend 에서는 상태관리 라이브러리로 Redux 를 사용하고 있습니다. Redux 를 사용하면서 마주치게 된 가장 큰 어려움은 커다란 보일러 플레이트였고, 이를 해결하기 위한, 혹은 이를 대체할 수 있는 라이브러리가 있을까? 에서 조사를 시작하게 되었습니다. 그럼 Recoil 에 대해서 깊지도, 얕지도 않게 조사했던 내용을 여러분들과 함께 나눠보도록 하겠습니다! 부담없이 즐겨주세요!
Recoil Overview
Recoil : React 에서 사용하는 상태관리 라이브러리
Version : 0.72 (2022년 6월 16일 기준)
Git : https://github.com/facebookexperimental/Recoil
npm Trends..
Recoil 만을 위한 글이지만, 해당 기술을 탐구하기 전에 같은 문제를 해결하기 위해 사용되고 있는 라이브러리와 비교를 하는것은 상당히 중요한 일이라고 생각합니다.
프론트엔드 개발을 하면서 state 를 관리하는 방법은 여러가지 방법이 있습니다.
첫 번째로, 라이브러리를 통해 관리 하지 않고 직접 state 를 관리하는 방법입니다. 이 방법은 작은 프로젝트에서 유효할 수 있습니다. 하지만 규모가 있는 애플리케이션에서 직접 state 를 관리하게 된다면 props drilling 이 심각하게 발생하거나, state 가 어디서 관리되고 있는지 개발자도 모르는 등, 커다란 문제에 직면할 수 있습니다
두 번째는 React 에서 자체적으로 제공하고 있는 Context API 를 사용하는 것입니다. React 팀은 16.3 버전에서 라이브러리를 사용하지 않아도 자체적으로 전역 상태를 관리할 수 있는 Context API 를 출시 했습니다. 그런데 React 팀에서 공식적으로 출시한 상태관리 API 가 있음에도 불구하고, 왜 많은 프로젝트에서 Redux 와 같은 상태 관리 라이브러리를 사용할까요? 그 이유는 Context API 를 사용할 때 익혀야 하는 기본적인 개념과 작성해야 하는 코드의 길이가 Redux 의 보일러 플레이트와 크게 차이나지 않기 때문입니다. 그렇기에 기존의 코드에서 벗어나 Context API 를 사용할 필요가 없는 것입니다.
세 번째는 상태관리 라이브러리의 사용입니다. React 에서 흔히 사용 되는 상태 관리 라이브러리는 react-redux 입니다. mobx 또한 많이 사용되지만, 이번 tech-day 에서 제외하기로 했기 때문에 조사하지 않았습니다. 마지막으로 비교적 최근에 나온 Recoil 은 페이스북에서 출시한 React 만을 위한 상태 관리 라이브러리 입니다.
사실 비교하는 게 무의미 할 정도의 격차를 살펴볼 수 있습니다. 라이브러리 간 격차가 왜 이렇게 두드러지게 보일까요?
- 출시일 Recoil 은 2020년 5월에 출시된 State Management Library 이다. Redux 7년 전에 개발된 라이브러리다. (세월의 차이)
- 익숙함 개발을 하는데 있어서 중요한 건 익숙함이다. 익숙함이란 숙련도 라고도 이야기 할 수 있을 것 같은데, Redux 가 오래된 만큼 팬 층이 깊고 숙련도 깊은 사람들이 많기 때문이다. 즉 요즘 말로 고인물 들이 많다.
개인적으로 두 가지 이유를 생각해 봤습니다.
이렇게 큰 격차가 남에도 불구하고, 해당 글에서는 아키텍처에 Recoil 을 사용하도록 여러분들을 설득하기 위해서 작고 귀여운 Recoil 에 대해서 탐구하고 소개해 보도록 하겠습니다.
React 에서 데이터의 흐름
React 가 추구하는 패턴
React 에서 데이터는 단방향으로 흐르며, 위에서 아래로 즉 부모에서 자식 컴포넌트로 흐릅니다. 이러한 방식은 Flux 패턴에 의해서 적용된 방식인데, 간단하게 Flux 패턴이 왜 적용이 되었는지 알아보도록 하겠습니다.
MVC 패턴
Flux 패턴은 MVC 패턴이 가진 문제점에서 시작했습니다.
위 사진은 기존의 MVC 패턴입니다. Model 에 데이터를 정의해 놓고, Controller 를 통해서 Model 의 데이터를 CRUD 작업하며, 변경된 데이터를 View 에 출력하는 식의 패턴입니다.
MVC 패턴은 Web 개발을 하는데 가장 많이 사용 되는 패턴입니다. 하지만 MVC 패턴에도 다양한 문제점이 존재합니다.
Web 애플리케이션이 커지면서 정의된 Model 과 이를 출력하는 View 가 다양해 졌습니다. 위 사진을 살펴보면 데이터의 흐름이 정말 많다는 것입니다. 어떤 데이터가 변경 되었을 시 해당 데이터를 사용하는 모든 곳에서 코드를 작성하고 변경해줘야 합니다. 만약 이런 과정을 생략 했을 시 예측하지 못한 부분에서 오류가 발생하거나 다르게 동작하는 등 Side Effect 가 발생할 수 있습니다.
Flux 패턴
페이스북에서는 이 같은 문제를 해결하기 위해서 Flux 패턴을 출시 했습니다.
간단하게 살펴보자면, Action 은 데이터의 상태를 변경하는 명령어입니다. Dispatcher 은 Action을 감지하여 Store 에 Action 을 전달해주는 역할을 합니다. Model 은 Store라고도 볼 수 있는데 state 가 저장되어 있는 공간입니다. Dispatcher 을 통해서 가져온 Action을 확인해 내부에 저장된 데이터를 변경합니다. 마지막으로 View 는 React 를 통해서 만드는 코드들입니다. Model 에 저장된 데이터를 가져와서 View 에 뿌려주고, View 는 해당 데이터들을 가지고 와서 화면에 렌더링 합니다.
쉽게 이해하기 위해서 보고 체계를 예시로 들어보겠습니다. 사령부에서 지시 (Action) 가 내려오고, 집배원 (Dispatcher) 이 해당 지시를 가지고 내려옵니다. 해당 지시를 통해서 병사는 창고 (Store) 에 저장된 물건 (데이터 혹은 state) 을 변경하여 꺼내서 훈련할 때 사용합니다. (View 에 렌더링 하는 과정을 비유)
Redux
Redux 는 Flux 패턴을 적용한 상태 관리 라이브러리 입니다. Store 에 상태들을 저장하고, 해당 어떠한 변화가 필요할 때 Action 을 Dispatch 하여 Reducer 에서 이를 받아 정해놓은 흐름으로 상태를 변화시키는 방식입니다.
왜 Redux 대신 Recoil 을?
위에서 살펴본 대로 Redux 는 React 에서 추구하는 데이터의 흐름을 그대로 구현해 놓은 라이브러리 입니다. 그렇다면 상태 관리 라이브러리를 선택할 때 Redux 대신 Recoil 을 선택할 이유를 생각해봅시다!
- Redux의 복잡한 코드
Redux 를 사용하고자 할 때 마주하는 가장 큰 어려움은 복잡한 코드다. Redux 를 활용하기 위해서는 action, dispatcher, reducer, store 등 구현해야 할 기본 코드 들이 큰 편이다. 이는 보일러 플레이트를 활용해서 해결할 수 있는 문제지만, 만약 여러 개발자가 공동 작업 할 때 컨벤션을 적용하지 않고 코드를 작성할 경우 자기만 알아볼 수 있는 구조의 코드를 작성하게 된다. - 간단한 Recoil 의 개념
Redux 를 이해하고 사용하려면 공부해야 할 것들이 많다. 데이터의 흐름을 추상화 하여서 익히려고 하여도 여러가지 복잡한 흐름을 이해하는 건 쉽지 않다. 이에 비해서 Recoil 에서 state 를 관리하는 방법은 굉장히 간단해 보인다. - 쉽게 사용하는 비동기 로직
Redux 에서 비동기를 활용하기 위해서는 middleware 을 활용한다. 비동기 통신을 한다면 통신의 결과가 Success 일수도 있고 Fail 일 수도 있다. 이를 구분하여 state 관리를 해야하는데, 이를 쉽게 하기 위해서 Redux 에서는 Redux-thunk 혹은 Redux-saga 같은 미들웨어를 활용한다. 하지만 Recoil 에서는 내장된 개념인 selector 을 활용해 추가적인 미들웨어의 사용 없이 쉽게 비동기 로직을 구현할 수 있다.
크게 위의 세 가지 이유를 생각해봤습니다. 자, 그럼 이제 여러분들이 Recoil 에 대해서 느껴볼 시간입니다!
Recoil 이 자신을 소개하는 문구
– 작고 React 스러운
” Recoil은 React처럼 작동하고 생각합니다. 앱에 추가하여 빠르고 유연한 공유되는 상태를 사용해 보세요. “
– 데이터 흐름 그래프
” 파생 데이터와 비동기 쿼리는 순수 함수와 효율적인 구독으로 관리됩니다. “
– 교차하는 앱 관찰
” 코드 분할을 손상시키지 않고 앱 전체의 모든 상태 변경을 관찰하여 지속성, 라우팅, 시간 이동 디버깅 또는 실행 취소를 구현합니다. ”
Recoil 시작하기
Recoil 을 시작하기 위해선 index.tsx (혹은 index.jsx) 에서 렌더링 하고 있는 root 를 RecilRoot 를 통해서 감싸줘야 합니다. 마치 Redux 에서 <Provider> 를 통해서 App 에 store 을 연결해주는 것과 비슷한 과정입니다. Redux 에서는 하나의 store 를 연결해주는 과정이지만, Recoil 에서는 atom 들이 떠다니는 Root 를 설정해준다고 추상화 하면 좋을 것 같습니다.
import * as ReactDOMClient from "react-dom/client";
import { RecoilRoot } from "recoil";
import App from "./App";
const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<RecoilRoot>
<App />
</RecoilRoot>
)
;
Recoil 의 핵심 개념
atom
Recoil 의 첫 번째 핵심 개념은 atom 입니다.
간단히 이해하고자 한다면 atom 을 비눗방울로 추상화 할 수 있습니다. 우리가 만드는 Web Application 을 구조화 한다면 그 구조의 상단에 atom 이 비눗방울 처럼 둥둥 떠다니고 있다고 추상화 할 수 있겠습니다. 만약 개발을 하다가 어떤 비눗방울 (상태) 이 필요하다면 해당하는 비눗방울만 쏙 빼서 쉽게 사용할 수 있습니다.
페이스북에서 Recoil 을 설명해주는 유튜브 영상에서 캡쳐해 온 화면 입니다. 위의 1,2,3,4,5,6 방울들이 atom 이며 아래 구조화 되어있는 우리의 Web 에서 해당하는 atom 을 사용하고 있습니다.
“오케이, 그럼 atom 은 알았는데 atom 에 무엇을 담는데?”
atom 에는 우리가 사용할 상태 (state) 를 담습니다. 쉽게 말하면, 우리가 전역적으로 사용하길 원하는 state 를 atom 이라는 비눗방울로 띄어서 어디서든지 사용할 수 있도록 만드는 것 입니다.
import { atom } from "recoil"
export const user = atom({
key: "user",
default: {
id: "Admin",
pwd: "Admin",
},
});
export const counting = atom({
key: "counting",
default: 0,
});
위 코드를 살펴보면, recoil 에서 atom 을 import 해왔고, atom 이라는 함수에 key 와 default 로 이루어진 객체를 넣어줌으로써 atom 을 만들었습니다.
여기서 key 는 atom 을 구분해줄 고유의 값이며, default 는 해당 key 값을 가진 atom 의 기본값으로 설정해줄 value 를 넣어주면 됩니다.
이렇게 간단한 코드로 하나의 전역 상태를 만들었습니다.
“오케이, 그럼 atom 을 어떻게 가져다 쓸 수 있는데?”
다섯 줄의 코드로 전역 상태를 만든 혁신적인 방법에 깜짝 놀랄 여러분을 위해 바로 코드를 통해 살펴보도록 하겠습니다.
import { useRecoilState } from "recoil";
import { counting } from "./store";
export function Example() {
const [count, setCount] = useRecoilState(counting);
const handleIncrease = () => {
setCount((prev) => prev + 1);
}
return (
<div>
<span>{count}</span>
<button onClick={handleIncrease}>increase</button>
</div>
);
}
위 코드를 살펴봤을 때 어떻게 atom 을 사용한 지 확인해보세요! 코드를 살펴봤을 때 useState 를 통해서 state 를 사용하는 모습과 비슷한 느낌입니다!
useRecoilState 라는 hook 을 recoil 라이브러리에서 가져와, 위에 정의한 atom 을 넣어주면서 값을 추적해 사용합니다.
이렇게 recoil 은 React 만을 위한 상태관리 라이브러리로서 가장 React 다운 상태관리 툴을 제공하고 있습니다. 그렇기에 useRecoilState 라는 간단한 hook 을 통해서 atom 을 가져와서 값을 추적하고, 값을 변경할 수 있는 것 입니다.
이렇게 짧은 과정을 통해서 전역 상태 관리를 하게되니, Redux 에서 사용했던 방식 (store 에 저장된 값을 가져오고, action 을 dispatch 해 reducer 를 통해서 state 를 변경 했던 과정) 에 아쉬움을 느끼게 됩니다.
import { useSetRecoilState, useRecoilValue } from "recoil";
import { counting } from "./store";
export function Example() {
const setCount = useSetRecoilState(counting);
const count = useRecoilValue(counting);
const handleIncrease = () => {
setCount((prev) => prev + 1);
}
return (
<div>
<span>{count}</span>
<button onClick={handleIncrease}>increase</button>
</div>
);
}
위 컴포넌트와 별 다른 구조는 없지만 atom 과 atom 의 modifier 를 분리해서 사용할 수 있다는 것을 소개하고자 예제 코드를 작성했습니다. 각각 useRcoilValue, useSetRecoilState 를 활용하면 이들을 분리해서 사용할 수 있게 됩니다.
atom with typescript
타입스크립트를 적용한 간단한 예제를 살펴보도록 하겠습니다!
//atom.ts
import { atom } from "recoil";
export interface IUser {
id: string;
pwd: string;
name: string;
}
export const user = atom<IUser>({
key: "user",
default: {
id: "admin",
pwd: "admin",
name: "관리자"
}
});
먼저 atom 을 타이핑 하기 위해서 IUser 이라는 interface 를 선언하였고, 해당 interface 를 user 이라는 atom 에 typing 했습니다.
import { useRecoilState } from "recoil";
import { IUser, user } from "./atom";
export default function App() {
const [LoginUser, setLoginUser] = useRecoilState<IUser>(user);
return (
<div>
<p>userName: {LoginUser.name}</p>
<p>userId: {LoginUser.id}</p>
<p>userPwd: {LoginUser.pwd}</p>
</div>
);
}
컴포넌트에서 atom 을 사용하는데 있어서도 마찬가지로 IUser 인터페이스와 타이핑 했습니다. 이는 React 에서 typescript 를 적용해 useState 를 활용하는 방법과 거의 유사합니다.
selector
Recoil 의 두 번째 핵심 개념은 selector 입니다.
저는 처음 Recoil 을 공부 했을 때 atom 은 정말 쉽게 이해했지만, selector 라는 개념은 이해하기가 어려웠습니다. 그 이유는 atom 에 비해서 조금은 머릿속에 추상화 하기 어려웠기 때문이지 않을까 라는 생각을 해봅니다. 결국 selector 을 이해하기 위해서 추상화 했던 방법은 SQL 의 select 질의문을 활용해 봤습니다.
selector 은 atom 을 활용해 개발자가 원하는 대로 값을 뽑아서 사용할 수 있는 API 입니다. Recoil 의 공식 문서에서 selector 을 소개하는 문구를 살펴보겠습니다.
Selector 는 파생된 상태(derived state)의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
즉 “ atom 을 원하는 대로 변형해 값을 리턴받는다. ” 라고 생각할 수 있겠습니다. 이 과정을 마치 데이터베이스에서 저장된 데이터를 Select 을 통해 원하는 결과를 뽑아오는 과정으로도 유추 해볼 수 있겠다 라는 생각을 하였고, 이를 Select 과 연결하여 추상화 하니 이해하기 편했습니다.
또한 selector 은 readonly 한 값 만을 반환합니다. 따라서 Recoil 을 활용할 때 수정 가능한 값을 반환 받고자 한다면 반드시 atom 을 활용해야 합니다.
selector 의 구조
function selector<T>({
key: string,
get: ({
get: GetRecoilValue,
getCallback: GetCallback,
}) => T | Promise<T> | RecoilValue<T>, // 타입 T에 해당하는 값, T를 리턴하는 Promise,
set?: (
{
get: GetRecoilValue,
set: SetRecoilState,
reset: ResetRecoilState,
},
newValue: T | DefaultValue, // setter로 전달하는 값은 T 타입 값이어야 한다.
) => void,
}
)
간단한 예제를 통해서 selector 을 확인해보자.
구조
- toDo 는 “DOING”, “DONE” 두 가지 상태를 가지고 있다. 현재 노출하기 원하는 상태는 atom 으로 관리한다.
- 생성된 toDo 는 현재 atom 에 배열로 담겨있다.
- 우리는 selector 을 통해서 status atom 을 추적하고, 원하는 상태의 값 만을 쏙쏙 골라올 것이다.
// atom.ts
import { atom, selector } from "recoil";
export type status = "DONE" | "DOING";
interface toDo {
status: status;
contents: string;
}
export const selectStatus = atom<status>({
key: "nowStatus",
default: "DOING"
});
export const toDos = atom<toDo[]>({
key: "toDos",
default: [
{ status: "DOING", contents: "default 1" },
{ status: "DONE", contents: "default 2" },
{ status: "DONE", contents: "default 3" },
{ status: "DOING", contents: "default 4" },
{ status: "DOING", contents: "default 5" }
]
});
export const selectToDo = selector<toDo[]>({
key: "selectToDos",
get: ({ get }) => {
const originalToDos = get(toDos);
const nowStatus = get(selectStatus);
return originalToDos.filter((toDo) => toDo.status === nowStatus);
}
});
위 코드를 살펴보면 toDos 라는 atom 에 toDo 배열을 담아놨고, selector 을 통해서 변화된 값을 리턴받아 사용하고 있습니다.
selector 의 구조를 살펴보면, atom 과 다른 부분이 있습니다. 위 코드에서 바로 get 이라는 코드를 살펴볼 수 있는데, selector 은 내부적으로 함수에서 get 을 반환 해주며 get 메서드를 활용해 현재 저장된 atom 이나 다른 selector 의 값을 받아올 수 있습니다. 이를 통해서 atom 을 input 받고 원하는 결과를 위해 배열을 변형해 output 해줍니다.
// App.tsx
import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { selectStatus, selectToDo, user } from "./atom";
export default function App() {
const [status, setStatus] = useRecoilState(selectStatus);
const selectToDos = useRecoilValue(selectToDo);
const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
setStatus(event.currentTarget.value as any);
};
return (
<>
<div>
<select value={status} onChange={handleStatus}>
<option value="DOING">DOING</option>
<option value="DONE">DONE</option>
</select>
<ul>
{selectToDos.map((toDo, index) => {
return (
<li key={index}>
<span>status: {toDo.status}</span>
<br />
<span>content: {toDo.contents}</span>
</li>
);
})}
</ul>
</div>
</>
);
}
UI 컴포넌트 입니다. <select> 태그를 활용해 status atom 을 변경해주고 있으며, selector 을 통해서 toDo 배열을 화면에 뿌려주고 있습니다.
selector 의 또 다른 기능 set
selector 은 get 뿐만 아니라 set 을 활용해서 atom 의 값을 변경해줄 수 있습니다. 이를 통해서 비동기 통신 처리를 할 수도 있습니다. 간단히 위 toDo 예제를 활용해 set 을 활용한 모습을 살펴보겠습니다.
// atom.ts
export const selectToDo = selector<toDo[]>({
key: "selectToDos",
get: ({ get }) => {
const originalToDos = get(toDos);
const nowStatus = get(selectStatus);
return originalToDos.filter((toDo) => toDo.status === nowStatus);
},
set: ({ set }, newToDo) => {
set(toDos, newToDo);
}
}
);
atom.ts 에서 변화한 코드는 selectToDo 의 set 부분입니다. get 을 활용 했을때와 마찬가지로 set 메서드를 인자로 받아와 사용합니다. 또한 인자로는 컴포넌트에서 넣어줄 newValue (여기서는 newToDo) 를 받을 수 있습니다. set 메서드의 첫 번째 매개변수로 변경할 atom 을 넣어주고 두 번째 인자로는 변경해 줄 값을 넣어줍니다.
import React, { useState } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { selectStatus, selectToDo, toDo, toDos } from "./atom";
export default function App() {
const [status, setStatus] = useRecoilState(selectStatus);
const selectToDos = useRecoilValue(selectToDo);
const toDoAtom = useRecoilValue(toDos);
// 아래가 selector 의 set 을 위해 추가된 코드
const [contents, setContents] = useState("");
const setNewToDos = useSetRecoilState(selectToDo);
const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
setStatus(event.currentTarget.value as any);
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setContents(event.currentTarget.value);
};
const handleSubmit = (event: React.ChangeEvent<HTMLFormElement>) => {
event.preventDefault();
if (contents === "") {
return;
} else {
const newToDoList: toDo[] = [
...toDoAtom,
{
contents,
status: "DOING"
}
];
setNewToDos(newToDoList);
setContents("");
}
};
return (
<>
<form onSubmit={handleSubmit}>
<input value={contents} onChange={handleInputChange} />
<button>Submit</button>
</form>
<br />
<div>
<select value={status} onChange={handleStatus}>
<option value="DOING">DOING</option>
<option value="DONE">DONE</option>
</select>
<ul>
{selectToDos.map((toDo, index) => {
return (
<li key={index}>
<span>status: {toDo.status}</span>
<br />
<span>content: {toDo.contents}</span>
</li>
);
})}
</ul>
</div>
</>
);
}
컴포넌트 예제 코드입니다. 윗 예제의 코드들과 겹쳐서 긴 코드가 들어왔습니다. useSetRecoilState 훅을 통해서 selector 의 set 을 활용할 수 있는데, set 을 활용할 때는 인자로 변경할 값을 넣어주면 됩니다. <form> 의 submit 이벤트 내부에서 selector 의 set 을 통해서 toDos 라는 atom 을 변경해 줬습니다.
아래에서 이를 활용한 비동기 통신 예제를 살펴보도록 하겠습니다!
selector 을 활용한 비동기 통신
Recoil 을 활용해 비동기 통신을 하기 위해서는 selector 을 사용해야 합니다. 간단하게 코드를 통해서 알아보겠습니다.
export const selectId = atom({
key: "selectId",
default: 1
});
export const selectingUser = selector({
key: "selectingUser",
get: async ({ get }) => {
const id = get(selectId);
const user = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
).then((res) => res.json());
return user;
},
set: ({ set }, newValue) => {
set(nowUser as any, newValue);
}
});
위 코드를 살펴보면 get 메서드를 통해서 API 콜을 하여서 데이터를 가져옵니다.
위 코드에서는 동적 parameter 을 위해서 id 라는 atom 을 사용합니다. 그럼 atom 을 사용하지 않고 컴포넌트에서 직접 파라미터를 넘겨주는 방법은 없을까요?
export const selectUser = selectorFamily({
key: "selectOne",
get: (id: number) => async () => {
const user = fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
).then((res) => res.json());
return user;
}
});
// 컴포넌트에서 사용 시
const user = useRecoilValue<IUser>(selectUser(id));
위 코드를 살펴보면 selectorFamily 를 활용해 비동기 통신을 하고 있는 것을 확인할 수 있습니다. 컴포넌트에서 동적 parameter 을 위해서 직접 매개변수를 넘겨주고 싶다면 selectorFamily 를 활용해 get 메서드 내부에서 직접 파라미터를 받아줄 수 있습니다.
selector 의 강력한 기능
selector 을 활용해 비동기 통신을 하였을 때 체감되는 강력한 기능은 캐싱 입니다. selector 을 활용해 비동기 통신을 해온다면, 내부적으로 알아서 값을 캐싱 해주기 때문에 이미 한번 비동기 통신을 하여 값이 캐싱되어 있다면 매번 같은 비동기 통신을 하지 않고 캐싱 된 값을 추적하여 사용합니다.
예제
selector 을 활용한 비동기 통신을 작성하며 간단한 예제를 만들어 봤습니다.
Recoil 을 활용 했으며, 초기 데이터 로딩에서는 react-query 를 활용해 봤습니다.
Suspense Trouble Shooting
selector 을 활용해 비동기 통신을 연습하는 과정에서 해당 에러를 확인했습니다.
위 에러는 비동기 통신을 할 때 데이터가 아직 도착하지 않았을 경우 (isLoading 상태) 대체해서 보여줄 UI 가 없다는 뜻입니다. 이를 해결하기 위해서는 아래 세 가지 방법이 있습니다.
- <React.Suspense> 활용
가장 보편적인 방법입니다. index.tsx 에서 <App> 컴포넌트를 <Suspense /> 태그로 감싸줬습니다.root.render( <RecoilRoot> <Suspense fallback={<div>loading..</div>}> <App /> </Suspense> </RecoilRoot> );
- Recoil 의 Loadable 활용
Recoil 에서는 atom 이나 selector 의 현 상태를 나타낼 수 있는 hook 을 제공합니다.const userLoadable = useRecoilValueLoadable(getUser);
Loadable 을 객체는 hasValue , hasError , loading 세 가지 상태를 가지고 있습니다. 이에 따라서 컴포넌트의 상태를 표현해 줄 수 있습니다. - startTransition 활용
이번 React 18 버전에서 Transition 이라는 기능이 나왔습니다. Transition 에 관한 내용은 아래 블로그 글에서 확인하시면 좋을 것 같습니다.
바로가기 >
Recoil 을 활용했을 때 느낀점
Recoil 을 사용해 보고 난 후 Recoil 을 출시하면서 Recoil 팀이 의도 했던 내용을 느껴볼 수 있었습니다.
- 가장 React 스러운 상태관리 라이브러리
Recoil 의 핵심 기능은 hook 을 활용하여 100% React 답게 활용할 수 있었습니다. - 낮은 진입장벽
Recoil 을 처음 배울 때 느꼈던 어려움은 거의 없었습니다. 아마 현재 사용하는 상태 관리 라이브러리 중 제일 쉽게 배울 수 있지 않을까 생각합니다. - DevTool 의 부재
많은 사람들이 꼽는 가장 아쉬운 점은 DevTool 의 부재입니다. Redux 에서는 DevTool 을 제공하고 있기 때문에 편리하게 state 를 시각화 하여 관리할 수 있습니다. 현재 Devtool 관련 부분은 Recoil 팀에서 테스트 하며 출시를 준비하고 있는 듯 합니다.
마무리하며
긴 글을 통해서 Recoil 의 전반적인 내용을 살펴 봤습니다. 100% 에 가까운 내용을 살펴볼 순 없었지만, 대부분의 내용을 담았다고 생각합니다. 공식 문서도 한글화가 잘 되어있고, 출시 팀이 Facebook 이기 때문에 앞으로의 발전될 기능을 기대해보며 아키텍처에 담아봐도 좋을 것 같다는 생각을 합니다.
One reply on “Recoil, 리액트의 상태관리 라이브러리”
Recoil을 막 도입하고 있는 입장에서 무척 도움이 되었습니다 감사합니다.