프론트엔드에 동적으로 표현되는 데이터이다. state는 부모 컴포넌트 -> 자식 컴포넌트로 prop으로만 전달이 가능하다.
A -> B -> C -> D의 구조에서 A의 state를 D에서 사용하려면 B, C는 그 state를 사용하지 않더라도 props로 A -> B -> C -> D로 내려줘야 한다.
상태 관리 라이브러리를 이용하면 state 저장소를 전역에 두어 어느 컴포넌트에서도 해당 저장소에 접근할 수 있어 위 문제가 해결된다.
$ npm i recoil @types/recoil
recoil에서 쓰이는 state의 단위 unique한 key값으로 구분된다.
atom이 업데이트가 되면 해당 atom을 구독(참조 / 사용)하는 모든 컴포넌트들의 state가 새로운 값으로 업데이트 되고 리렌더링된다.
import {atom} from "recoil"
interface IPerson {
name: string;
age: number;
}
// Person = IPerson의 배열
export const Person = atom<IPerson[]>({
key: "person",
default: []
});
먼저 사용하려면 최상위 컴포넌트에서 RecoilRoot로 다른 컴포넌트를 감싸줘야한다.
// src/index.tsx
import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement!);
root.render(
<div>
<RecoilRoot>
<App />
</ThemeProvider>
</RecoilRoot>
</div>
);
전반적으로, React의 useState Hook과 사용법이 비슷하다.
atom의 값만 얻어오는 것, 변경은 못함
const val = useRecoilValue(myAtom);
atom의 값을 변경할 수 있는 set 함수를 얻음
const setVal = useSetRecoilState(myAtom);
atom의 값도 얻고 변경도 할 수 있는 set 함수도 얻음
useState와 비슷
const [val, setVal] = useRcoilState(myAtom);
recoil은 전역 상태 관리 라이브러리 -> 두 컴포넌트가 구독 중인데 한 컴포넌트의 변경이 다른 컴포넌트에 영향을 줄 수 있다.
-> selector의 등장
파생된 state를 저장하는 Recoil에서 제공하는 순수함수
순수함수: 동일한 값(파라미터)을 제공하면 동일한 값을 Return
내부에서 get 메서드로 atom을 사용할 수 있음 & atom을 가공하여 새로운 값 반환 가능, 이때 기존 atom의 값은 변환시키지 않는다.
set 메서드로 기존 atom 값을 변형시킬 수 있는데, 이 때 selector에 대한 set 함수가 있어야한다.(useRecoilState 등을 이용)
// atom.tsx
import { atom, selector } from "recoil";
export const cmState = atom<number>({
key: "cm",
default: 0,
});
export const inchSelector = selector<number>({
key: "inch",
get: ({ get }) => {
const cm = get(cmState);
return cm / 2.54;
}, // get을 사용해서 cmState에 2.54를 나눈 값을 출력하게 만들었다.
set: ({ set }, newValue) => { // selector setter에 넘겨진 파라미터가 newValue로 들어감
set(cmState, Number(newValue) * 2.54);
},
});
//-----------------------------------------------------------------------------------------------//
// app.tsx
import { useRecoilState } from "recoil";
import { cmState, inchSelector } from "./atom";
function CmToInch() {
const [cm, setCm] = useRecoilState(cmState); // atom
const [inch, setInch] = useRecoilState(inchSelector); // selector
//selector도 set 함수를 만들어준다.
const onCmChange = (event: React.FormEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setCm(+value); // string to number
};
const onInchChange = (event: React.FormEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setInch(+value);
}; //onCmChange와 동일
return (
<>
<input onChange={onCmChange} value={cm} placeholder="CM" />
<input onChange={onInchChange} value={inch} placeholder="Inch" />
</>
);
}
export default CmToInch;
cm의 input에 값을 입력하면 inch에는 inchSelector의 get 함수를 통해 변형된 inch 값이 출력된다.
inch의 input에 값을 입력하면(setInch(value)) selector의 set 함수가 실행되고 atom인 cmState의 값을 바꾼다 -> cm에는 바뀐 cmState값이 출력된다.
비동기 처리: 비동기로 동작하는 코드가 있으면 그 결과값을 받아오기 위해 동기적으로 동작하도록 만들어주는 것
selector을 이용하여 처리 가능
selector을 활용하여 사용자, repository의 정보를 입력하면 star의 개수를 나타내는 프로그램
// atom.ts
export const githubInfoState = atom({ // 사용자 정보, repo를 저장하는 atom
key: "githubInfomation",
default: { users: "", repo: "" }
});
export const getStars = selector({
key: "get/github-repo-stars",
get: async ({ get }) => {
const { users, repo } = get(githubInfoState); // atom 정보를 얻어옴
if (!users || !repo) return;
const url = `https://api.github.com/repos/${users}/${repo}`;
try {
const response = await fetch(url); // 얻어온 atom 정보를 활용하여 url fetch
const data = await response.json();
return data?.stargazers_count; // star 개수를 반환
} catch (err) {
throw Error("잘못된 깃허브 정보입니다!");
}
}
});
//----------------------------------------------------------------------------------------------//
// Star.tsx
/* ... */
const Stars = () => {
const userRepoStars = useRecoilValue(getStars); // selector
const stars = userRepoStars ? `${userRepoStars} 개` : "";
return <StyledStars>{stars}</StyledStars>;
};
//----------------------------------------------------------------------------------------------//
// App.tsx
const App = () => {
...
return (
...
<Suspense fallback={<div>loading...</div>}>
<Stars />
</Suspense>
...
)
}
Selector은 기본적으로 값을 캐싱 -> 이전에 들어왔던 api라면 캐시에서 바로 꺼내서 사용한다
서버의 정보가 바뀌었는데 api를 캐싱해서 업데이트가 안될 수도 있다
→ selector은 구독중인(참조중인 atom) or Family의 파라미터 정보가 변경되면 다시 업데이트되므로 그것을 활용하거나 Refresh state를 하나 만들어서 selector가 구독하게 만들고 api 요청이 필요할 때 변경시키도록한다.
또는 selector의 set을 활용해서 atom을 변경시키면 구독중인 정보가 변경되는 것이므로 같은 효과를 얻을 수 있다.
set으로 atom의 state을 변경시키면 여러 selector가 그 atom을 구독중일 때 동시에 여러 selector을 갱신시켜줄 수 있는 장점이 있다.
→ 단, 클라이언트 정보 변경에 한정된다. 서버의 정보 변경은 클라이언트 단에서는 모른다.
→ 시간 정보를 쿼리로 담아 api 요청을 보내도록 해서 매번 새로운 요청을 하도록 하여 캐싱이 안되도록 하는 방법도 존재한다.
가장 좋은 것은 refresh 기능이 recoil에 탑재되는 건데 아직 없다고 한다.
recoil을 활용할 때 atom이 여러 selector에 활용될 수 있으므로 flow chart를 잘 설계해서 어떤 selector가 어떤 atom을 구독하는지 파악하는게 중요하다.
꼭 전역으로 관리될 필요 없는 것은 Hooks와 Props로 설계한다.
여러 state 값을 사용 용도에 따라 분류하기 → UI 전용, 폼데이터 전용 등
어떤 데이터를 캐싱하고, 어떤 데이터가 실시간으로 반영되어야 하는지 파악하기
캐싱: 사용자 정보 등, 주기적: 자주 바뀌지 않는 정보, 실시간: 주요 데이터
데이터가 어느 시점에 변경되고 어느 부분에 영향을 주는지 예측하기
https://tech.osci.kr/2022/09/02/recoil-selector/
query의 캐시에 client state을 저장하고 그것을 꺼내는 메커니즘이라고 한다.