📑 개요
리코일은 프로젝트를 하며 많이 사용했는데...
아무래도 리덕스에 비해 사용법이 쉬워서 짧은 시간 진행하는 프로젝트에 적용하기가 쉽다보니까.
흠 하지만 실무에서도 리코일을 적용할 수 있는가는 의문이었다.
차근차근 알아가며 고민해보자
🗂 Recoil이란?
2020년 Meta에서 발표한 라이브러리로 React의 상태관리를 돕는다.
Atom과 Seclector만으로 간단히 사용이 가능하며 리액트의 규칙(선언적 UI, 컴포넌트 중심 설계, Hooks)을 따르기 때문에 기존 React 개발자들에게 친숙하다.
- Recoil이어야 하는 이유
React의 useState Hook으로도 State 컴포넌트의 구축은 가능하나, 서비스 규모가 커지고 컴포넌트 간 관계가 복잡해질 때 상태를 효율적으로 관리하기 위한 라이브러리의 사용이 필요시 된다.
다양한 상태관리 라이브러리 중 Recoil을 사용하는 이유는 다음과 같다.
- 사용을 위해 들이는 비용이 적다.
- Recoil은 최소한의 API만을 제공하기 때문에 다른 상태 관리 라이브러리에 비해 러닝커브가 낮다.
- React를 위한 라이브러리로 제작되었다.
- 상태관리 라이브러리에는 Redux, MobX, Zustand 등 여러가지가 있으나, 기존에 자주 사용되던 라이브러리는 React를 위한 라이브러리가 아니었다. 하지만 Recoil은 React를 위한 라이브러리로 제작되어 store와 같이 외부요인으로 취급받지 않고, React 내부 스케줄러에 접근하여 렌더링을 최적화하는 데 활용이 가능하다.
사실 이 글이 2024년에 적는 게 아니었다면, 3번째 이유로는 React는 Meta에서 만들었으며, Recoil이 Meta에서 React를 위해 만든 상태관리 라이브러리인만큼 유지 보수와 업데이트, 커뮤니티가 활발할 것을 기대한다는 점을 적는 게 마땅했을 것이다.
하지만 현재로써는 Recoil의 담당 엔지니어가 정리 해고를 당하기도 했고(현재 내부 팀이 어떻게 변경되었을지는 모르지만)...... 생각보다 업데이트가 활발하게 이루어지지 않아 저물어가는 라이브러리라는 이야기도 있다.
- 설치 방법
npm install recoil
yarn add recoil
* Recoil은 ES5 문법으로 변환되지 않으므로 ES6부터 사용하는 것을 권장한다.
Babel 컴파일 이후 preset @babel/preset-env을 이용하여 사용할 수 있으나 문제가 발생할 수 있다.
- CDN을 통한 설치
<script src="https://cdn.jsdelivr.net/npm/recoil@0.0.11/umd/recoil.production.js"></script>
- Eslint 설정
공식문서에서는 useRecoilCallback을 additionalHooks에 추가하길 권장한다.
useRecoilCallback()을 사용하기 위해 전달된 종속성이 잘못 지정됐을 때 올바르게 사용되기 위함이다.- useRecoilCallback()은 비동기 작업이나 콜백을 다룰 때 사용된다. React의 훅 규칙은 훅이 컴포넌트의 최상위에서만 호출되고, 조건문이나 반복문 안에서 호출되건 안된다.
기본적으로 eslint-plugin-react-hooks는 React의 내장 훅(useState, useEffect 등)에 대해서만 이러한 규칙을 적용하므로, useRecoilCallback()에도 동일한 규칙을 강제하기 위해 필요하다.
- useRecoilCallback()은 비동기 작업이나 콜백을 다룰 때 사용된다. React의 훅 규칙은 훅이 컴포넌트의 최상위에서만 호출되고, 조건문이나 반복문 안에서 호출되건 안된다.
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useRecoilCallback"
}
]
}
}
⌨ Recoil 사용방법
- RecoilRoot 설정하기
recoil 컴포넌트는 부모 트리 어딘가에 RecoilRoot가 필요하다. 이 장소로 Root 컴포넌트를 추천한다.
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
- Atom
상태(State)의 일부를 나타내며 어떤 컴포넌트에서도 읽고 쓰는 게 가능하다.
Recoil은 구독(subscription)이란 걸 하는데, 이는 atom이나 selector의 변경을 감지하고, 반응하는 것을 뜻한다.
atom은 전역으로 공유되며, atom에 변화가 있을 경우 atom을 바라보는 컴포넌트들은 재렌더링 된다.
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
- useRecoilState()
Recoil 라이브러리에서 제공하는 Hook으로 React 컴포넌트에서 Recoil 상태를 읽고 업데이트할 수 있게 해준다.
React의 useState 훅과 비슷하지만 useRecoilState는전역 상태를 관리할 수 있다는 차이가 있다.
- atom을 읽고 쓰게 하는 코드 예시
사용자가 text를 입력할 때마다 textState의 상태가 변경된다.
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
- Selector
파생 상태(Derived State)를 생성하고 관리하는 데 사용된다. 파생 상태는 Atom이나 다른 Selector로부터 계산된, 의존 관계에 의해 생성되는 상태이다.
Selector는 기본 상태(Atom)로부터 파생된 상태를 계산한다. 이를 통해 하나 이상의 Atom이나 다른 Selector로부터 값을 계산해서 새로운 상태를 만들 수 있다.
Selector는 순수 함수로 동작하고, 필요한 데이터를 효율적으로 캐싱하여 성능을 최적화할 수 있다.
const charCountState = selector({
key: 'charCountState', // unique ID (with respect to other atoms/selectors)
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
주어진 코드에서 charCountState는 textState의 변경에 의존하는 파생 상태이다.
순수 함수이며, textState의 값이 변경될 때만 재계산되어 상태 관리에서 불필요한 계산을 최소화한다.
- useRecoilValue()
Recoil 라이브러리에서 제공하는 Hook으로 Recoil 상태(Atom이나 Selector)의 현재 값을 읽어올 때 사용된다.
상태 값을 읽기 전용으로 할 때(값의 업데이트 없이 현재 상태를 가져와서 사용하는 경우) 유용하다.
상태가 변경될 때 컴포넌트를 자동으로 리렌더링한다.
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
위의 코드를 모두 합치면 input에 글자를 적을 때마다 Echo에 text가 변경되며, Character Count가 text의 length를 세는 기능이 구현된다.
이 페이지의 가장 아래 스크롤에서 구현 기능 확인 가능!
- Recoil에서 비동기로 데이터를 받아올 때 State를 관리하는 법
Recoil에선 selector를 통해 비동기 데이터를 다룬다. 비동기 데이터의 로딩, 성공, 에러를 관리할 수 있다.
이를 위해선 get 메서드 안에 async 함수를 사용하여 데이터를 비동기로 가져오고 결과를 반환한다.
- fetch를 사용하여 데이터를 가져오는 예시
const charCountState = selector({
key: 'charCountState',
get: async ({ get }) => {
try {
// 비동기 요청 수행
const response = await fetch('https://api.example.com/data');
// 응답이 정상인지 확인
if (!response.ok) {
throw new Error('Failed to fetch data');
}
// 데이터 추출
const data = await response.json();
// 데이터를 기반으로 문자 수 계산
const text = data.someTextField; // 데이터의 특정 필드를 사용
return text.length;
} catch (error) {
throw error;
}
},
});
⏳ Loadable이란?
Recoil 라이브러리에서 비동기 데이터 로딩 상태를 나타내는 특정 유형의 객체이다.
useRecoilValueLoadable은 비동기 셀렉터의 상태를 로딩, 성공, 에러로 구분하여 처리할 수 있는 훅이다.
이 훅은 Loadable 객체를 반환하며, 객체는 다음 세 가지 상태를 가진다.
- Loadable 객체
- state: 상태 값은 loading, hasValue, hasError 중 하나이다.
- loading: 데이터가 아직 로딩 중임을 나타낸다.
- hasValue: 데이터가 로딩되어 성공적으로 값이 있는 상태를 나타낸다.
- hasError: 데이터 로딩 중에 에러가 발생한 상태를 나타낸다.
- contents: 상태 값이 hasValue일 때 실제 데이터를 포함한다.
- error: 상태 값이 hasError일 때 에러 정보를 포함한다.
- state: 상태 값은 loading, hasValue, hasError 중 하나이다.
- Recoil에서 로딩, 성공, 에러를 처리하는 법
1. Atom 정의
기본 상태를 정의한다.
import { atom } from 'recoil';
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
2. 비동기 셀렉터 정의
API 호출을 통해 데이터를 받아오고, 데이터의 특정 필드를 반환하는 비동기 셀렉터를 정의한다.
import { selector } from 'recoil';
const asyncDataSelector = selector({
key: 'asyncDataSelector',
get: async ({ get }) => {
try {
// 비동기 요청 수행
const response = await fetch('https://api.example.com/data');
// 응답이 정상인지 확인
if (!response.ok) {
throw new Error('Failed to fetch data');
}
// 데이터 추출
const data = await response.json();
return data.someTextField; // 데이터를 기반으로 반환
} catch (error) {
throw error;
}
},
});
3. 컴포넌트에서 useRecoilValueLoadable 사용
useRecoilValueLoadable을 사용하여 비동기 셀렉터의 상태를 가져오고, 로딩, 성공, 에러 상태에 따라 적절한 UI를 렌더링한다.
import React from 'react';
import { useRecoilValueLoadable } from 'recoil';
import asyncDataSelector from './asyncDataSelector';
function AsyncDataComponent() {
const loadable = useRecoilValueLoadable(asyncDataSelector);
switch (loadable.state) {
case 'loading':
return <div>Loading...</div>;
case 'hasValue':
return <div>Data: {loadable.contents}</div>;
case 'hasError':
return <div>Error: {loadable.error.message}</div>;
default:
return null;
}
}
export default AsyncDataComponent;
- 로딩 상태: 데이터를 로드하는 동안 표시할 UI를 정의합니다. 이 예제에서는 "Loading..." 텍스트를 표시한다.
- 성공 상태: 데이터를 성공적으로 받아온 경우, loadable.contents에 접근하여 데이터를 표시한다.
- 에러 상태: 데이터를 받아오는 도중 에러가 발생한 경우, loadable.error에 접근하여 에러 메시지를 표시한다.
- 추가 개념 : Suspense를 통한 비동기 처리
Suspense는 컴포넌트 렌더링을 잠시 멈추고, 비동기 작업이 완료될 때까지 대기하도록 해주는 기능이다.
주로 비동기 데이터 로딩, 코드 스플리팅, 이미지 로딩 등 다양한 상황에서 사용된다.
- Fallback UI: 비동기 작업이 완료될 때까지 보여줄 대체 UI를 정의할 수 있다. 일반적으로 로딩 스피너나 "Loading..." 메시지를 표시한다.
- Boundary: Suspense는 컴포넌트 트리에서 특정 부분을 감싸며, 이 영역 내에서 발생하는 비동기 작업을 처리한다. 이를 통해 비동기 작업이 완료될 때까지 이 영역의 렌더링을 지연시킬 수 있다.
import React, { Suspense } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import asyncDataSelector from './asyncDataSelector';
function AsyncDataComponent() {
const data = useRecoilValue(asyncDataSelector);
return <div>Data: {data}</div>;
}
function App() {
return (
<RecoilRoot>
<Suspense fallback={<div>Loading...</div>}>
<AsyncDataComponent />
</Suspense>
</RecoilRoot>
);
}
export default App;
- Recoil에서 비동기 상태를 관리할 때 useRecoilValueLoadable 훅을 사용하면 로딩, 성공, 에러 상태를 명확하게 구분하여 처리할 수 있다는 장점이 있다.
- Suspense를 사용한다면 비동기 데이터 로딩이 더 간단하고 직관적으로 처리된다는 장점이 있다.
📝 Redux와 Recoil, 어떤 것을 선택할까?
- Recoil과 Redux의 개념
Recoil | Redux |
React의 전역 상태와 비동기 상태를 쉽게 관리할 수 있게 해주는 상태 관리 라이브러리다. state는 Atoms와 Selectors로 나누어 관리되며, React의 컴포넌트 트리와 더 밀접하게 통합된다. |
전역 상태 관리를 위해 Flux 아키텍처를 기반으로 한 라이브러리다. state가 하나의 중앙 스토어에서 관리되며, 상태 변경은 순수 함수인 리듀서를 통해 일어난다. 액션과 디스패치 매커니즘을 사용한다. |
- Recoil과 Redux의 장단점 비교
Recoil | Redux | |
러닝 커브 | 비교적 완만, 리액트 훅과 유사한 API |
가파름, 보일러플레이트 코드 많음 |
상태 관리 방식 | Atoms, Selectors | Actions, Reducers, Store |
비동기 상태 관리 | 내장된 비동기 셀렉터 지원 | Redux Thunk, Redux Saga 등 미들웨어 필요 |
구성 요소 | Atoms, Selectors, Hooks | Actions, Reducers, Store, Middleware |
커뮤니티 및 생태계 | 상대적으로 작음 | 매우 크고 성숙 |
개발자 도구 | Recoil DevTools (아직 성숙하지 않음) | Redux DevTools (매우 성숙) |
성능 | 컴포넌트 수준의 세밀한 구독 관리 |
전역 상태 변경에 따른 리렌더링 최적화 필요 |
타입스크립트 지원 | 기본 지원 | 기본 지원, 많은 타입 정의 필요 |
미들웨어 지원 | - | 다양한 미들웨어 (Thunk, Saga 등) |
데이터 흐름 | 단방향, React 상태와 통합 용이 |
단방향, 명시적 액션 기반 |
전역 상태 관리 | 매우 효과적 | 매우 효과적 |
로컬 상태 관리 | 쉽게 통합 가능 | 별도의 리듀서 정의 필요 |
레거시 코드베이스와의 통합 |
비교적 쉬움, React 코드와 통합 용이 |
비교적 복잡, 보일러플레이트 코드 추가 필요 |
리액트 전용 여부 | 리액트 전용 | React에 주로 사용되지만 다른 프레임워크와도 사용 가능 |
어떤 것을 선택할지는 본인의 프로젝트 여부에 따라 달라질 것이다.
비교를 보고 잘 고민해보길...!
✨ 마무리
흐아악 생각보다 너무 길고 긴 내용이었다!!!!!!!!!!!
정리하다가 혼이 나가... 돌아오지 않아...
근데 생각보다 기능이... 상태관리 말고도 loadable 객체도 있고... 저건 내가 쓴 기억이 없는데 아쉽.
넥제로 토이프젝 하려 했는데 공부할수록 react 토이 프젝을 새로 하고 싶어지는 마음...
새로 공부한 기능들 써보구 싶어...
- 요약
- Recoil
React의 상태관리를 도우며 Atom과 Seclector만으로 간단히 사용이 가능하다. - Atom
상태(State)의 일부를 나타내며 어떤 컴포넌트에서도 읽고 쓰는 게 가능하다. - Selector
파생 상태(Derived State)를 생성하고 관리하는 데 사용된다. - useRecoilState
Recoil 라이브러리에서 제공하는 Hook으로 React 컴포넌트에서 Recoil 상태를 읽고 업데이트할 수 있게 해준다. - useRecoilValue
Recoil 라이브러리에서 제공하는 Hook으로 Recoil 상태(Atom이나 Selector)의 현재 값을 읽어올 때 사용된다. - Loadable
Recoil 라이브러리에서 비동기 데이터 로딩 상태를 나타내는 특정 유형의 객체이다. - useRecoilValueLoadable
비동기 셀렉터의 상태를 로딩, 성공, 에러로 구분하여 처리할 수 있는 훅이다.
'개발 > Front' 카테고리의 다른 글
[React] React Hook Form으로 양식 구현하기 (3) | 2024.06.10 |
---|---|
[React] React.fragment의 사용법과 사용 이유 (1) | 2024.06.09 |
[React] 제어 컴포넌트와 비제어 컴포넌트의 차이 (1) | 2024.06.09 |
[React] 메모이제이션 (1) | 2024.06.09 |
[React] HTML과 React의 이벤트 처리 차이점 (0) | 2024.04.17 |