Recoil 실전에서 사용해보기

Recoil 예제를 작성해보자

이 포스트는 이 전 포스트의 후속 포스트입니다.
 

Recoil 을 실전에서 사용해보기

이 전 포스트에서 Recoil 의 간단한 개념을 알아보았다. 실전에서는 어떤식으로 사용해야 할 지 직접 예제를 작성해보려고 한다. 가장 먼저 react app 을 생성하고 Recoil 을 설치해야 한다. 나는 간단하게 생성 가능한 CRA 를 이용하려고 한다.
$ npx create-react-app my-recoil-example --template typescript
나의 경우에는 typescript 를 이용하여 개발하기에 typescript 템플릿으로 설치하였다.
프로젝트폴더 내부에서 Recoil 을 설치해준다.
$ yarn add recoil
이 작업들이 귀찮다면 미리 만들어둔 템플릿을 클론받자.
 

작업 내용

이번 예제는 간단한 투두리스트를 만드는것이다. 사실 이런 새로운 것을 배울때 가장 좋은 예제가 아닐까 싶다.
  • Todo 아이템 추가
  • Todo 아이템 삭제
  • Todo 아이템 필터링
순서로 진행하려고 한다.

<RecoilRoot />

Recoil 을 이용하여 상태관리를 하기 위해서는 <RecoilRoot /> 컴포넌트로 상위컴포넌트를 감싸야 한다.
이 프로젝트에서는 App.tsx 파일에서 진행한다.
// App.tsx import React from 'react'; import { RecoilRoot } from 'recoil'; function App() { return ( <RecoilRoot> <div className="App"> </div> </RecoilRoot> ); } export default App;
가장 먼저 <RecoilRoot /> 를 이용하여 프로젝트 전체를 래핑한 모습이다.
 

폴더구조 만들기

Recoil 을 처음 사용하기에 어떤 구조로 만들어야 Best practice 인지 알 수 없는 상태에서 사전에 몇가지 조사를 해보았다. 참고 링크
  1. atoms 에는 State 를 붙여서 코드를 작성하는것과 selectors 에는 Value 를 붙여 사용하는것을 지양한다. 그 대신에 좀 더 명확한 Atomwith<Something> 을 붙여 사용하도록 하자.
  1. atomsselectors 를 분리해서 작성하지 않고 큰 주제의 한개의 폴더에서 함께 작성한다.
 
이 규칙들을 적용하여 폴더구조를 먼저 만들어보자.
├── recoil │   └── todoList │   ├── atom.ts │   ├── types.ts │   └── index.ts
이렇게 초기 세팅을 해주었다.

Atoms 생성하기

가장 먼저 Todo 아이템에 대한 인터페이스를 정의했다.
// src/recoil/todoList/types.ts export interface TodoItem { id: number; text: string; isComplete: boolean; }
 
이 후, Todo 목록에 대한 atom 을 생성한다.
// src/recoil/todoList/atom.ts import { atom } from 'recoil'; import { TodoItem } from "./types"; const todoListState = atom<TodoItem[]>({ key: 'todoListState', default: [], }); export default todoListState;
// src/recoil/todoList/index.ts export { default } from './atom';
먼저 간단하게 기본 구조만 잡아두었다.
 

TodoList 컴포넌트 작성

// src/components/TodoList.tsx import { useRecoilValue } from 'recoil'; import todoListState from '../recoil/todoList'; const TodoList: React.FC<{}> = () => { const todoList = useRecoilValue(todoListState); console.log(todoList); return ( <div /> ) } export default TodoList;
// App.tsx import React from 'react'; import { RecoilRoot } from 'recoil'; import TodoList from "./components/TodoList"; function App() { return ( <RecoilRoot> <div className="App"> <TodoList /> </div> </RecoilRoot> ); } export default App;
App.tsx 에서 작성한 <TodoList /> 를 렌더 해보자.
 
notion image
콘솔창에 이렇게 빈 배열이 표시된다면 정상적으로 Recoil 이 연결 된 것이다.
 

Todo 아이템 추가하기 구현

새로운 Todo 아이템을 추가하기 위해서는 생성했던 todoListState 의 내용을 업데이트하는 setter 함수에 접근해야한다.
가장 먼저 TodoItemCreator 컴포넌트를 생성하자.
 
// src/components/TodoItemCreator.tsx import { useState } from 'react'; import { useSetRecoilState } from 'recoil'; import todoListState from '../recoil/todoList'; const TodoItemCreator: React.FC<{}> = () => { const [value, setValue] = useState<string>(''); const setTodoList = useSetRecoilState(todoListState); // 1️⃣ const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.currentTarget.value) } const handleClickAddButton = () => { addItem(); setValue(''); } const addItem = () => { setTodoList((prev) => prev.concat({ id: getId(), isComplete: false, text: value })) } return ( <div> <div> <input type="text" value={value} onChange={handleChangeInput} /> </div> <div> <button type="button" onClick={handleClickAddButton}>추가하기</button> </div> </div> ) } // 고유한 Id 생성을 위한 유틸리티 let id = 0; function getId() { return id++; } export default TodoItemCreator
주석 1️⃣의 코드에서 처럼 useSetRecoilState 를 활용하여 todoListStatesetter 함수에 접근 할 수 있게 된다. 이렇게 접근한 setter 함수는 useStatesetState 함수와 유사하게 작동한다.
 
// src/components/TodoList.tsx import { useRecoilValue } from 'recoil'; import todoListState from '../recoil/todoList'; import TodoItemCreator from './TodoItemCreator'; const TodoList: React.FC<{}> = () => { const todoList = useRecoilValue(todoListState); return ( <> <div> <TodoItemCreator /> </div> <div> {todoList.map(todo => ( <div key={todo.id} style={{ cursor: 'pointer' }}> <span> {todo.isComplete ? <del>{todo.text}</del> : <>{todo.text}</>} </span> </div> ))} </div> </> ) } export default TodoList;
<TodoList /> 컴포넌트도 위와 같이 수정했다.
 

