【3】 Recoil 작고 유연한 React 상태관리 라이브러리 : kt NexR 기술블로그 ‘21 테크리포트

생성일
2021/12/09 03:52
태그
2021년
상반기
테크리포트
kt NexR
기술블로그
글쓴이
josh.kim
속성
ktNexR_techreport_Recoil 작고 유연한 React 상태관리 라이브러리.pdf
속성 1

I. 서론

SPA( single page application )가 웹 프론트엔드 프로그래밍의 기준이 되면서부터, Model 을 주로 담당하는 상태관리 시스템은 애플리케이션에서 빼놓을 수 없는 매우 중요한 요소가 되었다. React를 개발한 Facebook 에서 직접 Recoil 이라는 상태관리 라이브러리를 소개하였다. Recoil은 무엇이 새로운지, 어떤 장점이 있는지, 과연 배워볼만한 가치가 있는지 알아보도록 한다.

II. 관련 기술/연구, 선행작업

다른 상태관리 라이브러리들을 알아보고, React를 기준으로 하는 recoil의 특징들을 알아보도록 한다.

다른 상태관리 라이브러리들과 recoil

많이들 사용하고 있는 Redux와 Mobx 그렇다면 이러한 상태관리 라이브러리들은 현재 상황에서 어떠한가? 가장 큰 차이점을 보아하자면, 이러한 라이브러리 들은 React에서 자체적으로 만들어진 저장소가 아니였고, react와는 다른 외부 요소로써 다루어져 왔다. 이로 인하여 외부 라이브러리들은 React의 내부 적인 스케쥴러에 접근할 수가 없었다. 지금까지 내부 스케쥴러에 접근할 수 없어도 크게 상관이 없었다고 할 수 있지만, 리엑트에 동시성모드(concurrent mode) 라는것이 생겨나며 상황이 달라졌다. 동시성에 관한건 조금 아래에서 다루도록 하고, 일단 간단하게 다른 상태관리 라이브러리에 대해 알아보도록 하자.

Redux

매우 강력한 기능들을 제공하고 redux-saga와 같은 미들웨어를 통해 확장이 가능하다. Store, Action, Dispatch, Reducer 로 이루어져 있으며, 각각 유동적으로 연동되어 동작한다.
store : 실제 데이터가 저장되는 공간
action : 상태 변화를 일으키게 하는 객체로 type 값을 설정하고 해당 값을 통해 reducer가 동작한다.
dispatch : action을 발생 시키는 것으로 action 을 파라미터로 전달한다.
reducer : action 에서 설정된 type을 통해 현재 state에 데이터를 변경하여 새로운 state를 만들어내는 변화를 일으키는 함수
성능은 강력하지만, 자그마한 상태관리에도 store, action, dispatch, reducer를 모두 설정해 주어야 하며, 이는 코드의 복잡성을 증가시킨다.

Mobx

객체지향적이며, 캡슐화가 가능하다. 불변성 유지를 위한 노력이 불필요하며 보다 간결한 코드작성이 가능하다.
state : class 내부에서 멤버변수처럼 할당하며 observable하게 구독이 가능한 데이터
observable : makeObservable, makeAutoObservable, observable 3가지가 있으며, 모두 추적이 가능한 state로 만들어준다.
observer : 다른 component 에서 props값으로 들어온 mobx 객체를 구독할 수 있도록 한다.
import { observer } from "mobx-react-lite" // Or "mobx-react". const MyComponent = observer(props => ReactElement)
action : state값을 변경하는 역할
computed : state값을 계산되어 리턴하는 값. get double( return this.value * 2) 처럼 state값을 계산하여 리턴한다.

Mobx를 활용한 매우 간단한 예시코드

Timer라는 class 내부에 secondsPassed라는 store값을 생성하고 이값을 makeAutoObservable로 감싸주어 추적이 가능한 state로 만든다. TimerView component내부에서 myTimer값을 props로 받고, 내부에서 reset이라는 action이며, secondsPassed는 state를 변경한다.

context API

