오늘은 기업협업의 첫 날.
대표님께서 리덕스보다 리코일을 더 추천한다는 얘길 듣고, 개념이 궁금해져서 찾아보았다.
정말 공부할게 많구나 새삼 느꼈고
공부하는 만큼 계속 성장할 수 있다는 것에 설렘도 느꼈다.
리코일을 보기 전, 리덕스에 관해서도 이해해야할 것 같아서 리덕스 관련 벨로그도 첨부해본다.
Recoil - 또 다른 React 상태 관리 라이브러리?
위 링크의 내용을 토대로 작성한 내용들이다.
Recoil
복잡한 UI를 대상으로 전역 상태관리를 위한 최적화된 방법을 찾으려고 했으나 성능 및 효율성이라는 장벽에 부딪히게 됨
그래서 Redux 나 Mobx의 잘못이다?
아니다.
기존의 상태관리 라이브러리들은 문제가 없다.
But.
But.
Redux는 강력한 기능을 제공하긴 하지만, 기본적인 store 구성을 위해 많은 보일러 플레이트와 장황한 코드를 작성해야 한다.
그리고 비동기 데이터 처리 또는 계산된 값 캐시와 같은 중요한 기능은 라이브러리 기능이 아니며,
이를 해결하기 위해서는 또 다른 라이브러리를 사용해야한다.
그리고 만약 selector가 동적인 prop을 받는 경우 이 값을 정확하게 기억하는 것은 어려운 일이다.
그럼 Context API를 사용하면 안돼?
React가 가진 상태 공유 솔루션인 Context API도 한계가 있다.
반복적이고 복잡한 업데이트에 사용할 경우 비효율적.
Sebastian Markbage의 말을 인용하면:
개인적으로 새로운 Context는 locale/theme와 같은 낮은 빈도의 업데이트에 사용 가능하다고 생각한다. 또한 이전에 Context를 사용했던 방법으로 사용해도 좋다.(즉 정적인 값, 구독을 통해 업데이트를 전파하는 것) 하지만 Context는 Flux와 같은 상태 관리 시스템을 대체할 수 없다.
Context API는 데이터 서브셋을 대상으로 변경을 감지하고 업데이트 할 수 없다.
Provider 하위의 모든 consumer들은 Provider 속성이 변경될 때마다 다시 렌더링된다. (https://reactjs.org/docs/context.html#before-you-use-context)
만약 Provider의 값이 배열이나 객체인 경우 구조가 조금이라도 변경된다면, 그 Context를 구독하고 있는 하위의 모든 것(컴포넌트가 그 값의 일부분만 사용할지라도) 이 다시 렌더링될 것이다.
그렇다면 각각의 이미지가 각각의 Context를 가지고 있다면?
정확한 이미지 수를 알고있다면 아무 문제가 되지 않는다.
But.
이미지를 동적으로 변경할 수 있고, 추가할 수 있다면?
새로운 이미지에 Context Provider를 추가하여 컴포넌트 트리를 다시 구성하고 전체 서브 트리를 다시 마운트 해야한다. 더 좋지 않은 방법이다.
-> 동적으로 Provider를 추가하려면 전체 하위 트리가 다시 마운트 됨.
그래서 Recoil은 뭐가 다른데?
일단.
API가 단순하고 이미 Hook을 사용하고 있는 사람들에게 익숙할 것이다.
Recoil을 시작하기 위해서는 어플리케이션을 RecoilRoot로 감싸고, 데이터를 atom이라는 단위로 선언하여 useState를 Recoil의 useRecoilState로 대체해야한다.
그리고. 컴포넌트가 사용하는 데이터 조각만 사용할 수 있고, 계산된 selector를 선언할 수 있으며, 비동기 데이터 흐름을 위한 내장 솔루션까지 제공한다.
동적 키로 atom을 만들고, selector에 인자를 보내는 등 모두 간단하게 할 수 있다.
그리고 곧 React 동시성 모드에 대한 지원도 될 것.
Recoil의 기본?
Atom : atom
은 하나의 상태라고 볼 수 있다. 컴포넌트가 구독할 수 있는 React state
라고 생각하면 된다. atom
의 값을 변경하면 그것을 구독하고 있는 컴포넌트들이 모두 다시 렌더링 된다. atom
을 생성하기 위해 어플리케이션에서 고유한 키 값과 디폴트 값을 설정해야 한다. 디폴트 값은 정적인 값, 함수 또는 심지어 비동기 함수가 될 수 있다.
export const nameState = atom({
key: 'nameState',
default: 'Jane Doe'
});
useRecoilState: atom
의 값을 구독하여 업데이트할 수 있는 hook. useState
와 동일한 방식으로 사용할 수 있다.
useRecoilValue: setter
함수 없이 atom
의 값을 반환만 한다.
useSetRecoilState: setter
함수만 반환한다.
import {nameState} from './someplace'
// useRecoilState
const NameInput = () => {
const [name, setName] = useRecoilState(nameState);
const onChange = (event) => {
setName(event.target.value);
};
return <>
<input type="text" value={name} onChange={onChange} />
<div>Name: {name}</div>
</>;
}
// useRecoilValue
const SomeOtherComponentWithName = () => {
const name = useRecoilValue(nameState);
return <div>{name}</div>;
}
// useSetRecoilState
const SomeOtherComponentThatSetName = () => {
const setName = useSetRecoilState(nameStae);
return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}
selector: selector
는 상태에서 파생된 데이터로, 다른 atom
에 의존하는 동적인 데이터를 만들 수 있게 해준다. Recoil의 selector
는 기존에 우리가 알던 selector
의 개념과는 조금 다르다. Redux의 reselector 와 MobX의 @computed 처럼 동작하는 "get"
함수를 가지고 있다. 하지만 하나 이상의 atom
을 업데이트 할 수 있는 "set"
함수를 옵션으로 받을 수 있다.
// 동물 목록 상태
const animalsState = atom({
key: 'animalsState',
default: [{
name: 'Rexy',
type: 'Dog',
}, {
name: 'Oscar',
type: 'Cat',
}],
});
// 필터링 동물 상태
const animalFilterState = atom({
key: 'animalFilterState',
default: 'dog',
});
// 파생된 동물 필터링 목록
const filteredAnimalsState = selector({
key: 'animalListState',
get: ({get}) => {
const filter = get(animalFilterState);
const animals = get(animalsState);
return animals.filter(animal => animal.type === filter);
}
});
// 필터링된 동물 목록을 사용하는 컴포넌트
const Animals = () => {
const animals = useRecoilValue(filteredAnimalsState);
return animals.map(animal => (<div>{ animal.name }, { animal.type }</div>));
}
어플리케이션의 요구사항에 맞게 Recoil로 만들어보자.
1번 2번 요구사항을 위해 각각의 이미지를 자체 atom
에 저장하려고 한다. 이를 위해 atomFamily
를 사용할 수 있다. atomFamily
는 atom
과 동일하지만, 다른 인스턴스와 구분이 가능한 매개변수를 받을 수 있다. 아래의 두 코드는 같은 의미이다.
// atom
const itemWithId = memoize(id => atom({
key: `item-${id}`,
default: ...
}))
// atomFamily
const itemWithId = atomFamily({
key: 'item',
default: ...
});
atom
과 유일한 차이점은 atomFamily
는 내부적으로 memoization
을 할 것이기 때문에, 각 인스턴스마다 고유한 키를 만들 필요가 없다는 것이다.
atom
과 atomFamily
는 default
값을 만들기 위해 다른 함수를 호출할 수도 있다. 여기서도 마찬가지로, atomFamily
에서는 내부적으로 생성한 고유한 id를 넘겨줄 수 있다는 차이가 있다.
export const imageState = atomFamily({
key: "imageState",
default: id => getImage(id)
});
컴포넌트가 imageState
를 최초로 호출할 때, default
값을 만드는 함수 (getImage(id))
가 호출될 것이다.
이 함수는 비동기 함수 일 수 있으며, Recoil이 React Suspense
의 도움을 받아 처리할 것이다.
store의 코드:
const getImage = async id => {
return new Promise(resolve => {
const url = `http://someplace.com/${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)
});
컴포넌트의 코드:
// 이미지 목록
const Images = () => {
const imageList = useRecoilValue(imageListState);
return (
<div className="images">
{imageList.map(id => (
<Suspense key={id} fallback="Loading...">
<Image id={id} />
</Suspense>
))}
</div>
);
};
// 단일 이미지
const Image = ({ id }) => {
const { name, url } = useRecoilValue(imageState(id));
return (
<div className="image">
<div className="name">{name}</div>
<img src={url} alt={name} />
</div>
);
};
뭐? Selector가 데이터를 SET 할 수 있다고?
위에서 selector
에 대해 이야기 할 때 setter
함수를 selector
에 전달할 수 있다고 언급하였다. 이상해보이지만, 단지 네이밍 때문에 혼란스러운 것이다 (그리고 바뀌길 바란다). selector
를 하나의 상태이지만 파생된 것으로 생각해보자. selector
는 atom
로부터 계산된 값을 얻을 수 있고, 또한 복수의 atom
에게 영향을 줄 수도 있다.
아래 예시에서 selector
는 파생된 상태(특정 색상 상자의 카운터 객체)를 반환한다. 이 setter
함수는 box atomFamily
로 생성된 모든 box
들에 영향을 줄 수 있으며 값을 재설정할 수 있다.
const colorCounterState = selector({
key: "colorCounterState",
get: ({ get }) => {
let counter = { [COLORS.RED]: 0, [COLORS.BLUE]: 0, [COLORS.WHITE]: 0 };
for (let i = 0; i < BOX_NUM; i++) {
const box = get(boxState(i));
counter[box] = counter[box] + 1;
}
return counter;
},
set: ({ set }) => {
for (let i = 0; i < BOX_NUM; i++) {
set(boxState(i), COLORS.WHITE);
}
}
});