이 포스트는 이 전 포스트의 후속 포스트입니다.
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 인지 알 수 없는 상태에서 사전에 몇가지 조사를 해보았다. 참고 링크atoms
에는State
를 붙여서 코드를 작성하는것과selectors
에는Value
를 붙여 사용하는것을 지양한다. 그 대신에 좀 더 명확한Atom
과with<Something>
을 붙여 사용하도록 하자.
atoms
와selectors
를 분리해서 작성하지 않고 큰 주제의 한개의 폴더에서 함께 작성한다.
이 규칙들을 적용하여 폴더구조를 먼저 만들어보자.
├── 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 />
를 렌더 해보자.콘솔창에 이렇게 빈 배열이 표시된다면 정상적으로
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
를 활용하여 todoListState
의 setter
함수에 접근 할 수 있게 된다. 이렇게 접근한 setter
함수는 useState
의 setState
함수와 유사하게 작동한다.// 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 같은 기능이 제공되면 더 좋을것 같다.마지막으로 작성 된 코드는 이 곳에서 확인 가능하다.