여러 컴포넌트들에 전해줘야 하는 props의 경우 이 값을 각 트리 단계마다 props을 명시적으로 내려주어야 하지만, context api를 활용하면 상위 provider에서 값을 내려주면 하위 component에서 props를 사용해서 안받아도 된다. React 내부에사용되기 위해 나온 context API이지만 , Provider에서 전달해주는 value값이 바뀐다면, 그 provider 하위에서 구독하고있는 모든 컴포넌트가 다시 렌더링이 된다.
React.createContext : context 객체를 생성한다. context 객체를 구독하고 있는 컴포넌트를 렌더링 할때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽는다.
const MyContext =React.createContext(defaultValue);
Context.Provider context 객체를 구독하는 component에게 context의 변화를 알리는 역할을 한다.
<MyContext.Provider value={/* 어떤 값 */}>
Class.contextType : class형 componentn를 사용할때 createContext에서 생성한 context객체를 contextType 로 설정할때 사용
Context.Consumer : context 변화를 구독하는 React 컴포넌트
Context.displayName : context 객체에 문자열 속성을 설정할때 사용

React와 같이 보았을때 Recoil의 특징은?

React 자체에 내장된 state 관리 기능을 사용하는 것이 가장 좋다고 볼 수 있지만, React는 다음과 같은 한계가 있다.

React의 태생적 특징

컴포넌트의 상태는 공통된 상위요소까지 끌어올림으로써 공유될 수 있지만, 이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.
Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.
이 두가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 잎(state가 사용되는 곳)까지의 코드 분할을 어렵게한다.

Recoil의 특징 과 접근방식

Recoil은 직교하지만 본질적인 방향 그래프를 정의하고 React 트리에 붙인다. 상태 변화는 이 그래프의 root(atoms라고 부르는)로부터 순수함수(selectors라고 부르는 것)를 거쳐 컴포넌트로 흐르며, 다음과 같은 접근 방식을 따른다.
공유상태(shared state)도 React의 내부상태(local state)처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다. (필요한 경우 reducers 등으로 캡슐화할 수도 있다.)
동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.
상태 정의는 증가 및 분산되므로 코드 분할이 가능하다.
상태를 의존하고 있는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있다.
파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 파생된 데이터는 동기식과 비동기식 간에 이동할 수 있다.
탐색을 일급 개념으로 취급할 수 있고 심지어 링크에서 상태 전환을 인코딩할 수도 있다.
역호환성 방식으로 전체 애플리케이션 상태를 유지하는 것은 쉬우므로, 유지된 상태는 애플리케이션 변경에도 살아남을 수 있다.
쉽게 요약하자면 리엑트의 태생적 단점인 상위 state 데이터가 변경되면 하위 트리가 모두 렌더링 되는 문제를 해결 할 수 있다는 점이다.

그렇다면 동시성 모드(concurrent Mode) 이란 무엇인가?

아시다시피 javascript는 sigle-threaded language로써 각각의 작업들은 현재의 작업이 끝나기 전까지 실행이 중지된다. 하지만 그렇다고해서 한가지 이상의 작업을 동시에 못한다는 말은 아니다.

현실에서의 동시성 모드 예시

예를들어 아침에 일어나 하루를 시작하기 위해, 차한잔과 잼을 바른 토스트를 준비한다고 가정해보자. 만약 싱글스레드 방식으로 이러한 작업을 처리 한다면, 주전자에 물을 넣고 물을 끊인후, 물이 다 끓기까지 기다린후에 찻잔에 차 티백을 준비한후 물을 붓고 차를 준비한다. 그 이후에야 토스트를 준비해야한다.
이런 상황에서 조금 더 효율적으로 차 한잔과 잼을 바른 토스트를 얻기 위해서는, 각각의 작업을 조금 더 작은 단위 작업으로 쪼개고 아래와 같이 순서를 다시 바꾸면 된다.
이것이 바로 동시성의 개념으로, 각각 독립적으로 실행되는 작업들을 더 작은 단위로 쪼개어 구조화 시킴으로써, 싱글 쓰레드라는 한계를 돌파하고 앱을 더 효율적으로 만들 수 있게된다.

