Recoil 레퍼런스를 토대로 진행했습니다.
Recoil
A state management library for React.
recoiljs.org
리코일(Recoil)
호환성 및 단순함을 이유로 외부의 글로벌 상태관리 라이브러리보다는 React 자체에 내장된 상태 관리 기능을 사용하는 것이 가장 좋다. 그러나 React는 다음과 같은 한계가 있다.
- 컴포넌트의 상태는 공통된 상위요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.
- Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
- 이 두 가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게 한다.
우리는 리코일을 통해 API와 의미 및 동작을 가능한 React답게 유지하면서 이것을 개선하고자 한다.
리코일은 페이스북에서 개발한 오픈소스 상태 관리 라이브러리이다. 리코일은 리액트 컴포넌트 간 공유되는 상태를 관리하는 데 사용된다.
리코일은 전역 상태 관리를 위한 리덕스나 MobX와 같은 라이브러리와는 다르게, 로컬 상태를 관리하는데 초점을 둔다. 이것은 리코일이 간단하고 직관적인 상태 관리를 가능하게 하며, 동시성 모드와 같은 리액트의 새로운 기능들과의 호환성도 보장한다.
리코일은 직교(orthogonal)하지만 본질적인 방향 그래프를 정의하고 React 트리에 붙인다. 상태 변화는 이 그래프의 뿌리(atoms)로부터 순수함수(selectors)를 거쳐 컴포넌트로 흐르며, 다음과 같은 접근 방식을 따른다.
- 리코일은 공유상태(shared state)도 React의 내부상태(local state)처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다. (필요한 경우 reducers 등으로 캡슐화할 수도 있다.)
- 리코일은 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.
- 상태 정의는 점진적이고(incremental) 분산되어 있기 때문에, 코드 분할이 가능하다.
- 상태를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있다.
- 파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 파생된 데이터는 동기식과 비동기식 간에 이동할 수 있다.
- 리코일은 탐색을 일급 개념으로 취급할 수 있고 심지어 링크에서 상태 전환을 인코딩할 수도 있다.
- 전체 애플리케이션 상태를 하위 호환되는 방식으로 유지하기가 쉬우므로, 유지된 상태는 애플리케이션 변경에도 살아남을 수 있다.
아톰(Atoms)
리코일을 사용하면 atom(공유 상태)에서 selectors(순수 함수)를 거쳐 리액트 컴포넌트로 내려가는 data-flow-graph를 만들 수 있다. Atoms는 컴포넌트가 구독할 수 있는 상태의 단위이다. Selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환한다.
여기서, 리코일의 핵심 개념은 아톰(atom)이다. 아톰은 리액트 컴포넌트에서 사용되는 상태의 단위이며, 일종의 전역 변수라고 생각할 수 있다. 아톰은 읽기 및 쓰기가 가능하며, 다른 아톰과 연결될 수 있다. 또한, 아톰을 통해 업데이트와 구독이 가능하다. 아톰이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영해 다시 렌더링된다. 아톰은 런타임에서 생성될 수도 있다. 리액트의 로컬 컴포넌트에서 상태 대신 사용할 수도 있으며, 동일한 아톰이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.
아톰은 atom
함수를 통해 생성한다.
import { atom } from 'recoil';
export const isLoggedInState = atom({
key: 'isLoggedInState',
default: false,
});
아톰은 디버깅, 지속성 및 모든 아톰의 map을 볼 수 있는 특정 API에 사용되는 고유한 키가 필요하다. 두개의 atom이 같은 키를 갖는 것은 오류이기 때문에, 키값은 전역적으로 고유하도록 해야한다.
위의 코드에서는 isLoggedInState라는 키 값을 가지고 있고, default 값으로 false를 가진다.
- useRecoilState
useRecoilState는 아톰의 값을 읽고 쓰는 데 사용되며, 아톰의 값과 그 값을 변경할 수 있는 setState 함수를 반환한다. 위에서 정의한 isLoggedInState를 계속 활용해 다음과 같은 코드를 작성할 수 있다.
import { useRecoilState } from 'recoil';
import { isLoggedInState } from './atoms';
function LoginButton() {
const [isLoggedIn, setIsLoggedIn] = useRecoilState(isLoggedInState);
function handleLogin() {
setIsLoggedIn(true);
}
return (
<>
{isLoggedIn ? (
<p>Welcome, user!</p>
) : (
<button onClick={handleLogin}>Log In</button>
)}
</>
);
}
이 코드에서는 useRecoilState를 사용해 isLoggedInState의 값을 읽고 쓰고 있다. isLoggedInState의 값을 읽기 위해서는 배열 비구조화 할당 문법을 사용해 isLoggedIn과 setIsLoggedIn 함수를 얻어와야 한다. isLoggedIn은 isLoggedInState의 값이며, setIsLoggedIn은 isLoggedInState의 값을 변경하는 함수이다.
이제 handleLogin 함수를 호출하면 setIsLoggedIn 함수를 사용하여 isLoggedInState의 값을 변경할 수 있다. setIsLoggedIn 함수를 호출할 때 전달되는 값은 isLoggedInState의 새로운 값이 된다.
위의 코드에서는 타입스크립트를 사용하여 작성되었다. 따라서 타입스크립트를 사용하려면 해당 파일의 상단에 다음과 같은 코드를 추가해야 한다.
import React from 'react';
import { useRecoilState } from 'recoil';
import { isLoggedInState } from './atoms';
function LoginButton(): JSX.Element {
// ...
}
또한, isLoggedInState의 타입을 명시해주어야 한다. isLoggedInState는 atom 함수로 생성된 상태이므로, atom 함수가 반환하는 타입을 사용하여 isLoggedInState의 타입을 지정할 수 있다.
import { atom, AtomEffect } from 'recoil';
type IsLoggedInState = boolean;
export const isLoggedInState = atom<IsLoggedInState>({
key: 'isLoggedInState',
default: false,
});
위의 코드에서는 IsLoggedInState라는 새로운 타입을 정의하고, isLoggedInState의 타입으로 사용했다. 이렇게 하면 코드의 가독성이 높아지고, 타입 에러를 방지할 수 있다.
Selectors
리코일에서 Selector는 기존 상태를 이용해 새로운 값을 계산하는 함수이다. 이전에 정의된 아톰을 이용해 Selector에서 새로운 값을 만들어낼 수 있다.
Selector는 아톰과 마찬가지로 RecoilRoot 컴포넌트 내에서 정의된다. Selector는 아톰에 의존하며, 의존하는 아톰의 값이 변경될 때마다 Selector의 값도 변경된다. 이를 통해 Selector는 의존하는 아톰에 따라 동적으로 값을 계산할 수 있다.
위의 아톰을 이용해 Selector를 만들어보자.
import { selector } from 'recoil';
import { isLoggedInState } from './atoms';
type UserNameState = string;
export const userNameState = selector<UserNameState>({
key: 'userNameState',
get: ({ get }) => {
const isLoggedIn = get(isLoggedInState);
if (isLoggedIn) {
return 'user';
}
return '';
},
});
위 코드에서는 isLoggedInState를 사용해 현재 사용자가 로그인되어 있는지 여부를 파악하고, 이에 따라 userNameState를 반환한다. 이 Selector는 isLoggedInState에 의존하므로, isLoggedInState가 변경될 때마다 userNameState도 자동으로 변경된다.
Selector는 여러 개의 Atom에 의존할 수 있다. Selector의 get 함수에서는 get 함수를 사용하여 다른 Atom의 값을 읽어올 수 있다. 이를 통해 여러 개의 Atom을 조합하여 새로운 값을 계산할 수 있다.
get
속성은 계산될 함수이며, 전달되는 get 인자를 통해 아톰과 다른 selectors에 접근할 수 있다. 다른 아톰이나 selectors에 접근하면 자동으로 종속 관계가 생성되므로, 참조했던 다른 아톰이나 selectors가 업데이트되면 이 함수도 다시 실행된다.
import { useRecoilValue } from 'recoil';
import { userNameState } from './selectors';
function Greeting() {
const userName = useRecoilValue(userNameState);
return (
<>
<p>Hello, {userName}!</p>
</>
);
}
위의 코드에서는 useRecoilValue를 사용하여 userNameState의 값을 읽어오고 있다. userNameState는 Selector이므로, useRecoilValue를 사용하여 값을 읽어올 수 있다.
여기까지 작성했을 때, 화면을 렌더링해보자.
import React from 'react';
import { RecoilRoot } from 'recoil';
import LoginButton from './LoginButton';
import Greeting from './Greeting';
function App() {
return (
<RecoilRoot>
<LoginButton />
<Greeting />
</RecoilRoot>
);
}
export default App;
리코일 시작하기
본격적으로 리코일을 사용해보자. 리코일은 리액트를 위한 상태 관리 라이브러리이므로 리액트가 설치되어 있어야 한다.
npm create-react-app recoil-tutorial --template typescript
npm install @types/recoil
RecoilRoot
리코일에서 상태를 나타내는 컴포넌트는 부모 트리 어딘가에 나타나는 RecoilRoot
가 필요하다. 루트 컴포넌트가 RecoilRoot를 넣기에 가장 좋다.
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
export default App;
CharacterCounter
는 이후에 구현할 예정이다.
Atom
아톰은 상태의 일부를 나타낸다. 아톰은 어떤 컴포넌트에서나 읽고 쓸 수 있다. 아톰의 값을 읽는 컴포넌트들은 암묵적으로 아톰을 구독한다. 그래서 아톰에 변화가 있다면 그 아톰을 구독하는 모든 컴포넌트들이 리렌더링되는 결과가 발생할 것이다.
const textState = atom({
key: "textState",
default: "",
});
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
위 코드는 App 컴포넌트 코드 밑에 작성해주었다.
textState라는 아톰을 생성해주고, 이 아톰은 문자열 값을 가지며, default
값은 빈 문자열이다.
또한 TextInput
컴포넌트에서는 useRecoilState
Hook을 사용해 textState 값을 읽고 쓰는 것을 구현하고 있다. onChange
핸들러를 사용해 input 요소의 값이 변경될 때마다 textState
값을 업데이트 해준다.
Selector
Selector는 파생된 상태의 일부를 나타낸다. 파생된 상태란 상태의 변화를 말하며, 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
const charCountState = selector({
key: "charCountState",
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
charCountState
라는 새로운 selector를 정의해주었다. 이 selector는 textState
를 의존성으로 갖는다. 따라서 textState가 변경될 때마다 charCountState도 다시 계산된다.
charCountState의 get
함수는 get인자를 받아들이며, get함수를 호출해 textState의 최신 값을 가져온다. 그 다음, text의 길이를 반환한다. 따라서 charCountState
는 현재 textState의 길이에 대한 실시간 값을 제공한다.
CharacterCount
컴포넌트는 useRecoilValue
를 사용해 charCountState의 값을 구독하고 업데이트 될 때마다 다시 렌더링된다. count는 현재 charCountState의 값이며 CharacterCount 컴포넌트는 textState의 길이를 표시하는 간단한 문자 수 카운터이다.
아래는 렌더링한 결과이다.
'WEB > Recoil' 카테고리의 다른 글
리코일 사용해보기 - 2 [React/Recoil/typescript] (0) | 2023.03.30 |
---|