0. 도입 동기
최근 진행 중인 프로젝트에 전역상태관리가 필요하다고 판단하게 되어 Recoil을 도입하게 되었다.
이전에 Redux를 사용해본 적이 있었는데 Redux를 사용했을 때 규칙에 맞게 작성해야 하는 코드양이 많았던 것이 생각나서 이번 기회에 "React를 위한" 상태관리 라이브러리인 Recoil은 사용성이 어떨지 경험해보고 싶었다. 또한 Redux를 도입하게 되었을 때 Redux를 처음 접하는 팀원들의 러닝커브가 높아질 것에 대한 우려도 있었다.
1. Recoil이란
Recoil은 atom과 selector라는 두 가지 주요한 개념을 토대로 애플리케이션의 상태를 관리할 수 있게 해준다.

위 그림은 atoms(공유 상태)가 selectors(순수 함수)를 나타내는 화살표를 거쳐 컴포넌트로 내려가는 Recoil의 데이터 플로우를 그래프로 표현한 것이다. 각 컴포넌트들은 상태의 단위를 나타내는 atoms를 구독하게 되며 selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환하게 된다.
1.1. Atoms
- 상태의 단위- atom 업데이트 시 이를 구독하는 컴포넌트는 새로운 값을 반영하여 다시 렌더링 됨- 동일한 atom이 여러 컴포넌트에서 사용될 경우 모든 컴포넌트는 상태를 공유
- 모든 atom은 반드시 고유한 키를 가져야 함
- React 컴포넌트의 상태처럼 기본값(default)을 가질 수 있음
const ageState = atom({
key: 'ageState',
default: 24,
});
컴포넌트에서 atom 값을 읽고 쓰기 위해서는 훅을 사용한다. (이런 점이 Recoil이 Redux에 비해 React스러운 지점이 아닐까 싶다.)
useRecoilState라는 훅에 지정해준 키 값을 인자로 넣어 사용한다. 이렇게 하면 React의 useState를 쓰는 것과 거의 동일하게 상태를 사용할 수 있다.
atom의 상태는 사용되는 모든 컴포넌트에서 공유되기 때문에 한 컴포넌트에서 atom 값을 변경할 경우 다른 컴포넌트에서도 변경된 상태를 공유받을 수 있다.
function AgeButton() {
const [age, setAge] = useRecoilState(ageState);
return (
<button onClick={() => setAge((value) => value + 1)}>Add Age</button>
)
}
function AgeTag() {
const [age, setAge] = useRecoilState(ageState);
return <p>I'm {age}</p>
}
AgeButton 버튼을 클릭할 경우 ageState atom을 사용하는 AgeTag에서의 텍스트가 함께 변경된다.
1.2. Selectors
- atoms나 다른 selectors를 인자로 받는 순수 함수
- 상위의 atoms 또는 selectors가 업데이트 되면 하위의 selector 함수도 다시 실행됨
- 컴포넌트는 atoms를 구독하듯 selectors를 구독할 수 있음
- selectors 변경 시 컴포넌트는 다시 렌더링됨
❓ 순수 함수
- 부수 효과가 없는 함수
- 어떤 함수에 동일한 인자를 주었을 때 항상 같은 값을 리턴하며, 외부의 상태를 변경하지 않는 함수
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태는 atoms에 저장하고, 이로부터 파생되는 데이터는 selectors에 명시한 함수를 통해 계산함으로써 쓸모없는 상태의 보존을 방지할 수 있다.
실제로 이번 프로젝트의 한 컴포넌트에서 배율 정보를 나타내기 위해 두 가지 상태값을 사용하고 있었는데, 실제로 DB에 저장되는 값을 atom으로 지정하고, 이로부터 계산되어 사용되는 값을 selector 함수를 통해 계산하는 식으로 변경할 수 있었다.
function MyComponent() {
const [size, setSize] = useState(1); // DB에 저장되는 실수값
const [sizeInput, setSizeInput] = useState(100); // 실수값을 바탕으로 계산되는 퍼센티지 값
...
}
Recoil 도입 이전에는 위처럼 실제로 DB에 저장될 실수값을 나타내는 상태와 이 실수값을 바탕으로 퍼센티지로 변환되어 input에 보여질 값을 나타내는 상태 두 가지가 따로 관리되고 있었다.
const sizeState = atom({
key: 'sizeState',
default: 1,
});
const sizeInputState = selector({
key: 'sizeInputState',
get: ({ get }) => {
const size = get(sizeState);
return size * 100;
},
});
function MyComponent() {
const [size, setSize] = useRecoilState(sizeState);
const sizeInput = useRecoilValue(sizeInputState);
...
}
이를 위와 같이 atom과 selector로 분리해 필요한 최소 단위 상태인 size값만 관리를 하면서도 이로부터 파생되는 필요한 값을 적절히 계산하여 사용할 수 있었다. 또한 위처럼 atom이나 selector의 값을 읽어오기만 할 때는 useRecoilValue 훅을 사용할 수 있다.
또한 selector에 set 프로퍼티를 추가하여 selector를 writable한 상태로 만들 수도 있다.
const sizeState = atom({
key: 'sizeState',
default: 1,
});
const sizeInputState = selector({
key: 'sizeInputState',
get: ({ get }) => {
const size = get(sizeState);
return size * 100;
},
set: ({ set }, sizeInput) => {
set(sizeState, sizeInput / 100);
}
});
위처럼 setter 함수를 추가하여 복수의 atom 값을 변경할 수 있다.
'웹 개발 > React · Next.js' 카테고리의 다른 글
| [Next.js] 국제화(i18n) 자동화 시스템 구축하기 (0) | 2023.04.30 |
|---|---|
| [React] Web API를 활용한 영상 녹화 구현하기 (0) | 2023.03.31 |
| [번역] useMemo와 useCallback 제대로 알고 사용하기 (0) | 2023.02.28 |
| multer로 AWS S3에 파일 업로드하기 (0) | 2022.10.30 |
| [Next.js] "window is not defined" 에러 해결 (0) | 2022.05.14 |