브라우저 환경에서 react를 사용할때 동시성 모드의 중요성

브라우저의 UI thread는 CSS, 유저의 입력, 그리고 javascript에 의해 반응하게 되어있다. React는 javascript를 사용함에 따라 한번 컴포넌트 비교조정(reconciliation)국면으로 돌아간다면 해당 작업이 끝나기 전까지 멈추지 않는다. 그사이에 브라우저의 UI thread는 다른 작업, 예를들면 유저가 input 창에 데이터를 입력하는 행위등에 할당될 수가 없다. 이러한 부분 때문에, 비록 react의 비교조정 알고리즘이 매우 효과적일지라 할지라도, 웹 어플리케이션이 점점 더 커지고 dom tree가 커져감에 따라 프레임 드랍이 일어나고, 반응이 자꾸 느려지게 된다. 개발자들이 memoization이나 debounce와 같은 기술들을 사용해서 조금 더 나은 경험을 갖게 하도록 노력할지라도, 결국엔 rendering이라는 큰 트럭이 길을 막고 서있네... 라는걸 깨닫게 된다. 이를 해결하고자 나온게 대표적으로 useDeferredValue와 suspense 와 같은 기술로 간략하게 알아보도록 한다.

useDeferredValue

useDeferredValue는 prop/state를 받아 최대치의 시간만큼 렌더링 시간을 미룰 수 있게 만들어주는 hook이다. 이는 마치 이 state에 의존성을 갖고 있는 component 에게 이 시간까지 미루어도 괜찮아 라는 뜻과 같다.
import { useState, useDeferredValue } from 'react'; const [value, setValue] = useState(''); const deferredValue = useDeferredValue(value, { timeoutMs: 5000 });
value를 props로 받아 사용하는 component가 있다면, 이 components는 최대 5000 ms만큼 미루어져서 렌더링 될 수 있다. 이는 재빠르게 user로부터 input과 같은 데이터를 입력받고 받은 데이터 값을 기준으로 다른 component를 렌더링 하는 화면에서 매우 효과적일 수 있다. 만약 그렇지 않다면 유저들은 input을 통해 데이터를 입력하는 과정에서 react의 redering 때문에 입력이 뚝뚝 끊기게 받게 되어 불편함을 느끼게 된다.

suspense

React 16.8에서 소개된 React.lazy 관련 기능과 같이 나온 기능으로, 매우 쉬운 문법으로 코드를 분할 로드하는 동안 placeholder로 보여줄 대체 component를 보여주게 한다.
import Spinner from './Spinner'; <Suspense fallback={<Spinner />}> <SomeComponent /> </Suspense>
spinner나 skeleton과 같은 UI를 사용할때 효과적으로 보인다.

III. 본론

앞서 살펴본 바로는 recoil은 여러 이점을 지닌 상태관리 시스템으로 보인다. 실제로 설치 적용하여 어떤방식으로 동작하는지 알아보도록 한다.

설치

NPM을 통해 설치
npm install recoil
yarn을 통한 설치
yarn add recoil

Bundler

Recoil은 Webpack 또는 Rollup과 같은 모듈 번들러와도 문제없이 호환된다.

ES5 support

Recoil 빌드는 ES5로 트랜스파일 되지 않으므로, Recoil을 ES5와 사용하는 것은 지원하지 않는다. ES6 기능을 natively하게 제공하지 않는 브라우저를 지원해야 하는 경우 Babel로 코드를 컴파일하고 preset @babel/preset-env을 이용하여 이를 수행할 수는 있지만 문제가 발생할 수도 있다. 특히, React와 같이, Recoil은 ES6의 Map과 Set 타입에 의존하는데, 이러한 ES6의 요소들을 polyfills를 통해 에뮬레이션하는 것은 성능상의 문제를 야기할 수 있다.

주요 개념 및 기능

atom