Todo 아이템 삭제하기, 토글 구현

아이템 삭제를 위해서는 <TodoList /> 컴포넌트에 코드를 추가하면 된다.
import {useRecoilValue, useSetRecoilState} from 'recoil'; import todoListState from '../recoil/todoList'; import TodoItemCreator from './TodoItemCreator'; const TodoList: React.FC<{}> = () => { const todoList = useRecoilValue(todoListState); const setTodoList = useSetRecoilState(todoListState); // 1️⃣ const handleClickRemoveButton = (id: number) => { removeItem(id) } // 3️⃣ const handleClickItem = (id: number) => { setTodoList(prev => prev.map(todo => todo.id === id ? ({ ...todo, isComplete: !todo.isComplete }) : todo)) } // 2️⃣ const removeItem = (id: number) => { setTodoList(prev => prev.filter(todo => todo.id !== id)) } return ( <> <div> <TodoItemCreator /> </div> <div> {todoList.map(todo => ( <div key={todo.id} style={{ cursor: 'pointer' }}> <span> {todo.isComplete ? <del>{todo.text}</del> : <>{todo.text}</>} </span> <span><button type="button" onClick={() => handleClickRemoveButton(todo.id)}>삭제</button></span> </div> ))} </div> </> ) } export default TodoList;
마찬가지로 setTodoList 통해서 리스트를 조정할 수 있게 되었고, 1️⃣2️⃣ 함수들을 통하여 리스트를 변경했다.
그리고 3️⃣ 함수를 이용하여 토글기능도 구현되었다.
 

Todo 아이템 필터 구현하기

필터를 구현하기전에 필터에 사용할 상태값을 먼저 생성해야 한다.
// src/recoil/withStateFilter.ts import { atom } from 'recoil'; import type { TodoListState } from './types'; const todoListFilterState = atom<TodoListState>({ key: 'todoListFilterState', default: 'Show All' }); export { todoListFilterState };
 
그 다음 리스트를 필터하는 selector 를 정의한다
// src/recoil/withStateFilter.ts import { atom, selector } from 'recoil'; import todoListState from './atom'; import type { TodoListState } from './types'; const todoListFilterState = atom<TodoListState>({ 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(prev => prev.isComplete); case 'Show Uncompleted': return list.filter(prev => !prev.isComplete); default: return list; } } }) export { filteredTodoListState };
 
이제 필터를 만들어보자.
// src/components/TodoItemFilter.tsx import { useRecoilState } from 'recoil'; import { todoListFilterState } from '../recoil/todoList/withStateFilter'; import type { TodoListState } from '../recoil/todoList/types'; const TodoItemFilter: React.FC<{}> = () => { const [filterValue, setFilterValue] = useRecoilState(todoListFilterState); const handleChangeOption = (e: React.ChangeEvent<HTMLSelectElement>) => { setFilterValue(e.target.value as TodoListState); } return ( <select value={filterValue} onChange={handleChangeOption}> <option value="Show All">Show All</option> <option value="Show Completed">Show Completed</option> <option value="Show Uncompleted">Show Uncompleted</option> </select> ) } export default TodoItemFilter;
useRecoilState 를 활용하여 todoListFilterState 값을 변경하고 사용할 수 있게 만든다. handleChangeOption 함수로 셀렉트가 변경될때 값이 변경된다.
이제 필터된 값을 보여주기만 하면 된다.
// src/components/TodoList.tsx import {useRecoilValue, useSetRecoilState} from 'recoil'; import TodoItemCreator from './TodoItemCreator'; import TodoItemFilter from './TodoItemFilter'; import todoListState from '../recoil/todoList'; import { filteredTodoListState } from '../recoil/todoList/withStateFilter'; const TodoList: React.FC<{}> = () => { // const todoList = useRecoilValue(todoListState); const todoList = useRecoilValue(filteredTodoListState); // 1️⃣ const setTodoList = useSetRecoilState(todoListState); const handleClickRemoveButton = (id: number) => { removeItem(id) } const handleClickItem = (id: number) => { setTodoList(prev => prev.map(todo => todo.id === id ? ({ ...todo, isComplete: !todo.isComplete }) : todo)) } const removeItem = (id: number) => { setTodoList(prev => prev.filter(todo => todo.id !== id)) } return ( <> <div> <TodoItemCreator /> </div> <div> <TodoItemFilter /> {/* 2️⃣ */} </div> <div> {todoList.map(todo => ( <div key={todo.id} style={{ cursor: 'pointer' }} onClick={() => handleClickItem(todo.id)}> <span> {todo.isComplete ? <del>{todo.text}</del> : <>{todo.text}</>} </span> <span><button type="button" onClick={() => handleClickRemoveButton(todo.id)}>삭제</button></span> </div> ))} </div> </> ) } export default TodoList;
1️⃣의 todoList 값을 필터된 값 기준으로 변경하였다.
2️⃣에 <TodoItemFilter /> 컴포넌트를 추가하였다.
이렇게 간단히 몇줄의 코드를 추가/변경 함으로써 필터된 값을 렌더할 수 있게 된다.
 

마무리

실제로 작은 프로젝트에서 사용해보면서 코드를 작성해보았다. redux 에 비해서 장점은 생각보다 더 작은 러닝커브였고, 단점은 devtools 같은 기능이 제공되면 더 좋을것 같다.
마지막으로 작성 된 코드는 이 곳에서 확인 가능하다.
 

레퍼런스