상태관리 라이브러리 없이 프로젝트를 하다가 아, 이건 안되겠다. 전역으로 상태관리해야겠다! 싶은 부분이 있었다.
근데 오늘 아침 다른 개발자분 티스토리 피드를 보다가 Recoil 을 추천하시는 글을 읽고
이 참에 Recoil을 써보자 생각했다.
설치
먼저 설치한다
npm install recoil
Recoil을 사용하기 위해서는 애플리케이션의 최상위 컴포넌트에 <RecoilRoot> 컴포넌트를 사용해야한다.
index.js 의 파일을 다음과 같이 수정해준다.
index.js
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root')
);
그리고 상태를 정의한다.
상태 정의는 atom 이라는 함수를 사용해서 할 수 있다.
나는 recoilState.js 라는 파일을 만들어서 아래와 같이 작성해주었다.
key는 상태를 식별하는 고유한 키이고,
default 는 초기값이다.
Atom
// recoilState.js
import { atom } from 'recoil';
export const countState = atom({
key: 'countState',
default: 0,
});
특정 컴포넌트에서 Recoil의 상태를 사용하려면
useRecoilState 라는 훅을 사용해서 상태값을 가져오고, 상태값 변화 또한 가능하다.
useRecoilState
import React from 'react';
import { useRecoilState } from 'recoil';
import { countState } from './recoilState'; // Recoil 상태를 가져옴
function Counter() {
const [count, setCount] = useRecoilState(countState);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
일단 Redux처럼 엄청난 보일러 플레이팅을 안해도 된다는 점에서 너무 매력적이다.
useRecoilValue 와 useSetRecoilValue
이 둘은 Recoil 의 두가지 변형이다.
useRecoilState 는 상태의 값을 가져오고, 설정했다면
useRecoilValue 는 상태의 값을 가져오기 에만 사용되고
useSetRevoilState 는 상태의 값을 설정하는 데만 사용된다.
위에서 보았던 코드에 적용하면 다음과 같을 수 있다.
import React from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { countState } from './recoilState'; // Recoil 상태를 가져옴
function Counter() {
const count = useRecoilValue(countState); // 상태의 값을 가져옴
const setCount = useSetRecoilState(countState); // 상태를 설정하는 함수
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
useResetRecoilState()
useResetRecoilState() 는 말 그래도 인자로 받아온 atom의 state 를 default 값으로 reset 시킨다.
사용은 아래와 같이 한다.
import { atom, useRecoilState, useResetRecoilState } from 'recoil';
// 기본 상태(atom)
const countState = atom({
key: 'countState',
default: 0,
});
function Counter() {
const [count, setCount] = useRecoilState(countState);
const resetCount = useResetRecoilState(countState); // 상태 초기화
...
selector + get
Recoil 의 selector 는 다른 state 를 파생할 수 있다. (state의 변환, 조합, 필터링 등을 수행할 수 있다.)
아래는 selector의 예시이다.
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// 기본 상태(atom)
const countState = atom({
key: 'countState',
default: 0,
});
// 파생 상태(selector)
const evenOrOddState = selector({
key: 'evenOrOddState',
get: ({ get }) => {
const count = get(countState);
return count % 2 === 0 ? '짝수' : '홀수';
},
});
function Counter() {
const [count, setCount] = useRecoilState(countState);
const evenOrOdd = useRecoilValue(evenOrOddState); // 파생 상태를 가져옴
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<p>Even or Odd: {evenOrOdd}</p> {/* 파생 상태 출력 */}
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default Counter;
위의 예시에서 evenOrOddState 는 countState 를 짝수 또는 홀수로 구분해서 문자열을 반환한다.
get 함수를 통해서 countState 값을 가져와야한다.
그리고 이 evenOrOddState 를 사용할 때 useRecoilValue 를 사용해서 countState 의 파생상태를 가져오게 된다.
selector 는 read-only 라고 할 수 있다.
이렇게 작성한 evenOrOddState 는 write 가 불가능하다.
즉 useSetRecoilState 나 useRecoilState 의 파라미터로는 넣을 수 없다.
하지만 selector 에 set 속성을 추가하면 write 가 가능해진다.
selector + get + set
위에선 selector 안에 get 만 존재했는데, set 도 작성할 수 있다.
set 속성은 해당 selector 가 read-write 상태를 가지게 한다.
import { atom, selector, useRecoilState } from 'recoil';
const nameState = atom({
key: 'nameState',
default: 'John',
});
const greetingsState = selector({
key: 'greetingsState',
get: ({ get }) => {
const name = get(nameState);
return `Hello, ${name}!`;
},
set: ({ set }, newValue) => {
const newName = newValue.replace('Hello, ', '').replace('!', '');
set(nameState, newName);
},
});
function MyComponent() {
const [greetings, setGreetings] = useRecoilState(greetingsState);
const handleChangeGreetings = () => {
setGreetings('Hello, Alice!'); // `set` 함수를 사용하여 greetingsState 업데이트
};
return (
<div>
<p>Greetings: {greetings}</p>
<button onClick={handleChangeGreetings}>Change Greetings</button>
</div>
);
}
export default MyComponent;
이 예시에서 greetingState 는 nameState에 의존해서 파생state 를 계산하는 selector 이다.
여기서 setGreetings 함수는 selector 의 set 속성을 통해서 생성되는 함수이다.
set 함수는 업데이트할 새로운 값을 인자로 받아서 nameState 를 업데이트 해준다.
selector + 비동기처리 + useRecoilValueLoadable
이 selector의 역할을 생각하면, 원래 사용하던 방식을 조금 고칠 수 있다.
만약에 api 통신과 같은 비동기 처리를 평소에 어떻게 했었는지 생각해보자.
나는 component에서 api 통신을 하고, 불러온 데이터를 atom에 저장했었다.
하지만 이렇게 atom 상태가 변할때마다 각 컴포넌트에서 비동기 처리를 따로 해주면 atom을 구독하고 있던 컴포넌트들은
알아서 리렌더링된다.
selector 는 기본적으로 캐싱 기능이 있어 이미 들어왔던 적이 있는 값을 기억해서, 같은 응답을 보내는 api 호출은 추가적으로 요청하지 않는다. 따라서 성능상 유리하다.
import React from 'react';
import { atom, selector, useRecoilValueLoadable } from 'recoil';
const dataState = atom({
key: 'dataState',
default: null,
});
const dataSelector = selector({
key: 'dataSelector',
get: async ({ get }) => {
try {
// 비동기 API 호출을 수행
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
throw new Error('Failed to fetch data');
}
},
});
function MyComponent() {
const dataLoadable = useRecoilValueLoadable(dataSelector);
switch (dataLoadable.state) {
case 'loading':
return <div>Loading...</div>;
case 'hasValue':
const data = dataLoadable.contents;
return (
<div>
<p>Data: {data}</p>
</div>
);
case 'hasError':
return <div>Error: {dataLoadable.contents.message}</div>;
default:
return null;
}
}
export default MyComponent;
dateState 라는 atom 이 있고, dataSelector 는 비동기 API 호출을 수행하는 selector 이다.
dataSelector 의 get 함수는 비동기로 데이터를 가져온다.
useRecoilValueLoadable 훅으로 dataSelector 를 구독하고, 데이터 상태를 가져온다.
dataLoabable.state 를 사용하면 데이터 상태를 확인할 수 있다.
Loadable 객체는 state 와 contents 라는 프로퍼티가있다.
state : hasValue, hasError, loading
contents : hasValue 일 경우 value, hasError 일 경우 Error 객체, loading 일 경우 Promise
selectorFamily(동적 url 의 api 호출 할 때!)
외부에서 파라미터로 값을 받아와 selector에 적용해야할 때, selectorFamily를 이용할 수 있다.
예시는 아래 블로그 juno7803님의 코드를 참고했다.
export const githubRepo = selectorFamily({
key: "github/get",
get: (githubId) => async () => {
if (!githubId) return "";
const { data } = await axios.get(
`https://api.github.com/repos/${githubId}`
);
return data;
},
});
api 호출을 할때 url parameter 를 selectorFamily 를 통해 받아와보자
import { useRecoilValue } from 'recoil';
import { selectorFamily } from '../../state';
const Github = () => {
const githubId = 'Dawon00';
const githubRepos = useRecoilValue(githubRepo(githubId));
return(
<>
<div>Repos : {githubRepos}</div>
</>
)
}
export default Github;
useRecoilValue 를 사용하고, 그 안에 selectorFamily의 이름을 적어주면 된다.
마치며
redux 에 대한 보일러 플레이트도 적고, 러닝커브가 적다는 것을 몸소 느꼈다.
그리고 정말 React 스럽다고 느꼈다. useState 를 쓰는 방법과 비슷하니 적응하기 쉬웠다.
ChatGPT 에게 Recoil 의 장점을 물어보며, 포스팅을 마무리해보려한다.
ref : https://medium.com/humanscape-tech/recoil-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-285b29135d8e
https://recoiljs.org/docs/introduction/getting-started/
https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0