Atoms는 상태의 단위이며, 업데이트와 구독이 가능하다. atom이 업데이트 되면 atom을 구독하고 있던 모든 컴포넌트들의 state가 새로운 값을 반영하여 다시 렌더링 된다. atoms는 런타임에서 생성될 수도 있다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.

atom 함수의 정의

function atom<T>({ key: string, default: T | Promise<T> | RecoilValue<T>, effects_UNSTABLE?: $ReadOnlyArray<AtomEffect<T>>, dangerouslyAllowMutability?: boolean, }): RecoilState<T>

atoms 생성법

const fontSizeState = atom({ key: 'fontSizeState', default: 14, });
Atoms는 디버깅, 지속성 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다. 두개의 atom이 같은 키를 갖는 것은 오류이기 때문에 키값은 전역적으로 고유하도록 해야한다. React 컴포넌트의 상태처럼 기본값도 가진다. atom을 통해 상태를 공유하는 각각 작은단위의 component들

컴포넌트에서 atom 사용하는 방법 예제

생성된 atom은 component에서 useRecoliState라는 훅을 사용하여 읽고 쓸 수 있다. React의 useState와 비슷하지만 상태가 다른 컴포넌트와 공유가 될 수 있다는 점이 차이점이다.

FontButton component

function FontButton() { const [fontSize, setFontSize] = useRecoilState(fontSizeState); return ( <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}> Click to Enlarge </button> ); }

Text component

function Text() { const [fontSize, setFontSize] = useRecoilState(fontSizeState); return <p style={{fontSize}}>This text will increase in size too.</p>; }
FontButton 컴포넌트를 통해 버튼을 클릭하면 글꼴이 1씩 증가하며 fontSizeState라는 atom을 사용하는 Text component에 해당하는 글꼴도 같이 변화한다.

selector

Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다는 점은 동일하다. Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지한다. Selectors는 어떤 컴포넌트가 자신을 필요로하는지, 또 자신은 어떤 상태에 의존하는지를 추적하기 때문에 이러한 함수적인 접근방식을 매우 효율적으로 만든다. 컴포넌트의 관점에서 보면 selectors와 atoms는 동일한 인터페이스를 가지므로 서로 대체할 수 있다. Selectors는 selector함수를 사용해 정의한다. 또한 selector는 atom과 다르게 내부의 setter에서 atom의 값을 가져와 비동기로 호출을 진행하여 특정 값을 가져올 수 있다.

selection 함수의 정의

function selector<T>({ key: string, get: ({ get: GetRecoilValue }) => T | Promise<T> | RecoilValue<T>, set?: ( { get: GetRecoilValue, set: SetRecoilState, reset: ResetRecoilState, }, newValue: T | DefaultValue, ) => void, dangerouslyAllowMutability?: boolean, })

selection 생성법

const mySelector = selector({ key: 'MySelector', get: ({get}) => get(myAtom) * 100, });
selection 내부에서 다른 atom값을 받아와 값을 변경하여 return

비동기 selector

const myQuery = selector({ key: 'MyQuery', get: async ({get}) => { return await myAsyncQuery(get(queryParamState)); }, });

비동기 selector 중 suspense와 함께 동작

Selector가 promise 상태에서 resolve가 되기전까지 보류중인 데이터를 다루기 위해 React Suspense와 함께 동작하도록 디자인되어 있고, 컴포넌트를 Suspense의 경계로 감싸는 것으로 아직 보류중인 하위 항목들을 잡아내고 대체하기 위한 UI를 렌더링한다.
function MyApp() { return ( <RecoilRoot> <React.Suspense fallback={<div>Loading...</div>}> <CurrentUserInfo /> </React.Suspense> </RecoilRoot> ); }
selection 내부에서 다른 atom값을 받고 비동기 호출을 통하여 값을 변경하여 return

비동기 selector중 parameter를 사용하여 호출

atom값이 아니라 직접 parameter를 넣어서 selector값을 비동기로 받아와야 할 경우 사용한다. 아래 예제는 props로 userID를 받아오는 경우, 해당 atom에 넣지 않고 값을 바로 selector를 이용하여 호출하는 방법이다.

