React에서는 하나의 값을 여러 컴포넌트에서 공유하며 사용할 수 있다. 이때 등장하는 개념이 props와 state가 있다. props는 단순히 전달이다. 값을 변경하거나 수정할 수 없다. 반면에 state는 해당값을 변경할 수 있는 특징을 가진다. 일반적으로 useState를 통해서 생성된 {state, setState}를 가지고 하위컴포넌트에서 하나의 state를 유동적으로 setState하여 값을 제어한다.
그런데 이 과정에서 만약 상위컴포넌트와 하위컴포넌트의 간격이 멀다면, 이를 위해서 props Drilling이 발생되고, 또한 하위컴포넌트에서 변경시킨 state가 상위 컴포넌트까지 state Lifting 되어야 한다는 점에서 불편함이 야기된다.
이를 해결하기 위해서 리액트 팀은 Context API를 제언한다. 이를 통해서 무리한 propsDrilling과 state Lifting을 해결한다. 쉽게 설명하면 일일이 {state, setState}를 상위에서 하위에 해당되는 모든 컴포넌트에 props를 내려줘야 했다면, 값이 선언될 상위 컴포넌트에서는 선언하고 Provider로 하위 컴포넌트를 감싸고, 이를 사용할 컴포넌트에서만 useConText를 통해서 상태를 꺼내서 사용하면 되기 때문이다.
이를 위해서는 아래와 같은 코드를 통해서 구현된다. 상위컴포넌트 위치에서 외부에 ContextAPI(createContext)를 생성하고 변수에 담는다. 이후 하위 컴포넌트와 공유해야 하는 상태를 선언하고 이를 Provider로 감싸고, 그 안에 value에 해당요소를 담아주면 된다.
import React, { createContext, useState } from 'react';
import ChildComponent from './ChildComponent';
// 컨텍스트 생성
export const MyContext = createContext();
// 전역으로 관리할 데이터 설정
const globalData = {
count: 0,
};
export default function ContextAPI() {
// 전역 데이터를 관리할 상태 생성
const [data, setData] = useState(globalData);
return (
<MyContext.Provider value={{ data, setData }}>
<hr/>
<p>ContextAPI에서의 상태관리</p>
<ChildComponent/>
</MyContext.Provider>
);
}
반면에 전역에 선언된 상태를 사용할 하위컴포넌트에서넌 어떻게 기록하면 되는가 아래와 같다. useContext를 사용할 수도 있고 MyContext.Consumer를 사용할 수도 있다. 그러나 useContext가 코드 가독성적인 측면에서는 더 효율적인 측면이 있기 때문에 방법1의 작성법이 더 선호된다.
// 방법1
import React, { useContext } from "react";
import { MyContext } from "./ContextAPI";
export default function ChildComponent() {
const {data, setData} = useContext(MyContext)
return (
<div>
<button
onClick={() => setData((prevData) => ({ count: prevData.count + 1 }))}
>
+더하기
</button>
<div>MyContext + 더하기 : {data.count}</div>
</div>
);
}
// 방법2
import React from "react";
import { MyContext } from "./ContextAPI";
export default function ChildComponent() {
return (
<MyContext.Consumer>
{({ data, setData }) => (
<div>
<button onClick={() => setData(prevData => ({ count: prevData.count + 1 }))}>
+더하기
</button>
<div>MyContext + 더하기 : {data.count}</div>
</div>
)}
</MyContext.Consumer>
);
}
그러나 Context API에도 한계는 있다. 이것이 리코일이 만들어진 동기이기도 하다. 공식문서는 이렇게 언급한다. "Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값의 집합을 담을 수는 없다."
코드의 유지보수적 측면을 생각한다면 하나의 Context API는 하나의 값이 배정되어야 한다. 위에서와 같이 MyContext는 MyContext에 대한 값을, 만약 UserContext를 만든다면 UserContext에 대한 값이 기록되어야 할 것이다. 그렇다면 아래와 같이 다수의 Provider가 생성되야 한다는 것을 의미하는 것이다.
import React, { createContext } from 'react';
export const MyContext = createContext();
export const UserContext = createContext();
export const AuthContext = createContext();
export default function App() {
const [data1, setData1] = useState('');
const [data2, setData2] = useState('');
const [data3, setData3] = useState('');
return (
<MyContext.Provider value={{ data1, setData1 }}>
<UserContext.Provider value={{ data2, setData2 }}>
<AuthContext.Provider value={{ data3, setData3 }}>
<UserProfile />
</AuthContext.Provider>
</UserContext.Provider>
</MyContext.Provider>
);
}
이러한 Context API를 중첩으로 쌓아야 하는 불편함이 야기되는 코드작성에 해당된다. 이러한 문제를 인지한 페이스북팀은 Recoil을 개발하여 시장에 소개하였다.
- 출처 : npmTrand
ChpaGPT의 보고에 따르면 Recoil은 2020년에 페이스북에서 발표된 비동기적인 상태관리 라이브러리로 리액트 상태관리 라이브러리 중에서 가장 최근에 등장했지만, 인기와 성장세가 매우 빠르게 나타내고 있는 라이브러리이다. 나아가 페이스북에서 개발하고 유지보수하고 있기 때문에 안정성과 신뢰성도 높다는 것이 특징이다. Recoil은 기존의 상태관리 라이브러리와는 다른 방식의 상태관리를 제공하고 있습니다. 이러한 새로운 방식은 많은 개발자들로부터 호평을 받고 있으며, 특히 비동기 상태관리와 성능 개선에 매우 용이하다고 한다.
- 출처 : recoil
리코일 설치 : yarn add recoil
atom은 Recoil의 가장 작은 단위로, 업데이트와 구독이 가능한 요소이다. 이를 구독한 컴포넌트에서 해당 요소의 값을 변경하면, 새로운 값을 렌더링해낸다.
코드로 살펴보자. atom은 아래와 같이 두개의 객체를 가진다. 하나는 key이고, 하나는 초기값이다. 마치 react-query에서 querykey가 고유하듯이 atom에서도 이 key는 고유해야 한다.
import { atom } from "recoil";
export const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
이렇게 선언되 상태를 다른 컴포넌트에서 사용해보자. 먼저 늘 그렇듯 Provider를 상위에 선언해주어야 한다.
import React from 'react'
import { RecoilRoot } from 'recoil'
import FontButton from './components/recoilexample/FontBtton'
function App() {
return (
<RecoilRoot>
<FontButton/>
</RecoilRoot>
)
}
이제 사용한 FontButton 컴포넌트를 살펴보자.
import React from 'react'
import { useRecoilState, useRecoilValue } from 'recoil';
import { fontSizeState } from './CounterStore';
export default function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeup = useRecoilValue(fontSizeState)
return (
<>
<button
onClick={() => setFontSize((size) => size + 1)}
style={{ fontSize }}>
Click to Enlarge - font-Size : {fontSize}
</button>
<div>Atom fontSizeState: {fontSizeup}</div>
</>
);
}
useRecoilState는 useState와 같이 상태와 상태를 변경하는 함수가 호출되는 메서드이다. 반면에 useRecoilValue는 상태만을 호출하는 메서드이다. 컴포넌트를보면, button onClick를 통해서 상태의 값을 높이는 것을 볼 수 있다. 그러면 본래 fontSizeState의 default값으로 선언된 14는 어떻게 될까? 바로 아래 div 태그에서 이를 확인 할 수 있도록 fontSizeup를 선언해주었다. 이렇게 함으로 실행된 결과과 위의 이미지이다. 그리고 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.상하위 컴포넌트의 관계없이 말이다.
Recoil의 가장 작은 단위인 뿌리(atoms)로부터 순수함수(selectors)를 거쳐 컴포넌트로 상태의 값을 전달한다. 이러한 방향을 Recoil에서는 data-flow graph라고 부른다. 이러한 과정을 토앻서 atoms의 상태는 동기 또는 비동기 방식을 통해 변환한다.
export const fontSizeState = atom({
key: 'fontSizeState',
default: 12,
});
export const fontSizevalueSquare = selector({
key: 'fontSizevalueSquare',
get: ({get}) => {
const fontSize = get(fontSizeState);
return fontSize*fontSize
}
})
위에서 선언된 atom을 아래와 같이 함수(동기, 비동기)로 상태를 값을 변경할 수 있다.
import React from 'react'
import { useRecoilState, useRecoilValue } from 'recoil';
import { fontSizeState, fontSizevalueSquare } from './CounterStore';
export default function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeup = useRecoilValue(fontSizeState)
const fontSizeSquare = useRecoilValue(fontSizevalueSquare)
return (
<>
<button
onClick={() => setFontSize((size) => size + 1)}
style={{ fontSize }}>
Click to Enlarge - font-Size : {fontSize}
</button>
<div>Atom fontSizeState: {fontSizeup}</div>
<div>Atom fontSizevalueSquare: {fontSizeSquare}</div>
</>
);
}
그리고 사용하고자 하는 컴포넌트에서는 useRecoilValue 값만을 가져와서 화면에 그려주었다. 그 결과가 위에 능장하는 이미지이다. atom이 상태의 값을 위와 같이 다루되, 글로벌 차원에서 모든 컴포넌트에 상태를 공유한다면, selector는 그 상태에 함수처리를 더하여 즉시적으로 전역에서 사용할 수 있도록 상태를 다루는 막강한 도구이다. 이런 도구는... 너무 좋다. 확실히 리덕스에 배해서 코드가 간결하고 깔끔하다.