August 29, 2022
Recoil이라는 상태관리 라이브러리에 대해 알기 전에 React에는 어떠한 한계가 있고 Recoil은 어떤 역할을 하는지 알아보았다.
React에는 아래와 같은 한계가 있다.
=> https://recoiljs.org/ko/docs/introduction/motivation 에 위와 같이 Recoil에 대한 설명이 나와있는데, 위 내용만 보면 잘 알기가 어렵다. Recoil에 대해 더 알아보기 전에 React에서 데이터의 흐름에 대해 정리해보았다.
React에서 데이터는 단방향으로 흐른다. 위에서 아래로 (부모에서 자식 컴포넌트로) 흐른다. 이런 방식은 Flux패턴에 의해 적용된 방식이다.
flux패턴은 MVC패턴이 가진 문제점에서 시작이 되었다. MVC패턴
은 Model에 데이터를 정의해 놓고, Controller를 통해서 Model의 데이터를 CRUD작업하며, 변경된 데이터를 View에 출력하는 식의 패턴이다.
web애플리케이션이 커지면서 정의된 Model과 이를 출력하는 View가 다양해졌다. 아래 이미지에서 보는 것처럼 데이터의 흐름이 정말 많다.
어떤 데이터가 변경되면 해당 데이터를 사용하는 모든 곳에서 코드를 작성하고 변경해줘야 한다.
Flux 패턴에서 Action
은 데이터의 상태를 변경하는 명령어이다. Dispatcher
는 Action을 감지하여 Store에 Action을 전달해주는 역할을 한다.Model
은 Store라고 볼 수 있는데 state가 저장되어있는 공간이다.
Dispatcher를 통해 가져온 Action을 확인해 내부에 저장된 데이터를 변경한다. View
는 React를 통해서 만드는 코드들이다. Model에 저장된 데이터를 가져와서 View에 뿌려주고, View는 해당 데이터들을 가지고 와서 화면에 렌더링한다.
Redux는 이 Flux패턴을 적용한 상태관리 라이브러리다. 이번에 우리 개발팀은 새로운 프로젝트에 Redux대신 Recoil을 쓰기로 결정했는데 크게 다음과 같은 이유가 있었다.
Redux를 쓰면 엡이 전체적으로 무거워지고 코드가 복잡하다. 이왕이면 Redux를 지양하고 러닝커브가 낮고 state를 관리하는 방법도 상대적으로 간단한 Recoil을 사용해보자
Recoil
을 사용하면 atoms(공유상태)에서 selectors(순수 함수)를 거쳐 React컴포넌트로 내려가는 data-flow graph를 만들 수 있다. Atoms
는 컴포넌트가 구독할 수 있는 상태의 단위다.
Selectors
는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환한다.
Atoms는 상태의 단위이며 업데이트와 구독이 가능하다. atom
이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영하여 다시 렌더링된다.
atom에는 사용할 상태(state)를 담는다. 전역적으로 사용하기 원하는 state를 atom에 띄어 어디서든지 사용할 수 있게 하는 것이다.
Atoms
는 atom함수를 사용해 생성한다.
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
})
Atoms
는 디버깅, 지속성 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다. 두 개의 atom이 같은 키를 갖는 것은 오류이기에 키 값은 전역적으로 고유하도록 해야 한다.
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return (
<button
onClick={() => setFontSize((size) => size + 1)}
style={{ fontSize }}
>
click to Enlarge{' '}
</button>
)
}
컴포넌트에서 atom을 읽고 쓰려면 useRecoilState
라는 훅을 사용한다. React의 useState와 비슷하지만 상태가 컴포넌트 간에 공유될 수 있다는 차이가 있다.
아래 예시) 버튼을 클릭하면 버튼의 글꼴 크기가 1만큼 증가한다.
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return <p style={{ fontSize }}>This text will increase in size too. </p>
}
Selector
는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수다. 상위의 atoms 또는 selectors가 업데이트되면 하위의 selector함수도 다시 실행된다.
컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
즉, atom을 원하는대로 변형해 값을 리턴받는다.
Selectors
는 상태를 기반으로 하는 파생 데이터를 계산하는데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지한다.
Selectors
는 selector함수를 사용해 정의한다.
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()
를 이용하지 않는다.
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>
</>
)
}
변경된 값을 내부 state에 담아서 리스트형 atom에 넣는 로직이다.
function RegisterModal() {
const [formData, setFormData] = useState()
const [list, setList] = useRecoilState(productState)
const product = useRecoilValue(productState)
const resetProduct = useResetRecoilState(productState)
//쓰기전용
const setList = useSetRecoilState(productsState)
const setIsOpen = useSetRecoilState(modalState)
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
const handleSubmit = async (e) => {
e.preventDefault()
setList((prev) => [...prev, formData])
setIsOpen(false)
}
}
컴포넌트가 사라질 때 값을 초기화 해줘야 한다. recoil은 전역 상태기에 특정 값으로 변경되지 않는 이상 이전 값을 유지하게 된다.
recoil에서는 reset함수를 추가로 제공해준다. useEffect를 통해 컴포넌트가 언마운트 되는 시점에 값을 초기화 해주지 않으면 기존 데이터가 그대로 불러와지는 오류가 발생한다.
//상태 초기화
useEffect(() => {
return () => {
resetProdcut()
}
}, [])
export const productState = atom({
key: 'productState',
default: {
idx: 0,
name: '',
category: '',
brand: '',
price: 0,
desc: '',
},
})
function Product({data}){
const list =useRecoilValue(productState);
const setProduct = useSetRecoilState(productState);
const handleDetail =(idx)=>{
setProduct(list.filter((row)=>row.idx ===idx[0]);
setRegisterOpen(true);)
}
return (
data && (
<article
className='product'
onClick={()=>handleDetail(data.idx)}>
</article>
)
)
}
버튼를 클릭하면 버튼의 글꼴 크기가 증가하는 동시에 현재 글꼴 크기를 반영하도록 글꼴 크기 레이블을 업데이트하는 두 가지 작업이 수행된다.
참고 : https://recoiljs.org/ko/docs/introduction/core-concepts https://www.youtube.com/watch?v=0-UaleJZOw8 https://tech.osci.kr/2022/06/16/recoil-state-management-of-react/