동기 방식으로 컴포넌트에서 selector를 사용하는 방법 예제

fontSizeLabelState
const fontSizeLabelState = selector({ key: 'fontSizeLabelState', get: ({get}) => { const fontSize = get(fontSizeState); const unit = 'px'; return ${fontSize}${unit}; }, });
get 속성은 계산될 함수다. 전달되는 get 인자를 통해 atoms와 다른 selectors에 접근할 수 있다. 다른 atoms나 selectors에 접근하면 자동으로 종속 관계가 생성되므로, 참조했던 다른 atoms나 selectors가 업데이트되면 이 함수도 다시 실행된다. 이 fontSizeLabelState 예시에서 selector는 fontSizeState라는 하나의 atom에 의존성을 갖는다. 개념적으로 fontSizeLabelState selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작한다. Selectors는 useRecoilValue()를 사용해 읽을 수 있다.
useRecoilValue()는 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다. fontSizeLabelState selector는 writable하지 않기 때문에 useRecoilState()를 이용하지 않는다. (writable한 selectors에 대한 더 많은 정보는 selector API reference에 자세히 기술되어 있다.)
FontButton
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> </> ); }
버튼을 클릭하면 버튼의 글꼴 크기가 증가하는 동시에 현재 글꼴 크기를 반영하도록 글꼴 크기 레이블을 업데이트하는 두 가지 작업이 수행된다. atom과 selector의 차이를 쉽게 말하자면, atom은 변수처럼 업데이트가 가능한 데이터이며, selector는 atom을 참조하여 값이 계산되어서 return되는 computed 속성에 가깝다.

useRecoilState

atom으로 만들어진 값을 구독하고있는 hook으로 useState처럼 사용된다. React component에서 사용하면 상태가 업데이트 되었을때 리렌더링을 하도 록 컴포넌트를 구독한다.

useRecoilState 함수 정의

function useRecoilState<T>(state: RecoilState<T>): [T, SetterOrUpdater<T>]; type SetterOrUpdater<T> = (T | (T => T)) => void;

useRecoilState를 활용하여 atom, selector을 사용하는 방법

tempFahrenheit값을 atom으로 설정하고 tempCelsius값을 selector을 이용해서 설정함. TempCelsius에서 useRecoilState를 통해 값을 받아와 렌더링 하고 있다.

useRecoilValue

atom으로 만들어진 값을 리턴해주는 hook으로 setter 함수가 따로 없다.

useRecoilValue 함수의 정의

function useRecoilValue<T>(state:RecoilValue<T>): T;

useRecoilValue를 활용하여 atom, selector을 사용하는 방법

위의 useRecoilState와 비슷한 예제이다. setter가 없기 때문에 단순히 값을 받아 사용한다. 정적인 component에서 상태를 읽을 수만 있게 할때 사용할만 하다.
import {atom, selector, useRecoilValue} from 'recoil'; const namesState = atom({ key: 'namesState', default: ['', 'Ella', 'Chris', '', 'Paul'], }); const filteredNamesState = selector({ key: 'filteredNamesState', get: ({get}) => get(namesState).filter((str) => str !== ''), }); function NameDisplay() { const names = useRecoilValue(namesState); const filteredNames = useRecoilValue(filteredNamesState); return ( <> Original names: {names.join(',')} <br /> Filtered names: {filteredNames.join(',')} </> ); }

useSetRecoilState

setter 함수를 리턴한다. component가 읽지 않고 쓰기만 하려고 할때 사용한다. useRecoilState와 다른 점이라면 useRecoilState는 atom이나 selector가 업데이트 되었을때 다시 렌더링을 하는대, useSetRecoilState를 사용하면 다시 렌더링 없이 setter 함수만 넘겨줄 수 있다는 점이다.

useSetRecoilState 함수의 정의

function useSetRecoilState<T>(state: RecoilState<T>):SetterOrUpdater<T>; type SetterOrUpdater<T> = (T | (T => T)) => void;

