이 글은 리코일을 공부하는 입장에서 공식문서에 시작하기를 번역하면서 이해한 글입니다.
CRA(npx create-react-app)로 리액트 파일을 생성한 후 아래 cli로 리코일을 설치한다.
npm i recoil // or yarn add recoil
리코일 state를 사용하는 컴포넌트들은 <RecoilRoot>
를 필요로 한다. <RecoilRoot>
를 사용하는 가장 좋은곳은 root component이다.
// app.js
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil'; // import!!! import하는것은 important하닌까 ~
function App() {
return (
<RecoilRoot> // 이렇게 감싸주어야한다.
<CharacterCounter />
</RecoilRoot>
);
}
아톰은 상태를 말하며 어떠한 컴포넌트에서 씌여지고 읽혀질 수 있다. 아톰의 밸류를 읽는 컴포넌트들은 암묵적으로 그 아톰에게 참고(subscribe(구독이라고 해도 될거 같다.) )되어지고 있다. 그래서 아톰의 업데이트는 이 아톰을 참고하고 있는 컴포넌트를 리-렌더 시킨다.
const textState = atom({
key: 'textState', // unique ID (다른 atoms/selectors을 구별하기 위해서)
default: '', // default value (aka initial value)
});
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
셀렉터는 하나의 추출된 state다. '추출된(derived 또는 파생된)'이라는 의미는 state가 변경(transformation)된것을 의미한다. 다시 말해서 추출된 상태란, 주어진 상태를 변경(modify)시키는 순수 함수에 의해서 변경된 스테이트(the output)라고 말할 수 있다.(실제 상태를 변경시키지는 않는거 같다.)
(A selector represents a piece of derived state. Derived state is a transformation of state. You can think of derived state as the output of passing state to a pure function that modifies the given state in some way)
// usage
const charCountState = selector({
key: 'charCountState', // unique ID (with respect to other atoms/selectors)
get: ({get}) => {
const text = get(textState);
return text.length;
},
});
function CharacterCount() {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
}
(아래 헷갈리는것들을 정리했다. 공부한것을 바탕으로 아래와 같이 생각한다. 틀렸다면 죄송.....)
: useState와 같은 역할이라고 봐도 될거 같다. 그러나 다른 파일에 있는 아톰을 읽을 수 있다는 점에서 useSelector와 비슷한거 같기도 하다.
다시 말해서, 아톰의 상태를 설정할 수 있고, 변경 시킬 수 도 있다.
: 아톰을 조회할 때만 사용한다.
: setter 역할을 한다. useSetRecoilState를 사용하여 아톰을 변경 시킬 수 있다.
: 아톰 값을 디폴트 값으로 변경시켜준다.
useSetRecoilState와 useRecoilState 둘다 사용해서 setter함수를 구현 시킬 수 있다. 그럼 둘의 차이점과 언제 무엇을 써야하나라는 의문이 든다.
아직 정확하게는 모르겠지만, 공식문서를 참고해보면
We use useRecoilState() to read todoListState and to get a setter function that we use to update the item text, mark it as completed, and delete it:
useRecoilState는 아톰(todoListState)을 읽고, setter함수를 사용하기위해서(get a setter function)라고 적혀 있다.
추측건대, 둘중 무엇을 쓰든 상관은 없다. useSetRecoilState를 사용하면 읽기도 가능하고 상태 변경도 가능하다는것 같다.
위의 정보를 바탕으로 간단한 카운터 예제를 만들어봤다.
// app.js
import './App.css';
import { RecoilRoot } from 'recoil';
import Counter from './Counter';
function App() {
return (
<RecoilRoot>
<Counter />
</RecoilRoot>
);
}
export default App;
// atom.js
import { atom } from 'recoil';
let countState = atom({
key: 'counter',
default: 0,
});
export default countState;
// Counter.js
import React from 'react';
import countState from './Atoms';
import { useRecoilState,
useRecoilValue,
useSetRecoilState,
useResetRecoilState
} from 'recoil';
function Counter() {
const [counter, setCounter] = useRecoilState(countState);
// useState와 같지만, useRecoilState을 사용하여 다른 파일에 있는 아톰을 읽을 수 있다.
const currentCount = useRecoilValue(countState); // 읽기 전용!
const counterHandler = useSetRecoilState(countState); // 값만 변경 시키기
const resetCounter = useResetRecoilState(countState); // 디폴트값으로 값 변경
const plusCount = () => {
counterHandler((pre) => pre + 1);
};
const minusCount = () => {
counterHandler((pre) => pre - 1);
};
return (
<div>
<div>
{/* <div>{counter}</div> */} // counter 또는 currentCount 둘 중 하나를 사용해도 상관없는거 같다.
<div>{currentCount}</div> // 그러나 읽기만 하려고 currentCount를 사용했다.
{/* <button onClick={() => setCounter((num) => num + 1)}>+</button>
<button onClick={() => setCounter((num) => num - 1)}>-</button> */}
// 위의 코드도 작동한다.
<button onClick={plusCount}>+</button>
<button onClick={minusCount}>-</button>
<button onClick={resetCounter}>reset</button>
</div>
);
}
export default Counter;
이번에는 셀렉터까지 추가해보겠습니다.
우선 atom.js
를 아래와 같이 바꾸었습니다.
// atom.js
import { atom } from 'recoil';
let countState = atom({
key: 'counter', // unique ID (with respect to other atoms/selectors)
default: 0, // default value (aka initial value)
});
let inputState = atom({ // 기존에서 추가된 아톰.
key: 'input',
default: 0,
});
export { countState, inputState };
그리고 새로운 파일을 selector.js
에 아래의 코드를 추가해보겠습니다.
// selector.js
import { selector } from 'recoil';
import { countState, inputState } from './Atoms';
const countStateSelector = selector({
key: 'CountState',
get: ({ get }) => {
const inputVal = get(inputState);
const count = get(countState);
return `추가된 카운트는 ${inputVal}이고, 현재 카운트는 ${count}입니다.`;
},
});
export default countStateSelector;
마지막으로 Counter.js
를 아래와 같이 변경하였습니다.
// Counter.js
import React from 'react';
import countState from './Atoms';
import { countState, inputState } from './Atoms'; // 새로 변경된 코드
import countStateSelector from './selector'; // 새로 추가된 코드
import {
useRecoilState,
useRecoilValue,
useSetRecoilState,
useResetRecoilState
} from 'recoil';
function Counter() {
const [counter, setCounter] = useRecoilState(countState);
// useState와 같지만, useRecoilState을 사용하여 다른 파일에 있는 아톰을 읽을 수 있다.
const currentCount = useRecoilValue(countState); // 읽기 전용!
const counterHandler = useSetRecoilState(countState); // 값만 변경 시키기
const resetCounter = useResetRecoilState(countState); // 디폴트값으로 값 변경
// 새로 추가된 코드
const currentInput = useRecoilValue(inputState);
const inputHandlerState = useSetRecoilState(inputState);
const resultValue = useRecoilValue(countStateSelector);
const plusCount = () => {
counterHandler((pre) => pre + 1);
};
const minusCount = () => {
counterHandler((pre) => pre - 1);
};
// 새로 추가된 코드
const inputHandler = (e) => {
let target = e.target.value;
inputHandlerState(target);
};
const submitCount = () => counterHandler((pre) => pre + Number(currentInput));
return (
<div>
<div>
{/* <div>{counter}</div> */} // counter 또는 currentCount 둘 중 하나를 사용해도 상관없는거 같다.
<div>{currentCount}</div> // 그러나 읽기만 하려고 currentCount를 사용했다.
{/* <button onClick={() => setCounter((num) => num + 1)}>+</button>
<button onClick={() => setCounter((num) => num - 1)}>-</button> */}
// 위의 코드도 작동한다.
<button onClick={plusCount}>+</button>
<button onClick={minusCount}>-</button>
<button onClick={resetCounter}>reset</button>
// 새로 추가된 코드
<div>
<input type='text' onChange={inputHandler}></input>
<button onClick={submitCount}>입력값 더하기</button>
<div>{resultValue}</div>
</div>
</div>
);
}
export default Counter;
상태관리 라이브러리는 리덕스만 사용해봤었다. 몇개 안되는 state때문에 스토어, 액션, 리듀서들을 만들어야 했지만, 리코일은 조금 더 가볍게 상태를 관리할 수 있었다. 지금은 당장 기본만 해본것이라서 정확한 판단은 안서지만, 상태관리 라이브러리로 괜찮다는 생각이 든다.