리액트에서는 자체적으로 상태 관리를 할 수 있는 Hooks나 Context를 제공합니다.
그러나 내장된 상태 관리 프로그램은 다음과 같은 몇 가지 단점이 있죠.
- 컴포넌트의 상태는 공통된 상위요소에서 공유될 수 있지만, 이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 함
- Context는 단일 값만 저장할 수 있으며, 여러 값들의 집합을 담을 수는 없음
- 이 두가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 잎(state가 사용되는 곳)까지의 코드 분할을 어렵게 만듦
그래서 사람들은 보통 Redux, Mobx을 사용합니다.
그리고 페이스북에서 새로운 상태관리 라이브러리 Recoil도 선보였죠.
오늘은 그 중에서 Recoil에 대해서 간단히 살펴보고 적용해보겠습니다!
※ 저는 주로 MobX를 사용했는데요, 라이브러리 간의 차이는 추후에 다른 글에서 구체적으로 살펴보겠습니다
※ 아래 콘텐츠의 내용과 예제는 Recoil 공식 문서를 참고하였습니다.
Recoil은 페이스북에서 만든 새로운 React를 위한 상태 관리 라이브러리 입니다. 최대한 React의 시맨틱과 동작을 유지하면서 위의 문제점을 개선하기 위해 만들어졌고, 다음과 같은 특징을 지녔다고 합니다.
- 공유상태(shared state)도 React의 로컬 상태(local state)처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다. (필요한 경우 reducers 등으로 캡슐화할 수도 있다.)
- 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.
- 상태 정의는 증분 및 분산되므로 코드 분할이 가능하다.
- 상태를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있다.
- 파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 파생된 데이터는 동기식과 비동기식 간에 이동할 수 있다.
- 탐색을 일급 개념으로 취급할 수 있고 심지어 링크에서 상태 전환을 인코딩할 수도 있다.
- 역호환성 방식으로 전체 애플리케이션 상태를 유지하는 것은 쉬우므로, 유지된 상태는 애플리케이션 변경에도 살아남을 수 있다.
사실 저도 무슨 뜻인지 전체가 와닿지는 않았는데요,
사용법을 익히고나니 확실히 Redux보다 깔끔한 방식이라는 것은 느낄 수 있었습니다.
우선 직접 사용하면서 살펴보겠습니다!
Recoil을 사용하면 atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있습니다. Atoms는 컴포넌트가 구독할 수 있는 상태의 단위이며 Selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환해주는데요, 하나씩 살펴보겠습니다.
atom은 상태의 단위로, 값이 업데이트되면 값을 구독(subscribe)한 컴포넌트는 다시 렌더링이 됩니다.
Atoms는 atom함수를 사용해 생성할 수 있습니다.
const fontSizeState = atom({
key: 'fontSizeState', // 여기서 key값은 전역적으로 고유해야 합니다.
default: 14,
});
컴포넌트에서 atom을 읽고 쓰려면 useRecoilState라는 훅을 사용해야 합니다. React의 useState와 비슷하지만 상태가 컴포넌트 간에 공유될 수 있다는 차이가 있죠.
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Click to Enlarge
</button>
);
}
Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)입니다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행되죠.
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용됩니다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors를 통해 계산함으로써 쓸모없는 상태의 보존을 방지하는 것이죠.
(얼핏 보면 MobX의 computed와 유사해 보이기도 하네요)
Selectors는 selector함수를 사용해 정의됩니다.
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
get 속성은 계산될 함수입니다. get 인자를 통해 atoms와 다른 selectors에 접근할 수 있는데요,
참조했던 다른 atoms나 selectors가 업데이트되면 이 함수도 다시 실행됩니다.
이 fontSizeLabelState 예시에서 selector는 fontSizeState라는 하나의 atom에 의존성을 갖고 있습는다.
개념적으로 fontSizeLabelState selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작하는 것이죠.
Selectors는 useRecoilValue()를 사용해 읽을 수 있습니다.
useRecoilValue()는 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환하는데요,
fontSizeLabelState selector는 writable하지 않기 때문에 useRecoilState()를 이용하지 않습니다.
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>Current font size: ${fontSizeLabel}</div>
<button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
Click to Enlarge
</button>
</>
);
}
우선 recoil 패키지를 설치합니다.
npm install recoil // 혹은 yarn add recoil
그리고 recoil 상태를 사용하는 부모 컴포넌트는 부모 트리 어딘가에 RecoilRoot가 필요한데요, 일반적으로 루트 컴포넌트에 넣습니다.
// 기존 루트 파일 (_app.tsx)
import type { AppProps } from "next/app"
import "../styles/globals.css"
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp
// 수정한 루트 파일
import type { AppProps } from "next/app"
import "../styles/globals.css"
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from "recoil"
function MyApp({ Component, pageProps }: AppProps) {
return (
<RecoilRoot>
<Component {...pageProps} />
</RecoilRoot>
)
}
export default MyApp
이제 한 번 위에서 배운 atoms과 selectors을 사용해보기 위해 다음과 같은 코드를 추가해보겠습니다.
atom을 통해 string을 담을 textState를 만들고, selector로 글자 수를 리턴할 charCountState를 선언했습니다.
그리고 useRecoilState와 useRecoilValue를 통해서 선언한 값들을 사용해보았습니다.
import { ChangeEvent } from "react"
import { atom, selector, useRecoilState, useRecoilValue } from "recoil"
const textState = atom({
key: "textState", // unique ID (with respect to other atoms/selectors)
default: "", // default value (aka initial value)
})
const charCountState = selector({
key: "charCountState", // unique ID (with respect to other atoms/selectors)
get: ({ get }) => {
const text = get(textState)
return text.length
},
})
const TestPage = () => {
const [text, setText] = useRecoilState(textState)
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
}
const count = useRecoilValue(charCountState)
return (
<div
style={{
padding: "3rem",
}}
>
<h2
style={{
fontSize: "2rem",
marginBottom: "1rem",
}}
>
DEMO
</h2>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
<br />
Character Count: {count}
</div>
)
}
export default TestPage
그럼 이제 다음과 같이 제대로 작동하는 것을 확인할 수 있을 것입니다.
Recoil을 써보니 그렇게 간결하다던 MobX 못지 않게 간결하고 오히려 더 편하다는 느낌도 받았는데요, 아직 초기버전이니 앞으로 얼마나 더 활성화될지 더욱 기대가 되는 라이브러리입니다!
ESLint의 no-unused-vars 규칙과 Typescript 인터페이스 충돌 해결! (2) | 2021.05.19 |
---|---|
NextJS에 Recoil 적용하기 (NextJS React 프로젝트 07) (0) | 2021.05.18 |
NextJS에 ESLint와 Prettier 적용하기 (NextJS React 프로젝트 06) (1) | 2021.05.17 |
JS코드를 깔끔하게 해주는 ESLint 알아보기! (적용방법과 상세 옵션) (0) | 2021.05.16 |
NextJS에서 링크 태그로 라우팅시키는 방법 <Link> (NextJS React 프로젝트 05) (0) | 2021.05.11 |
댓글 영역