useSetRecoilState를 활용하여 setter함수를 props로 넘겨주는 예시

import {atom, useSetRecoilState} from 'recoil'; const namesState = atom({ key: 'namesState', default: ['Ella', 'Chris', 'Paul'], }); function FormContent({setNamesState}) { const [name, setName] = useState(''); return ( <> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={() => setNamesState(names => [...names, name])}>Add Name</button> </> )} // This component will be rendered once when mounting function Form() { const setNamesState = useSetRecoilState(namesState); return <FormContent setNamesState={setNamesState} />; }

useResetRecoilState

hook은 인자로 받아온 atom의 state를 default 값으로 reset 시키는 역할을 한다. 컴포넌트가 상태가 변경될 때 다시 렌더링을 하기위해 컴포넌트를 구독하지 않고도 상태를 기본값으로 리셋할 수 있게 한다.

useResetRecoilState 함수정의

function useResetRecoilState<T>(state: RecoilState<T>): () => void;

useResetRecoilState를 사용하는 예시

import {todoListState} from "../atoms/todoListState"; const TodoResetButton = () => { const resetList = useResetRecoilState(todoListState); return <button onClick={resetList}>Reset</button>; };

활용예제

화면내에 Image 목록을 불러와, 클릭하면 해당 정보를 표시하는 UI Add image 버튼을 클릭하면 다른 서버로부터 비동기 통신으로 특정 Image 파일에 대한 정보를 불러온 후 불러온다. image list에 해당하는 목록 갯수만큼 화면에 표시하고, 해당 image를 클릭하면 Meta정보가 표시되는 간단하지만 매우 활용적인 UI 예제이다.

완성화면 미리보기

프로젝트 생성 및 package 설치

CRA를 통해서 빠르게 recoil이라는 프로젝트를 생성한다.
create-react-app recoil
해당 프로젝트 폴더로 이동후 recoil 설치
yarn add recoil

Root 생성 index.js

root에 해당하는 index.js파일에 RecoilRoot를 import하고 App component를 한번 감싼다.
import React from "react"; import ReactDOM from "react-dom"; import { RecoilRoot } from "recoil"; import App from "./App"; const rootElement =document.getElementById("root") ReactDOM.render( <React.StrictMode> <RecoilRoot> <App /> </RecoilRoot> </React.StrictMode>, rootElement );

App.js

application 진입점에 해당하는 App component
import React, { Suspense } from "react"; import { useRecoilState } from "recoil"; import Metadata from "./Metadata"; import Images from "./Images"; import { imageListState } from "./store"; import "./App.css"; const App = () => { const [imageList, setImageList] = useRecoilState(imageListState); const counter = imageList.length + 1; const addImage = () => { setImageList([...imageList, counter]); }; return ( <> <div className="App"> <Suspense fallback="Loading..."> <Images /> <Metadata /> </Suspense> </div> <button onClick={addImage}>Add image</button> </> ); } export default App;

App.css

