서론
비즈니스 모델의 복잡성이 증가함에 따라 프론트엔드 개발에서 처리하는 데이터의 양과 복잡도도 더욱 커지고 있다. 사용자와 상호작용하는 뷰는 점차 더 많은 액션을 포함하게 되고, 데이터베이스에서 데이터를 가져오는 횟수와 데이터의 크기도 증가하고 있다. 이러한 환경에서 데이터를 효율적으로 관리하는 것은 필수적이다. 실제로 프론트엔드 개발자라는 직군이 등장한 배경도 이러한 필요성에서 비롯된 것이라고 본다. 이 글에서는 프론트엔드 개발에서 데이터를 효율적으로 관리하는 데 도움을 주는 상태 관리 라이브러리 중 하나인 Recoil의 사용 이유와 기본적인 사용 방법에 대해 살펴보겠다.
왜 Recoil인가
상태 관리 라이브러리를 사용해야 하는 이유는 앞에서 간략하게 설명하였다. 상태 관리 라이브러리에는 redux, zustand, zotai등 많은데 왜 recoil을 사용해야 하는지 recoil 개발진이 recoil을 소개한 영상을 바탕으로 설명해보겠다. recoil을 만들게 된 이유를 소개한 영상은 이 영상을 보면 좋을 것 같다.
기존 React의 한계
먼저 개발진은 tree에서 자식 컴포넌트의 상태를 부모 컴포넌트에 전달하고 싶은 상황을 가정한다. 이는 당연히 쉽지 않다. 왜냐하면 첫째로 부모 컴포넌트들은 너무 많은 자식 컴포넌트를 가지고 있어 부모 컴포넌트까지 상태를 hoisting한다면 하나의 상태가 변할 때 그 상태를 공유하는 모든 컴포넌트들이 리렌더링 될것이기 때문이다. react의 usecontext를 쓰면 된다고 할지 모르지만 우선 자식 컴포넌트가 얼마나 많아질지에 대해 모르고(1부터 6까지 숫자가 붙은 자식 컴포넌트들은 유저가 마음대로 추가하고 제거할 수 있는 컴포넌트들이다) provider가 너무 많이 필요해지기 때문이다.
기존 react에서 자식 컴포넌트의 상태를 부모 컴포넌트에 전달하기 위해선 많은 provider(wrapper)가 필요한데 사용자가 새로운 컴포넌트를 추가하여 새로운 provider를 dom tree에 추가하려면 tree의 상단, insert 화살표가 가르키는 곳,에 추가해야 하기에 react는 전체 dom tree를 unmount하고 새로운 tree를 mount해야 한다. 또한 상태를 공유하기 위해선 공통된 부모 컴포넌트를 가져야 하기 때문에 tree가 너무 거대해질 수 있고 거대한 tree 전체를 매번 re-render해야하니 성능에 안 좋다. 오른쪽 사진처럼 coupling을 통해 상태를 전달하면 code split이 어려워지고 만약 자식 컴포넌트가 외부 라이브러리를 사용하면 더욱 어려워지기 때문에 이 역시 좋은 해결방법은 아니다.
Recoil 도입
Atom
Recoil개발진들은 recoil을 통해 위의 문제를 해결하는데 dom tree를 3차원 공간으로 옮겨 기존 dom tree가 존재하는 평면에 직교하는 새로운 tree를 만듬으로써 이를 해결한다. 새롭게 만들어진 tree의 node들이 atom이고 이들은 각각 기존 dom tree의 자식 컴포넌트들에 연결된다. atom은 recoil에서 가장 작은 단위로 변경 가능하고 구독 가능한(subscrible) 상태의 가장 작은 단위이다.
atom의 코드이다. 먼저 Fig3의 그림처럼 각각의 atom은 고유한 id를 통해 고유한 key을 가진다. 이를 통해 서로 다른 atom을 구분할 수 있고 각각의 값은 캐시되어 메모리에 저장됨을 알 수 있다.
Selector
Recoil에서 Atom 다음으로 Recoil에서 상태를 관리하는 중요한 개념은 Selector이다. Selector는 순수 함수로서, Atom의 상태를 동기적 또는 비동기적으로 변경할 수 있는 기능을 가진다. 함수형 프로그래밍 원칙을 준수하는 Selector는 다른 Atom이나 Selector를 입력으로 받아, 파생 상태(derived state)를 생성한다. 이러한 입력이 변경될 경우, Selector는 재평가(re-evaluated)되므로 효율적인 상태 관리가 가능하다
순수함수인 selector의 출력인 derived state은 비동기적으로도 얻어질 수 있다. 단순히 기존 selector의 return에 promise를 추가하는 것으로 출력을 비동기적으로 얻을 수 있다. 물론 async/await 문법도 사용 가능하다. 이를 통해 동기적인 React 컴포넌트 렌더 함수에서 비동기 함수를 쉽게 사용할 수 있게 해준다.
결론
위에서 Reocil이 도입된 이유와 간단한 구현에 대해 살펴보았다. 정리하면 recoil은 react의 상태처럼 간단한 get/set interfacce로 보일러 플레이트가 없는 api는 공유 상태를 만들수 있었다. 또한 react hook을 기반으로 만들었기에 Concurrent Mode같은 기능들과도 호환이 잘되고 후에 react에 새로운 기능들이 추가되어도 잘 호환될 가능성이 크다. 또한 동기적 비동기적으로 derived state를 얻을 수 있고 이는 순수함수의 출력으로 얻어져 함수형 프로그래밍의 원칙을 준수함을 알 수 있었다. 마지막으로 atom은 메모리에 캐시되어 성능 최적화에 유리함을 알 수 있었다. 간략하게 recoil을 사용하는 이유와 원리에 대해 알아봤으니 recoil을 사용하는 방법에 대해 알아보겠다.
Recoil 사용방법
Recoil 설치
npm install recoil
//
yarn add recoil
먼저 recoil을 설치해준다. 사용하는 라이브러리 관리자에 따라 위 아래 명령어 중 선택하여 설치하면 된다.
ESLint를 사용한다면 eslint.config파일에 다음 코드를 추가하여 useRecoilCallback에 잘못된 종속성을 전달됐을 때 경고를 받을 수 있다.
// modified .eslint config
{
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": [
"warn", {
"additionalHooks": "(useRecoilCallback|useRecoilTransaction_UNSTABLE)"
}
]
}
}
App 최상단 root에 RecoilRoot 추가
설치를 완료했으면 app의 최상단 root 파일에 RecoilRoot을 추가해야 한다. 일반적으로 App.js나 index.js일 것이다.
import React from 'react';
import {RecoilRoot} from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
Atom 사용하기
전역상태를 위해 atom을 사용하기 위한 코드이다. atom의 값을 사용하는 컴포넌트들은 atom을 구독한다. atom에 변화가 생기면 그 atom을 구독하는 모든 컴포넌트들이 re-render될 것이다.
atom은 각각 고유한 key값을 가져야 함에 주의하자. atom도 react의 local state처럼 default value를 지정할 수 있고 데이터 타입은 무엇이든 가능하다.
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
선언한 atom을 컴포넌트에서 불러오기 위해선 다음과 같이 쓰면 된다.
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
Selector 사용하기
atom이나 selector을 입력으로 받아 derived data를 출력하는 순수함수인 selector는 다음과 같이 사용할 수 있다. 비동기적으로 값을 변경하기 위해선 get에 async/await를 추가하거나 return에 promise를 추가할 수 있다. selector의 key값도 고유해야 한다. react의 suspense혹은 errorboundary와도 같이 사용할 수 있다.
const charCountState = selector({
key: 'charCountState', // unique ID (with respect to other atoms/selectors)
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
컴포넌트에서 selector를 구독하기 위해선 다음과 같이 쓸 수 있다.
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
Atom effects 사용하기
atom의 side effect를 관리하고 atom이 초기화되거나 동기화될 때 사용하기 위해 atom effects api를 사용할 수 있다. 이는 상태를 관리, 유지하거나 변화를 감지하고 기록(logging)하는데 사용될 수 있다. 리액트의 useEffect hook과 비슷하지만 aotm effects는 각각의 atom에서 고유의 policy를 사용할 수 있기에 유용하다.
atom effects는 atom이 RecoilRoot에서초기화될때 최우선으로 실행되며 atom의 effect 옵션으로 구현된다. useEffect처럼 clean-up함수를 통해 정리할 수도 있고 정리되거나 사용되지 않을때 재초기화될 수 있다.
const myStateFamily = atomFamily({
key: 'MyKey',
default: null,
effects: param => [
() => {
...effect 1 using param...
return () => ...cleanup effect 1...;
},
() => { ...effect 2 using param... },
],
});
사용예제는 다음과 같다. 콜백함수로 로그인한 유저의 아이디를 출력하는 atom effects이다. 더 자세한 예시는 이 글을 참고하기 바란다.
const currentUserIDState = atom({
key: 'CurrentUserID',
default: null,
effects: [
({onSet}) => {
onSet(newID => {
console.debug("Current user ID:", newID);
});
},
],
});
Query Refresh
selector를 비동기적으로 서버에서 데이터를 받아오기 위해 사용하는 경우 수동으로 selector를 재실행하고 싶을때가 있다. 해당 기능을 수행하는데는 총 3가지 방법이 있지만 이 글에선 여백상 한 가지 방법만 소개한다. 나머지 방법을 알고 싶다면 이 글을 참고하자.
const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
}
})
function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRecoilRefresher_UNSTABLE(userInfoQuery(currentUserID));
return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={() => refreshUserInfo()}>Refresh</button>
</div>
);
}
수동으로 selector를 refresh하는 방법은 useRecoilRefresher()를 사용하는 것이다. 변수 refreshUserInfo에useRecoilRefresher()를 할당하여 버튼을 누를시 callback함수를 실행시켜 수동으로 query를 재실행함을 볼 수 있다.
참고 자료
[ReactEurope]. (2020, 5 15). [Recoil: State Management for Today's React - Dave McCabe aka @mcc_abe at @ReacteuropeOrgConf 2020] [비디오 파일]. YouTube. 링크
Recoil. (2023). [Motivation] [웹 페이지]. 링크
'Library, Tool' 카테고리의 다른 글
Git의 동작원리 (0) | 2024.07.15 |
---|---|
CSS Framework들의 특징과 장단점 비교 (tailwind, styled-componets) (0) | 2024.07.09 |
상태 관리 라이브러리 비교: Redux vs Justand vs Recoil (0) | 2024.01.07 |
ESLint와 Prettier로 컨벤션 준수하기 with Webstorm (0) | 2023.10.27 |
MSW로 실제 네트워크 환경에서 Mocking하기 (0) | 2023.10.26 |