npx create-react-app@^5 recoil
chown -R jihun ./recoil
npm i recoil
import { RecoilRoot } from "recoil";
import TodoList from "./TodoList";
function App() {
return (
<RecoilRoot>
<TodoList />
</RecoilRoot>
);
}
export default App;
Atom
은 상태의 단위이고 일종의 공유되는 상태를 말한다.- 컴포넌트 어디에서든 atom 을 읽고 쓸 수 있고 atom 의 값을 읽는 컴포넌트들은 암묵적으로 atom 을 구독한다.
- atom 이 여러 컴포넌트에서 사용될 경우 모든 컴포넌트는 상태를 공유한다.
- atom 은 고유한 key 를 가져야 한다.
- atom 을 읽고 쓰려면
useRecoilState
훅을 사용한다.
- 이 훅은 useState 와 유사한데 상태가 컴포넌트간 공유된다는 차이가 있다.
- atom 을 쓰려면
useSetRecoilState
훅을 사용한다.- atom 을 읽으려면
useRecoilValue
훅을 사용한다.- 쓰기 가능한 RecoilState 객체를 반환한다.
// state.js
import { atom } from "recoil";
export const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
// FontButton.jsx
import { useRecoilState } from "recoil";
import { fontSizeState } from "./state";
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Click to Enlarge
</button>
);
}
useRecoilState()
: atom 을 읽고 쓰려고 할 때 사용, atom 에 컴포넌트를 등록useRecoilValue()
: atom 을 읽기만 할 때 사용, atom 에 컴포넌트를 등록useSetRecoilState()
: atom 에 쓰려고만 할 때 사용useResetRecoilState()
: atom 을 초깃값으로 초기화할 때 사용useRecoilCallback()
: 컴포넌트가 등록되지 않고 atom 의 값을 읽어야 할 때 사용
useRecoilState 예시코드
import {atom, selector, useRecoilState} from 'recoil';
const tempFahrenheit = atom({
key: 'tempFahrenheit',
default: 32,
});
const tempCelsius = selector({
key: 'tempCelsius',
get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({set}, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});
function TempCelsius() {
const [tempF, setTempF] = useRecoilState(tempFahrenheit);
const [tempC, setTempC] = useRecoilState(tempCelsius);
const addTenCelsius = () => setTempC(tempC + 10);
const addTenFahrenheit = () => setTempF(tempF + 10);
return (
<div>
Temp (Celsius): {tempC}
<br />
Temp (Fahrenheit): {tempF}
<br />
<button onClick={addTenCelsius}>Add 10 Celsius</button>
<br />
<button onClick={addTenFahrenheit}>Add 10 Fahrenheit</button>
</div>
);
}
useRecoilValue 예시코드
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 예시코드
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 예시코드
import {todoListState} from "../atoms/todoListState";
const TodoResetButton = () => {
const resetList = useResetRecoilState(todoListState);
return <button onClick={resetList}>Reset</button>;
};
- atom 이나 다른 selector 를 입력으로 받는 순수함수이다.
- 컴포넌트들은 selector 를 atom 처럼 구독할 수 있고 selector 가 변하면 컴포넌트들도 다시 렌더링된다.
- 상태를 기반으로한 파생 데이터를 계산하는데 쓰인다.
- 말 그대로 기본 상태를 atom 에서 만들고 이를 가공해서 필요한 데이터를 선택 select 해서 사용할 수 있다.
- get 함수는 계산될 함수로 get 인자를 통해 atom 과 다른 selector 에 접근할 수 있다.
- selector 는 파생된 상태의 일부를 나타내며 예를 들어 필터링된 todo 리스트, todo 리스트 통계등이 파생된 상태를 의미한다.
- 상위의 atoms 또는 selectors 가 업데이트되면 하위의 selector 함수도 다시 실행된다.
- get 함수만 제공되면 selector 는 읽기만 가능한
RecoilValueReadOnly
객체를 반환한다.set
함수도 제공되면 Selector 는 쓰기 가능한 RecoilState 객체를 반환한다.
import { selector } from "recoil";
import { fontSizeState } from "./state";
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
예를 들어 다음과 같은 상태가 있을때 이 상태에 대한 파생된 상태를 다음과 같이 atom 과 selector 로 나타낼 수 있다.
const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
});
///
const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({get}) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case 'Show Completed':
return list.filter((item) => item.isComplete);
case 'Show Uncompleted':
return list.filter((item) => !item.isComplete);
default:
return list;
}
},
});
const mySelector = selector({
key: 'MySelector',
get: ({get}) => get(myAtom) * 100,
});
const toggleState = atom({key: 'Toggle', default: false});
const mySelector = selector({
key: 'MySelector',
get: ({get}) => {
const toggle = get(toggleState);
if (toggle) {
return get(selectorA);
} else {
return get(selectorB);
}
},
});
const proxySelector = selector({
key: 'ProxySelector',
get: ({get}) => ({...get(myAtom), extraField: 'hi'}),
set: ({set}, newValue) => set(myAtom, newValue),
});
///
const transformSelector = selector({
key: 'TransformSelector',
get: ({get}) => get(myAtom) * 100,
set: ({set}, newValue) =>
set(myAtom, newValue instanceof DefaultValue ? newValue : newValue / 100),
});
비동기 평가함수를 갖고 Promise 를 출력값으로 반환가능하다.
const myQuery = selector({
key: 'MyQuery',
get: async ({get}) => {
return await myAsyncQuery(get(queryParamState));
},
});
import {selector, useRecoilValue} from 'recoil';
const myQuery = selector({
key: 'MyDBQuery',
get: async () => {
const response = await fetch(getMyRequestUrl());
return response.json();
},
});
function QueryResults() {
const queryResults = useRecoilValue(myQuery);
return <div>{queryResults.foo}</div>;
}
function ResultsSection() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<QueryResults />
</React.Suspense>
);
}
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
└── src
├── App.jsx
├── Header.jsx
├── TodoItem.jsx
├── TodoItemCreator.jsx
├── TodoList.jsx
├── TodoListFilters.jsx
├── TodoListStats.jsx
├── index.js
├── state.js
└── style.js
styled-component 는 모두 style.js 에서 관리하고 atom 과 selector 는 state 에서 모두 관리한다.
// state.js
import { atom, selector } from "recoil";
export const todoListState = atom({
key: "todoListState",
default: [],
});
export const todoListFilterState = atom({
key: "todoListFilterState",
default: "Show All",
});
export const filteredTodoListState = selector({
key: "filteredTodoListState",
get: ({ get }) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case "Show Completed":
return list.filter((item) => item.isComplete);
case "Show Uncompleted":
return list.filter((item) => !item.isComplete);
default:
return list;
}
},
});
export const todoListStatsState = selector({
key: "todoListStatsState",
get: ({ get }) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
const totalUncompletedNum = totalNum - totalCompletedNum;
const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
};
},
});
// style.js
import styled from "styled-components";
export const HeaderStyle = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-weight: 800;
background-color: ivory;
line-height: 3;
font-size: x-large;
`;
export const BtnStyle = styled.button`
margin: 5px;
border: none;
background: black;
color: white;
border-radius: 20px;
padding: 10px;
`;
export const ContainerStyle = styled.div`
display: flex;
`;
export const ListContainerStyle = styled(ContainerStyle)`
flex-direction: column;
height: 300px;
overflow-y: auto;
`;
export const InputStyle = styled.input`
width: 100%;
margin: 10px;
`;
export const CheckBoxStyle = styled.input`
width: 25px;
`;
export const ReverseContainerStyle = styled(ContainerStyle)`
justify-content: flex-end;
padding: 10px;
`;
export const StatsStyle = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
& span {
padding: 5px;
}
`;
한 파일 내에서 스타일 관련 코드가 너무 길어지는 것 같아 따로 분리하긴 했는데 프로젝트 규모가 커지면 이런 식의 관리는 힘들 것 같고 각 컴포넌트에서 쓰이는 styled-component 는 해당 파일에서 관리하되, atomic design 에 따라 style 을 분리했을 경우 atom 에 속하는 스타일들은 중복해서 적용될 가능성이 높으니 그런 경우만 ㄴstate.js 로 관리하면 좋을 것 같다는 생각이 들었다.
atom 과 selector 는 한곳에 모아서 관리하는 것이 좋은 것 같은데 이 역시 redux 처럼 규모가 커지면 features 로 나누어 atom 과 selector 를 관리하면 더 좋을 것 같았다. 아직은 작은 todo 앱이니 하나의 파일에서 한꺼번에 관리했다.
전체적으로 코드를 작성해봤을 때 상태 관리를 useState 와 props 로 하는 대신 atom 과 selector 로 마치 useContext 를 사용하듯이 각 컴포넌트에서 상태를 공유해서 사용한다는 느낌이 들었다. 그 외엔 모두 동일해서 redux 만큼의 러닝커브가 높게 느껴지지도 않았다.
만약 전역적으로 상태관리를 하고자 할때 급히 사용해야 하는 경우라면 recoil 도 좋은 선택지가 될 것 같다.
리액트 스터디는 공통적으로 진행하는 것은 이번 주차를 마지막으로 하기로 했다.
의욕이 앞서서 다같이 함께 공부하고 싶었는데 각자의 상황을 깊게 고려하지 못했던 것 같다. 꾸준히 같이 할 수 있으면 좋겠는데 좀 아쉬운 부분이 있던 것 같다.
스터디 진행을 이렇게 주도적으로 진행해본게 처음이라 부담을 낮추기 위해 각자 공부한 내용만 슬랙에 공유하자고 했는데 스터디 같은 느낌이 부족했다는 생각이 든다. 그래서 우아한 스터디나 다른 스터디에서 내가 평소에 관심이 있던 클린코드, 리펙토링, 타입스크립트, 함수형 코딩에 대한 북스터디가 보이면 이 분들은 어떤 방식으로 스터디를 진행했었는지 알아보곤 했다.
다음에 진행해보고 싶은 스터디는 리펙토링 2판 혹은 클린코드 / 자바 혹은 스프링 입문을 위한 객체지향의 원리와 이해 / 코어 자바스크립트 정도?
이런 북스터디를 진행하기에 앞서 먼저 책 한권을 읽어보고 위의 북스터디를 진행해보고 싶어 최근 쏙쏙 들어오는 함수형 코딩
책을 읽으면서 redux, recoil 등을 활용한 todo 앱을 만들어보는 과정에 함수형 프로그래밍을 한다는 생각으로 액션, 계산, 데이터에 대해 어떻게 계산을 뽑아낼 수 있을지 생각하며 코드를 작성해보고 있다.
원래 스터디를 아는 사람들끼리 진행해서 팀프로젝트에 공통의 이해를 바탕으로 새로운 지식을 활용해보고 싶었다. 그런데 부트캠프 특성인지 모르겠지만 다들 취준하느라 바쁜것도 있고 새로운 분야에 발을 내민다는 데에 거부감이 있는지 그런 개발문화에 대해 열린 마음으로 다가가기 어려워하는 것 같다. 이게 너무 아쉽다..
앞으로 3달정도 남았는데 그 기간동안 책 1~2권 많으면 3권정도 북스터디를 진행해보고 싶다. 아마 스터디는 취업을 해도 꾸준히 할 것 같다. 개발자는 언제나 새로운 기술을 익혀나가야 하니 시니어도 항상 공부하는 모습을 봤던 것 같다. 주니어인데도 익숙하고 편안하다면 뭔가 잘못된 것이라는 글을 읽었는데 개인적으로 마음에 찔렸었다... 기왕이면 우아한 스터디에 참여해서 누구나 학습에 열정을 갖고 각자의 의견을 활발히 공유해보고 싶다.