style 파일 추가한다. 해당 style파일은 한번 설정하면 더 건드릴 일은 없다.
.App { font-family: "Rhodium Libre", sans-serif; text-align: center; display: flex; height: 80vh; } .images { background: #defcff; width: 100%; display: flex; flex-wrap: wrap; justify-content: space-around; padding: 10px; } .image { margin: 5px; cursor: pointer; width: 33%; max-width: 150px; height: 150px; } .image img { width: 100%; border: 5px solid transparent; } img.selected { border-color: #0085ff; } .metadata { width: 230px; height: 100%; background: #c4c4c4; flex-grow: 0; } button { margin-top: 10px; width: 200px; height: 50px; border: 0px; cursor: pointer; } input { width: 145px; height: 20px; margin: 5px; border: 0px; }

App.js

recoil로부터 useRecoilState를 import한다. useRecoilState로 받아온 imageList는 해당 갯수가 6개 이하일때만 Image를 불러오는 버튼을 노출시키도록 사용된다. Image 목록들을 표시할 Image와 해당 Image의 상세 정보를 불러오는 영역인 Metadata component도 import 해온다. Suspense로 Images 와 Meta component를 감싸면 내부의 component가 불러오기 전까지 Suspense에서 fallback으로 설정한 "Loading..." 메세지가 표시된다.
import React, { Suspense } from "react"; import { useRecoilState } from "recoil"; import Metadata from "./Metadata"; import Images from "./Images"; import { imageListState } from "./store"; import "./App.css"; const App = () => { const [imageList, setImageList] = useRecoilState(imageListState); const counter = imageList.length + 1; const addImage = () => { setImageList([...imageList, counter]); }; return ( <> <div className="App"> <Suspense fallback="Loading..."> <Images /> <Metadata /> </Suspense> </div> {counter <= 6 && <button onClick={addImage}>Add image</button>} </> ); } export default App;

Store

selectedImageState라는 atom과 ImageListState라는 atom을 생성하여 다른 component에서 이 값을 참조 할수 있도록 만든다. ImageState라는 atom값은 atomFamily 기능을 이용하여 특정 id를 기준으로 getImage라는 비동기 함수를 실행시켜 값을 받아오도록 한다.
import { atom, atomFamily } from "recoil"; const getImage = async id => { return new Promise(resolve => { const url = https://res.cloudinary.com/dqsubx7oc/image/upload/w_149,h_104/g_auto/recoil-demo/${id}.png; let image = new Image(); image.onload = () => resolve({ id, name: Image ${id}, url, metadata: { width: ${image.width}px, height: ${image.height}px } }); image.src = url; }); }; export const imageState = atomFamily({ key: "imageState", default: async id => getImage(id) }); export const selectedImageState = atom({ key: "selectedImageState", default: 1 }); export const imageListState = atom({ key: "imageListState", default: [1, 2, 3] });

Image component

store로 부터 생성된 atom imageState, selectedImageState 값을 받아와 참조하도록 한다. useRecoilValue를 통해서 imageState라는 atomFamily에 생성시 props로 받은 id를 파라메터로 실행하여 값을 받아온다. useRecoilState(selectedImageState)에서 설정한 atom값과, setter기능을 하는 selectedImage, setSelectedImage 값을 생성한다. Image가 클릭 되었을때 setSelectedImage에 해당 id를 넘겨, selectedImagState atom에서 현재 선택된 id값을 셋하여, 다른 component에서도 해당 atom값을 참조하도록 한다.
import React from "react"; import { useRecoilValue, useRecoilState } from "recoil"; import { imageState, selectedImageState } from "./store"; const Image = ({ id }) => { const { name, url } = useRecoilValue(imageState(id)); const [selectedImage, setSelectedImage] = useRecoilState(selectedImageState); const onClick = () => { setSelectedImage(id); }; return ( <div className="image"> <div className="name">{name}</div> <img className={selectedImage === id ? "selected" : ""} src={url} alt={name} onClick={onClick} /> </div> ); }; export default Image;

Images component

useRecoilvalue로 부터 받아온 imageList 객체를 참조한다. 해당 imageList 갯수만큼 Image component를 생성하며, 각각의 Image들은 Suspense 기능을 이용하여 화면에 노출되기 전까지 "Loading..."메세지를 노출시키도록 한다.
import React, { Suspense } from "react"; import { useRecoilValue } from "recoil"; import { imageListState } from "./store"; import Image from "./Image"; const Images = () => { const imageList = useRecoilValue(imageListState); return ( <div className="images"> {imageList.map(id => ( <Suspense key={id} fallback="Loading..."> <Image id={id} /> </Suspense> ))} </div> ); }; export default Images;

Metadata component

useRecoilValue로부터 값을 받아와 현재 선택된 image의 id값을 받아온다. useRecoilState에서 imageState에 현재 선택된 image id값을 파라메터로 실행해 image에 대한 정보를 받아오고 이 값들을 파싱하여 화면에 보여준다.
import React from "react"; import { useRecoilValue, useRecoilState } from "recoil"; import { selectedImageState, imageState } from "./store"; const Metadata = () => { const selectedImageId = useRecoilValue(selectedImageState); const [image, setImage] = useRecoilState(imageState(selectedImageId)); const onChange = e => { const value = e.target.value; setImage({ ...image, name: value }); }; return ( <div className="metadata"> <div> <input type="text" value={image.name} onChange={onChange} /> </div> <img src={image.url} alt={image.name} /> <div> <b>Id:</b> {image.id} </div> <div> <b>Width:</b> {image.metadata.width} </div> <div> <b>Height:</b> {image.metadata.height} </div> </div> ); }; export default Metadata;

예제 동작화면

IV. 결론

현재 기준의 recoil은 공개된지 얼마 되지 않아서 아직은 미흡한 점이 있다. 그렇게까지 유명하지도 않고 그로인해 사용자가 많이 늘어나지도 않았다. 하지만 태생 자체가 react의 상태관리를 위해서 태어났으며, 다른 상태관리가 갖고 있는 문제점을 해결하기 위해 나온 만큼 그 활용가치가 크다. react는 유연하게 설계가 가능하지만, 이로인해 발생하는 코드의 복잡성은 언제나 개발자의 고통을 야기시킨다. recoil을 사용한다면 hook에 익숙한 유저라면 매우 쉽게 가볍게 적응이 가능할것이며, 비동기 selector를 활용하면 무분별하게 널려있는 호출 부분을 깔끔하게 통합하고 조금 더 간단하고 직관적인 코드로 변경 할 수 있을꺼라 기대가 된다. vue 에는 vuex가 있어서, vue 프로젝트에 vuex를 기본적으로 사용한다면, 앞으로 react 에는 recoil이 정석처럼 사용될 여지가 충분히 있다고 본다. 아울러 Facebook 그들이 꿈꾸는 메타버스 세상처럼, VR과 AR, 홀로그램등이 자연스럽게 받아들여질 때가되면, 그러한 기술들을 유연하게 표현할 수 있는 상태관리 라이브러리로 발전해 있을꺼라 믿어 의심치 않는다.

V. 후속 연구

recoil은 아직도 추가적인 개발이 진행중이고, 완성형에 가까워지려면 아직은 부족한 감이 있다. 그럼에도 불구하고 본문에는 다 소개하지 못한, 비동기를 컨트롤하게 하는 다른 유틸들과, 스냅샷기능, Jest unit testing 추가된 test 기능들도 있다. 우리의 애플리케이션이 점점 커지면 커질수록, 복잡성이 증가되며, 요청된 비동기 데이터를 섬세하게 다루고, 여러 경우의 수를 대입하여 테스트를 해야 하는 경우가 생겨난다. 따라서 이런 부분을 용이하게 다루기 위해서는 조금 더 본격적으로 깊게 해당 기능들을 알아봐야 할 필요성이 있다. 특히 redux-saga와 같이 state에 데이터를 할당하기 전에, 이 값을 핸들링 해주는 일종의 미들웨어로써 동작하는 무언가는 반드시 필요한 부분이다. 공식적으로 지원이 바로 되면 정말 좋겠지만, 그 전까지 이런 부분을 custom하게 만들어서 사용할 수만 있다면, 현재 개발중인 애플리케이션에도 적용이 가능하다고 생각되어 이어서 연구할 가치가 있다고 본다.
【3】 Recoil 작고 유연한 React 상태관리 라이브러리 : kt NexR 기술블로그 ‘21 테크리포트
Reference
본 기술블로그에 게재되는 모든 컨텐츠의 저작권은 케이티넥스알(kt NexR)에서 가지고 있으며, 동의 없는 컨텐츠 수정 및 무단 복제는 금하고 있습니다. 컨텐츠(글/사진/영상 등)를 공유하실 경우 반드시 출처를 밝혀주시기 바랍니다. Copyright(c) 2021 kt NexR, Inc. All Rights Reserved.