React 상태관리 라이브러리인 recoil에 대해 정리를 해보려고한다.
상태관리가 필요한 이유?
React는 state에 변동이 생기면 리렌더링이 되어 페이지에 변동이 생기는데 이 state을 전달하려면 부모 컴포넌트에서 자식 컴포넌트로만 가능하다. 예를들어 A부터 Z까지 알파벳 컴포넌트가 있다고 가정하고 A라는 컴포넌트의 state를 Z라는 자식 컴포넌트가 쓰고싶다면 A~Z사이에 있는 모든 컴포넌트는 A컴포넌트의 state가 필요없어도 Props로 전달해 줘야 한다. 이 부분이 Props drilling 이라고한다. Props drilling으로 인한 불필요한 리렌더링 발생을 방지하고 유지보수의 어려움을 해결하기 위해 상태관리가 필요하다.
recoil 특징
React 전용 상태 관리 라이브러리이기 때문에 React처럼 작동을 하며 쉽게 접근할 수 있어 React 문법 친화적이다.
store, action, reducer 등 여러 가지를 신경 쓸 일 없이 초기 세팅이 직관적이고 간단하다.
추가 라이브러리 없이 비동기 처리를 간단하게 할 수 있다.
내부적으로 자동적 캐싱되어 빠르다.
시작하기
// 설치
npm install recoil
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { RecoilRoot } from "recoil";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<RecoilRoot>
<BrowserRouter>
<App />
</BrowserRouter>
</RecoilRoot>
);
최상단의 App 컴포넌트를 RecoilRoot로 감싸주면 전역으로 상태관리 할 수 있도록 해준다.
Atoms
Atom은 Recoil 상태의 단위를 의미하고 업데이트와 참조가 가능하며 key, default 2개의 프로퍼티를 필수로 설정해야한다.
export const numberState = atom({
key: "numberState",
default: 0,
});
해당 Atom을 컴포넌트에서 사용하려면 Recoil에서 제공하는 hooks을 사용해야 한다.
const number = useRecoilValue(numberState)
const setNumber = useSetRecoilState(numberState)
const [number, setNumber] = useRecoilState(numberState) // useState와 유사
const resetNumber = useResetRecoilState(numberState)
selectors
selector는 파생된 상태의 일부를 나타낸다. 즉 선언된 atom을 처리해 새로운 값을 return 하거나 값을 변경하는 역할을 하는데 사용된다.
export const numberSelector = selector({
key: "numberSelector",
get: ({ get }) => {
const result = get(numberState) / 1.61;
return result.toFixed(2);
},
set: ({ set }, newNumber) => set(numberState, (newNumber * 1.61).toFixed(2)),
});
selector로 간단하게 km와 mile 단위를 변환하면서 적용시켜보았다.
(1mile = 1.609344km로 기준을 쉽게 1.61km로 잡고 소수점 자릿수 2자리로 지정)
// App.js
function App() {
return (
<div className="absolute modalPosition flex flex-col gap-4">
<Atest />
<Btest />
</div>
);
}
// Atest.js
import React, { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import { numberState } from "Atom/atom";
const Atest = () => {
const [stateNum, setStateNum] = useRecoilState(numberState);
const [value, setValue] = useState("");
useEffect(() => {
setValue(stateNum);
}, [stateNum]);
return (
<div className="flex gap-4">
<input className="w-40 h-10 border border-black px-2" onChange={(e) => setValue(e.target.value)} value={value}></input>
<button type="button" className="w-12 h-10 border border-black" onClick={() => setStateNum(+value)}>
km
</button>
</div>
);
};
export default Atest;
//Btest.js
import React, { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import { numberSelector } from "Atom/atom";
const Btest = () => {
const [selectorNum, setSelectorNum] = useRecoilState(numberSelector);
const [value, setValue] = useState("");
useEffect(() => {
setValue(selectorNum);
}, [selectorNum]);
return (
<div className="flex gap-4">
<input className="w-40 h-10 border border-black px-2" onChange={(e) => setValue(e.target.value)} value={value}></input>
<button type="button" className="w-12 h-10 border border-black" onClick={() => setSelectorNum(+value)}>
mile
</button>
</div>
);
};
export default Btest;