4.1.1 Recoil4.1.2 Recoil 특징4.1.3 Redux와 Recoil의 차이점1) 상태 관리 방식2) 보일러플레이트 코드와 복잡성3) 개발자 경험4) 비동기 지원4.1.4 Recoil 상태 관리1) RecoilRoot2) Atom3) Selector(1) Atom을 구독하는 기능(2) 서버의 response 값을 저장하는 역할4) Recoil Hook(1) useRecoilState(2) useRecoilValue(3) useSetRecoilState(4) useResetRecoilState
4.1.1 Recoil
리코일(Recoil)은 페이스북에서 개발한 자바스크립트 라이브러리로, 리액트 애플리케이션의 상태 관리를 위한 도구입니다. 리액트는 컴포넌트를 재사용하는 방식으로 개발하기 때문에 상태 관리가 복잡해질 수 있는데, 리코일은 이러한 문제를 해결하기 위한 상태 관리 방법을 제공합니다. 이 챕터에서는 리코일에 대한 포괄적인 소개를 제공하며, 리액트 개발에서의 역할과 중요성을 설명합니다. 리코일의 atoms, selectors, RecoilRoot 등 핵심 개념을 다루며, 이러한 기본 개념을 이해하는 예제 코드를 제공합니다.
4.1.2 Recoil 특징
리코일은 컴포넌트 범위의 상태를 지원하며 다음과 같은 특징을 가집니다.
- 상태 관리의 유연성
리코일을 사용하면 상태를 개별적으로 관리할 수 있습니다. 상태를 저장하고 조회하는 방법을 제공하며, 상태를 변경하는 함수(effect)를 등록할 수 있습니다.
- 비동기 처리 지원
리코일은 데이터 가져오기 및 부작용과 같은 작업에 중요한 비동기 데이터 처리를 위한 기본 지원을 제공합니다. 이 기능은 애플리케이션 내에서 비동기 작업 처리를 단순화합니다.
- React Hooks 통합
리코일은 리액트 훅과 함께 사용되어 리액트 애플리케이션 내에서 사용이 용이합니다. useRecoilState, useRecoilValue 등의 훅을 사용해 개발자가 사용하기 편리합니다.
- 효율적인 재 렌더링
리코일은 세부적으로 종속성을 추적해 재 렌더링을 최적화합니다. 구성 요소는 종속된 특정 상태가 변경될 때만 다시 렌더링 되어 성능이 향상되고 사용자 경험이 향상됩니다.
4.1.3 Redux와 Recoil의 차이점
리덕스와 리코일은 모두 리액트 애플리케이션의 상태 관리를 지원하는 라이브러리이지만, 다양한 접근 방식과 특징을 가지며 주요 차이점은 다음과 같습니다.
1) 상태 관리 방식
- Redux : 리덕스는 중앙 집중식 상태 관리 패턴을 따릅니다. 애플리케이션의 모든 상태는 단일 스토어에 중앙 집중식으로 저장되며, 모든 컴포넌트가 이 스토어에서 상태를 읽고 변경할 수 있습니다. 리덕스의 상태 업데이트는 불변성을 유지하며 액션과 리듀서를 통해 이루어집니다.
- Recoil : 리코일은 컴포넌트 범위의 상태 관리를 강조합니다. 각 컴포넌트는 자체 상태를 갖고, 다른 컴포넌트에 영향을 미치지 않습니다. 리코일의 상태는 아톰과 셀렉터를 사용해 정의되며, 각 컴포넌트에서 필요한 상태를 선택적으로 구독하거나 수정할 수 있습니다.
2) 보일러플레이트 코드와 복잡성
- Redux : 리덕스는 액션 타입, 액션 생성자, 리듀서 등을 정의하는 보일러플레이트 코드가 많이 필요합니다. 이에 따라 코드가 증가하고, 상태 업데이트 로직이 상대적으로 복잡해질 수 있습니다.
- Recoil : 리코일은 리덕스보다 훨씬 적은 보일러플레이트 코드가 필요합니다. 대부분의 작업은 간결한 구문으로 처리되며, 리액트 훅과 통합해 사용할 수 있습니다. 따라서 코드 작성과 유지보수가 훨씬 간단해집니다.
3) 개발자 경험
- Redux : 리덕스는 초기 학습 곡선이 상대적으로 높을 수 있으며, 상태 관리에 대한 명확한 계획과 구조가 필요합니다. 중앙 스토어의 개념과 데이터 흐름을 이해해야 합니다.
- Recoil : 리코일은 리액트 훅과 유사한 구문을 사용하므로 리덕스보다 상대적으로 쉽게 배울 수 있습니다. 컴포넌트 범위의 상태 관리를 지원하기 때문에 상대적으로 덜 복잡합니다.
4) 비동기 지원
- Redux : 비동기 작업을 처리하기 위해서는 리덕스 미들웨어를 사용해야 합니다. Redux-Thunk, Redux-Saga 등의 미들웨어를 통해 비동기 작업을 처리할 수 있습니다.
- Recoil : 리코일에는 셀렉터라는 개념이 있고 이를 사용해 비동기 상태를 처리할 수 있습니다. 비동기 데이터 패치 및 사이드 이펙트 관리를 간단하게 수행할 수 있습니다.
4.1.4 Recoil 상태 관리
1) RecoilRoot
RecoilRoot는 리코일 라이브러리를 사용하기 위해 리액트 애플리케이션 최상위 컴포넌트에 한 번만 추가하면 사용 가능합니다. 그리고 App 컴포넌트 위에 감싸면 App 컴포넌트와 그 하위에 있는 모든 컴포넌트들은 리코일 기능을 사용할 수 있습니다. RecoilRoot는 initializeState() 함수와 override 속성을 선택적으로 사용할 수 있는데 initializeState 함수는 초기 상태 값을 설정할 수 있고 애플리케이션이 처음 실행될 때 한 번 호출되어 초기 상태 값을 제공합니다.
아래 예시를 보시면 initializeState 함수는 set 함수를 인자로 받아서 사용합니다. 이 함수는 리코일 초기 상태를 변경하는 데 사용되며, count 상태 초깃값을 5로 설정했습니다. 따라서 이 코드가 실행되면, count 상태 값은 5가 됩니다.
import React from 'react'; import { RecoilRoot } from 'recoil'; import App from './App'; import { Counter } from 'atom이 정의된 파일 경로'; function initializeState({ set }) { set(Counter, 5); } function Root() { return ( <RecoilRoot initializeState={initializeState}> <App /> </RecoilRoot> ); } export default Root;
override 속성은 리코일 상태를 테스트하거나 모의(mock, 테스트할 때 필요한 실제 객체와 동일한 모의 객체) 하기 위해 사용될 수 있습니다. override를 사용하면 특정 리코일의 아톰의 초깃값을 변경할 수 있습니다. 이렇게 보면 둘의 차이가 없어 보이지만, 사용 시점에서 차이가 있습니다. initializeState는 애플리케이션 시작할 때 리코일의 초기 상태 값을 재정의하여 설정하지만, override는 주로 테스트 케이스에서 사용되며, 특정한 조건에서 아톰 혹은 셀렉터의 값을 임의적으로 변경하고 싶을 때 사용합니다.
2) Atom
아톰(Atom)은 리덕스의 스토어와 같은 개념으로 리코일의 상태 단위를 나타내며 변경과 구독이 가능합니다. 아톰은 여러 개로 생성이 가능하며 업데이트되면 구독하고 있는 컴포넌트는 새로운 값으로 업데이트되고 재렌더링이 됩니다.
아톰 함수는 key와 default 속성을 가졌는데 key는 여러 아톰을 식별하는데 필요한 고유 문자열입니다. default 속성은 아톰의 초깃값을 설정해 줍니다. 여러 아톰이 같은 이름의 키를 가지면 오류가 발생하기 때문에 전역적으로 유일해야 합니다.
const Atom = atom({ key: '키값', default: 초기값, });
3) Selector
Selector(셀렉터)는 global state를 선언하는 함수 중 하나입니다. Atom(아톰)이 리코일에서 상태의 최소단위라면 셀렉터는 아톰을 기반으로 파생된 데이터라고 할 수 있습니다. 셀렉터는 side effect가 없는 순수 함수이기 때문에 같은 인풋이 들어오면 같은 인풋을 리턴합니다. 따라서 리코일의 상태를 셀렉터로 관리하면 기존의 값을 캐싱하기 때문에 불필요한 연산을 줄일 수 있습니다.
우리는 앞에서 아톰에 대해 배웠고 아톰만을 이용해 전역 상태 값을 변경했습니다. 그러면 왜 selector를 또 이용해야 하는지 의문이 생깁니다. 그 이유는 셀렉터에서만 가능한 두 가지 기능 때문입니다.
(1) Atom을 구독하는 기능
셀렉터 안에 선언된 아톰의 값이 변할 때, 아톰을 구독하고 있다가 셀렉터에 할당된 함수가 다시 한번 실행됩니다.
(2) 서버의 response 값을 저장하는 역할
서버와 비동기 통신을 할 때 서버에서 받아온 값을 global state 값으로 사용하기 위해서 입니다.
셀렉터의 기본적인 사용방법은 다음과 같습니다.
// 예시 1 const Selector = selector({ key: 'unique key', get: ({get}) => { const 원본 = get(atom) return 원본변형값 } })
읽기만 가능한 셀렉터는 의존성을 기준으로 셀렉터의 값을 호출하는 get 메서드가 있습니다. 의존성 중 어떠한 것이 업데이트되면 셀렉터는 다시 호출됩니다.
- key : 내부적으로 아톰 또는 셀렉터를 식별하는 데 사용되는 고유한 문자열이며 호출할 때 사용됩니다.
- get : 셀렉터는 get이라는 프로퍼티를 통해 셀렉터 내에서 구독하는 아톰의 값을 가져옵니다. 상태 값을 직접 반환하거나 서버와 통신하는 비동기적인 Promise와 같은 유형의 값을 반환할 수 있습니다.
- 매개변수 get : 다른 아톰이나 셀렉터로부터 값을 찾는데 사용되는 함수. 이 함수에 전달된 모든 아톰과 셀렉터는 암시적으로 셀렉터에 대한 의존성 목록에 추가됩니다. 셀렉터의 의존성이 변경되면 셀렉터가 다시 호출됩니다.
const Selector2 = selector({ key: 'unique key', get: ({get}) => { const 원본 = get(atom) return 변형값 }, set: ({get,set}, newValue 또는 DefaultValue) => { const 원본 = get(atom) const 원본2 = get(atom2) // 원본, 원본2, newValue를 활용한 로직 set(atom, 변형값) set(atom2, 변형값) // atom, atom2의 상태값이 변경되면 다시 Selecotr2의 get이 실행된다. } })
그리고 두 번째 셀렉터를 보면 set 프로퍼티가 추가되어 있습니다. 셀렉터는 아톰을 구독하는 하나의 파편이기 때문에 아톰과 달리 자체적으로 setState를 사용할 수 없습니다. 따라서 set 프로퍼티 안에 상태 값을 변경하는 함수를 따로 할당해 줘야 합니다.
- set : 이 속성이 설정되면 셀렉터는 쓰기 가능한 상태를 반환합니다. 첫 번째 매개변수로 콜백 객체와 새로 입력 값이 전달됩니다.
- 매개변수 get : 다른 아톰이나 셀렉터로부터 값을 찾는데 사용되는 함수. 함수는 셀렉터를 주어진 아톰이나 셀렉터를 구독하지 않습니다.
- 매개변수 set : 업스트림 Recoil 상태의 값을 설정할 때 사용되는 함수. 첫 번째 매개변수는 Recoil 상태, 두 번째 매개변수는 새로운 값입니다. 새로운 값은 업데이트 함수나 재설정 액션을 전파하는 DefalutValue 객체일 수 있습니다.
4) Recoil Hook
(1) useRecoilState
useRecoilState는 리코일의 아톰, 셀렉터를 읽고 쓰는데 사용됩니다. 리액트의 useState 함수와 비슷한 형태이며 두 요소를 갖는데 첫 번째 요소는 상태 값이며, 두 번째 요소는 그 상태 값을 변경하는 세터 함수입니다. 그리고 기본값 대신 리코일의 인자를 받습니다.
import { atom,useRecoilState } from 'recoil'; export const Counter = atom({ key: 'Count', default: 0, }); function Counter() { //useRecoilState 사용 const [count, setCount] = useRecoilState(Counter); return ( <> 카운트 : {count} <button onClick{() => setCount(count + 1)}>증가</button> </> ) }
위 코드에서 useRecoilState(resultState)을 호출하여 count 상태 값과 setCount 함수를 얻었습니다. resultState의 초기값이 0이였는데 버튼 클릭 시마다 setCount를 호출하여 count 값이 증가합니다.
(2) useRecoilValue
useRecoilValue는 리코일의 상태를 읽기 전용으로 사용합니다. 리코일의 상태를 읽을 수는 있지만 상태 변경은 할 수 없습니다. 이 훅은 주로 상태 값을 읽고, 화면에 보여주는 경우에 유용합니다.
(3) useSetRecoilState
useSetRecoilState는 리코일의 상태를 변경하는 데 사용합니다. 이 훅은 상태의 현재 값을 제공하지 않기 때문에 상태를 변경할 수는 있지만 그 값을 읽어오지 못합니다. 어떠한 이벤트로 인해 상태 값을 변경해야 하지만 그 값을 읽을 필요가 없을 때 유용합니다. 즉, useRecoilValue와 useSetRecoilState를 합친 것이 useRecoilState 훅이라고 생각할 수 있겠습니다.
(4) useResetRecoilState
useResetRecoilState는 리액트 상태를 초기값(default)으로 리셋하는 데 사용합니다. 이 훅은 주로 atom이나 selector의 값을 초기화 또는 어떠한 이벤트로 인해 상태 값을 초기화해야 할 때 유용합니다.
import { atom,useRecoilState, useResetRecoilState } from 'recoil'; export const countState = atom({ key: 'countState', default: 0, }); function CounterFunc() { //useRecoilState, useResetRecoilState 사용 const [count, setCount] = useRecoilState(countState); const resetCount = useResetRecoilState(countState); return ( <> 카운트 : {count} <button onClick{() => setCount(count + 1)}>증가</button> <button onClick{resetCount}>리셋</button> </> ) }
useResetRecoilState 훅을 활용해서 리셋 버튼을 추가하였습니다. 이 버튼을 클릭 시마다 resultState의 초깃값 0으로 변경됩니다. useResetRecoilState는 주로 atom에 초깃값으로 리셋하는 데 사용되지만, selector에 대해 사용되면, selector의 set 함수가 어떠냐에 따라 달라집니다.
import { atom, useRecoilState, useResetRecoilState, DefaultValue, selector } from "recoil"; export const countState = atom({ key: 'countState', default: 0, }); export const countSelector = selector({ key: "countSelector", get: ({get}): number => { const count = get(countState); return count; }, set: ({set, get}, newValue)=>{ return set( countState, newValue instanceof DefaultValue ? newValue : newValue*2, ) } }) function CounterFunc() { // selector의 useResetRecoilState 사용 const [count, setCount] = useRecoilState(countState); const resetCount = useResetRecoilState(countSelector); return ( <> 카운트 : {count} <button onClick{resetCount}>리셋</button> </> ) }
리코일 공식 문서에서 selector의 set 속성의 두 번째 파라미터인 newValue의 타입은 number | DefaultValue 로 되어있음을 알 수 있습니다. DefaultValue 타입은 set 콜백이 setter(업데이트) 함수를 통해 호출되었는지, resetter(초기화) 함수로 호출되었는지 구분해 주는 역할을 합니다. 위 코드에서는 일반적으로 set을 할 경우 받아온 카운트에 2를 곱해서 반환하지만, DefaultValue가 오게 되면 초깃값을 반환합니다.