📙

4.2 Recoil 동작 원리

 
이번 챕터에서는 여러 상태 관리 라이브러리 중에서도 리액트에 최적화되어 있는 상태 관리 라이브러리인 리코일의 사용법과 3장에서 리액트 툴킷을 활용해 구현했던 계산기와 투두리스트를 리코일을 활용해 다뤄보도록 하겠습니다. 리코일을 사용하기 전에  설치 및 환경 설정을 진행하겠습니다.

4.2.1 Recoil 사용 환경 세팅하기

먼저  React 프로젝트가 없다면,  React 애플리케이션을 생성하고 진행하도록 하겠습니다.
npx create-react-app calc-todo cd calc-todo
 
생성한 프로젝트 디렉토리로 이동 후, 터미널을 열고, 리코일 라이브러리를 설치합니다. npm 또는 yarn을 사용하여 설치할 수 있습니다.
npm으로 설치하는 경우 다음과 같은 명령어를 입력해주세요.
npm install recoil
혹은 yarn으로 설치하는 경우 다음과 같은 명령어를 입력해주세요.
yarn add recoil
설치가 완료됐으면 npm start , yarn start로 리액트 프로젝트를 실행해주세요.
 

4.2.2 상태 관리 설정 (RecoilRoot)

리코일을 사용하기에 앞서 RecoilRoot 컴포넌트에 대해서 알아보도록 하겠습니다.
RecoilRoot 컴포넌트는 전역 상태를 관리하고 애플리케이션 내에 모든 컴포넌트가  전역 상태의 동일한 값을 공유할 수 있게 해주는 컴포넌트입니다 . 마치  리덕스에서 Provider를 이용하여 스토어를 설정했던 것과 비슷하게  리코일을 사용해서 상태를 관리하려면 전역 상태를 사용할 컴포넌트를  RecoilRoot 컴포넌트로 감싸 선언해 주어야 합니다.
// index.js import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import App from "./App"; import { RecoilRoot } from "recoil"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <RecoilRoot> <App /> </RecoilRoot> );
위와 같이 Root 컴포넌트를 RecoilRoot 컴포넌트로 감싸줌으로써 모든 컴포넌트에서 전역 상태를 사용할 수 있도록 해줍니다. 물론 RecoilRoot를 애플리케이션의 최상위 레벨 컴포넌트로 감싸주지 않아도 Recoil을 사용할 수 있지만, RecoilRoot를 최상위에 두는 것은 코드의 가독성과 효율적으로 유지 보수를 관리할 수 있습니다. 또한, Recoil 공식 문서에서 RecoilRoot를 Root 컴포넌트로 추가하는 것을 권장하고 있습니다.
 
다음으로 RecoilRoot 컴포넌트의 특징에 대해 간단하게 알아보도록 하겠습니다. Recoil의 여러 개의  RecoilRoot 컴포넌트를 사용할 수 있고, 기본적으로  RecoilRoot 컴포넌트 간의 override 속성은  false로 설정되어 있기 때문에 서로 다른 전역 상태를 유지하고 각각의 상태가 독립적으로 동작하도록 할 수 있습니다.
function App() { return ( <RecoilRoot> <CounterA /> </RecoilRoot> <RecoilRoot> <CounterB /> </RecoilRoot> ); }
 
위의 코드로 예를 들어보겠습니다. 각각의 RecoilRoot로 감싸진 CounterA와 CounterB 컴포넌트는 서로 독립된 상태를 사용하며 CounterA의 상태와 CounterB의 상태는 서로 영향을 미치지 않습니다.
 
즉, 각 RecoilRoot 컴포넌트는 독립된 상태 저장소를 가지며, 그 아래에서 정의된 상태는 해당 RecoilRoot 컴포넌트 내에서만 접근이 가능한 걸 알 수 있습니다. 만약 override 속성을 true로 설정하고 싶다면 직접 RecoilRoot의 속성으로 주는 방법과 아래의 예시 코드처럼 RecoilRoot 컴포넌트를 중첩해 설정할 수 있습니다.
 
아래의 예시 코드를 보면 두 번째 RecoilRoot가 첫 번째 RecoilRoot의 하위에 중첩되어 있고, 두 개의 CounterA 와 CounterB 컴포넌트가 각각의 RecoilRoot 안에 위치해 있습니다. 때문에 CounterA의 상태와 CounterB는 동일한 상태를 공유하고, CounterA의 상태가 업데이트 된다면 CounterB컴포넌트에서도 동일하게 상태 값이 업데이트하게 됩니다.
function App() { return ( // 첫 번째 RecoilRoot <RecoilRoot> <CounterA /> // 두 번째 RecoilRoot <RecoilRoot> <CounterB /> </RecoilRoot> </RecoilRoot> ); }
이로써, 리코일의 사용하기 전 설치 방법과 RecoilRoot에 대해서 알아봤습니다. 이제 본격적을 리코일의 atom , selector, Recoil의 주요 API를 계산기와 투두리스트의 코드를 보면서 알아보도록 하겠습니다.
 

4.2.3 계산기

1) store 구현 - atom 사용

atom(아톰)은 Recoil에서 상태의 기본(최소) 단위를 나타냅니다. atom함수를 사용하여atom을 생성하고 초기 상태를 정의할 수 있습니다.
가장 먼저 atom을 사용하기 위해서는 리코일에서 제공하는 atom함수를 import 해주어야 합니다.
import { atom } from "recoil";
3.1에서 atom의 기본적인 사용법과 개념에 대해 다루었기에 앞의 내용을 읽어보는 것을 권장합니다.
 
바로 계산기 코드를 살펴보겠습니다. 아래의 코드에서 resultState는 ‘resultState’라는 고유한 키로 식별되며, 초기 상태는 0으로 설정됩니다. 그럼, 아래에 있는 inputValueState는 어떻게 설명할 수 있을까요? resultState와 동일하게 inputValueState는 ‘inputValueState’라는 고유한 키로 식별되며, 초기 상태는 빈 문자열로 설정됩니다.
// store/calculator.js import { atom } from "recoil"; export const resultState = atom({ key: "resultState", default: 0, }); export const inputValueState = atom({ key: "inputValueState", default: "", });
이제 이 두 atom은 애플리케이션의 어떤 부분에서든 사용가능해졌습니다. 이때, key는 전역적으로 유일해야 합니다. 컴포넌트가 atom을 사용할 때, 같은 key를 가진 atom이 있으면 사용하는 컴포넌트도, 사용되어질 atom도 헷갈리게 되고 콘솔 또한 에러를 냅니다.
 

2) 컴포넌트 구현

import { resultState, inputValueState } from '../store/calculator'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; export default function Calculator() { const [inputValue, setInputValue] = useRecoilState(inputValueState); const result = useRecoilValue(resultState); const setResult = useSetRecoilState(resultState); ... }
useRecoilState hook은 리액트의 useState과 아주 비슷하게 작동하지만, 리코일의 전역 상태인 atom를 대상으로 합니다. atom을 인자로 받아 배열을 반환하는데, useState와 마찬가지로 첫 번째 요소는 현재 상태 값이고 두 번째 요소는 그 상태를 변경하기 위한 setter 함수입니다.
 
계산기 프로그램 내의 사용자 입력 값을 상태로 관리하고, 암묵적으로 inputValue에 컴포넌트를 구독하고자 const [inputValue, setInputValue] = useRecoilState(inputValueState); 라고 작성합니다. 이제 상태가 업데이트 될 때마다 컴포넌트는 리렌더링 될 것입니다.
 
useRecoilValue hook은 호출되면 리코일 atom이거나 selector을 인자로 받아 해당 상태의 현재 값을 반환합니다. const result = useRecoilValue(resultState); 라고 작성하여 resultState atom의 현재 값을 가져와 result에 담아 활용합니다. 다른 컴포넌트에서 해당 atom을 변경하면 useRecoilValue가 반환하는 값도 자동으로 업데이트됩니다. 즉, 어느 컴포넌트에서든지 상태를 변경하면 그 변경사항이 모든 관련 컴포넌트에 즉시 반영됩니다. 하지만, useRecoilValue로 반환받은 값은 읽기 전용입니다. 만약 값을 수정하고 싶다면, 대신 useRecoilState를 사용해야 합니다.
 
useSetRecoilState hook은 주어진 리코일 상태를 변경하기 위한 setter 함수만을 반환합니다. 컴포넌트가 해당 상태의 현재 값을 알 필요는 없지만, 그저 상태를 업데이트하고 싶을 때 유용합니다. 예를 들어, 버튼 클릭과 같은 이벤트에 반응하여 상태를 변경하고 싶지만, 버튼 컴포넌트 자체가 해당 상태의 현재 값을 알 필요가 없는 경우에 useSetRecoilState를 사용할 수 있습니다. const setResult = useSetRecoilState(resultState); 라고 작성하여 resultState 상태를 업데이트 할 수 있는 setResult라는 setter 함수를 선언해 사용합니다. useRecoilState와 달리 useSetRecoilState를 사용하면 해당 hook을 사용하는 컴포넌트 자체는 리렌더링 되지 않습니다.
 
물론 useSetRecoilState가 업데이트 한 atom을 구독하고 있는 다른 모든 컴포넌트는 새로운 값으로 리렌더링 되겠지만, 업데이트 작업만 하고 있던 해당 컴포넌트에는 불필요한 리렌더링이 동작하지 않습니다.
 
이전 값이 반환되진 않지만, useSetRecoilState을 통해 반환받은 setter 함수의 인자로 리코일 상태의 이전 값을 참조할 수 있습니다. useSetRecoilState setter 함수의 비동기적 동작으로 인한 문제를 방지하기 위해 이 prev을 선언하여 이전 상태 값 기반으로 새로운 값을 계산하는 함수형 업데이트 방식을 사용합니다.
const add = (value) => setResult((prev) => prev + value); const subtract = (value) => setResult((prev) => prev - value); const multiply = (value) => setResult((prev) => prev * value); const divide = (value) => setResult((prev) => prev / value); const reset = () => setResult(0);
받아 온 값을 setResult의 prev와 계산해 다시 resultState 상태에 저장하는 함수를 정의합니다. 버튼을 누르면 누적값을 초기화하는 reset 함수도 정의해 줍니다.
return ( <> <div>계산 결과: {result}</div> <input type='number' value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> <button onClick={() => add(Number(inputValue))}>+</button> <button onClick={() => subtract(Number(inputValue))}>-</button> <button onClick={() => divide(Number(inputValue))}>/</button> <button onClick={() => multiply(Number(inputValue))}>*</button> <button onClick={() => { reset(0); setInputValue(''); }} > C </button> </div> </> );
UI 구조입니다. useRecoilValue 로 가져온 읽기 전용 상태 현재값을 계산 결과에 보여주는 부분, 계산할 값을 입력받는 input 태그, 계산기의 연산 버튼들 그리고 초기화 버튼으로 이루어져 있습니다. 사용자 입력이 들어와 값이 변경되거나 리셋 버튼이 눌려 입력값 또한 초기화되어야 할 때 setInputValue 함수를 사용합니다.
 
아래는 components/Calculator.jsx의 전체 코드입니다.
import { resultState, inputValueState } from '../store/calculator'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; export default function Calculator() { const [inputValue, setInputValue] = useRecoilState(inputValueState); const result = useRecoilValue(resultState); const setResult = useSetRecoilState(resultState); const add = (value) => setResult((prev) => prev + value); const subtract = (value) => setResult((prev) => prev - value); const multiply = (value) => setResult((prev) => prev * value); const divide = (value) => setResult((prev) => prev / value); const reset = () => setResult(0); return ( <> <div>계산 결과: {result}</div> <input type='number' value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <div> <button onClick={() => add(Number(inputValue))}>+</button> <button onClick={() => subtract(Number(inputValue))}>-</button> <button onClick={() => divide(Number(inputValue))}>/</button> <button onClick={() => multiply(Number(inputValue))}>*</button> <button onClick={() => { reset(0); setInputValue(''); }} > C </button> </div> </> ); }
 

4.2.4 투두리스트

1) store 구현 - Atom, Selector 사용

Selector는 Atom을 기반으로 파생된 데이터를 말합니다.
Selector 또한 atom과 동일하게 리코일에서 제공하는 selector 함수를 import 해주어야 합니다.
import { selector } from "recoil";
3.1에서 selector의 기본적인 사용법을 제시하고 있으니 읽어보길 권장합니다.
 
이제 투두리스트의 예시를 보겠습니다. 아래 투두리스트의 예시에서 atom은 계산기를 구현했던 코드와 속성값만 다를 뿐 나머지는 동일합니다. todoList는 key 속성을 통해 ‘todolist’라는 이름으로 식별되고 todos에 할 일을 넣어주어야 하므로 초기 상태를 정의하는 default에 todos: []를 객체로 넣어줍니다. filterTodo 또한 동일합니다.
const todoList = atom({ key: "todolist", default: { todos: [] } }); export const filterTodo = atom({ key: "filterTodo", default: "" });
 
이제 selector 코드를 살펴보겠습니다. getTodoList는 getTodoList라는 key로 식별되고, 위에서 정의한 atom을 get을 통해 가져옵니다.
export const getTodoList = selector({ key: 'getTodoList', get: ({ get }) => { // get을 통해 atom을 가져옵니다 const { todos } = get(todoList); const filter = get(filterTodo); if (filter === '') return todos; // filter가 빈 문자열이 아니면 filter가 포함된 콘텐츠를 기준으로 // 목록을 필터링합니다. return todos.filter((todo) => todo.content.includes(filter)); },
atom과 selector에는 return 되는 것에 차이가 있습니다. atom은 상태가 무조건 리턴되지만 selector는 위에서의 예시처럼 조건을 걸어서 원하는 값만 리턴할 수도 있습니다.
 
위의 코드에서 if문으로 조건을 걸어 filter가 빈 문자열이면 모든 todos를 리턴하고 filter가 빈 문자열이 아니면 filter의 내용을 포함하고 있는 todo만 리턴됩니다. 예를 들어, filter에 ‘안'이라는 문자열이 들어있을 때, ‘안'을 포함하고 있는 ‘안녕하세요’ 처럼 ‘안'이 포함된 todo만 리턴됩니다.
 
아래 예시는 set속성으로 getTodoList를 수정하는 작업을 보여줍니다. set 함수는 상태를 수정하는 작업을 처리하는 데 사용되며, 첫 번째 매개변수는 recoil의 상태, 즉 변할 atom값, 두번째 인자로는 변경될 새로운 값을 받아옵니다. 아래의 예시 코드에서는 두 번째 인자를 함수로 받아 각각 조건에 따라 return 값이 새로운 값이 됩니다. 이때, set함수는 기존 상태를 직접 수정하는 대신 객체와 배열을 생성하여 불변성의 원칙을 따릅니다.
set: ({ set }, action) => { // action의 type에 따라 todoList의 상태를 바꿔줍니다 set(todoList, (prevState) => { switch (action.type) { // 할 일을 추가하는 액션 case 'add': { return { todos: [...prevState.todos, action.payload] }; } // 할 일을 삭제하는 액션 case 'delete': { // 주어진 id와 일치하지 않는 할 일을 유지합니다 const filteredTodos = prevState.todos.filter((todo) => todo.id !== action.payload); return { todos: filteredTodos }; } // 할 일을 토글(완료/미완료 상태 변경)하는 액션 case 'toggle': { // 주어진 id와 일치하는 할 일을 상태를 변경(토글)합니다 const filteredTodos = prevState.todos.map((todo) => todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo ); return { todos: filteredTodos }; } // 일치하는 case가 없으면 현재 상태를 리턴합니다 default: return todoList; } }); },
여기서 set함수가 리듀서 코드처럼 보이지 않나요? 위 코드는 독자의 이해를 돕기 위해 switch case문을 사용해 리듀서 형태로 작성했지만 꼭 이렇게 작성할 필요는 없습니다. 예시처럼 switch case문을 이용해도 되고, if문을 이용해도 되고 조건이 필요하지 않다면 한 줄로 구현할 수도 있습니다.
 
단순히 todo를 추가하는 경우 아래와 같이 한줄로도 작성할 수 있습니다.
set: ({ set }, content) => { set(todoList, (prevState) => { todos: [...prevState.todos, content] }) });
 
또한, 예시에서 보여준 set 함수 선언 시, 두 번째 인자로 들어오는 변수 이름이 꼭 action일 필요는 없습니다. 여러분의 이해를 돕기 위해 action이라는 이름을 사용했습니다.
set: ({ set }, action) => { set(todoList, (prevState) => { switch (action.type) { case 'delete': { const filteredTodos = prevState.todos.filter((todo) => todo.id != action.payload); return { todo: filteredTodos };
 
잠깐 투두리스트 컴포넌트 코드의 한 부분을 보겠습니다. 객체형태로 키값을 type과 payload로 넘겨주고 있는데, 여기서도 type과 payload는 독자의 이해를 돕기 위한 것이지 꼭 이렇게 type과 payload를 쓸 필요는 없습니다. 여기서 type과 payload로 정의했기 때문에 위의 코드에서 action.type, action.payload라고 이름이 정해진 것 뿐입니다.
const onDeleteTodo = (id) => { setTodos({ type: "delete", payload: id }); };
 
그럼 위의 두 코드를 변경해 봅시다. 위의 코드에서 type을 myType으로, payload를 data로 코드를 변경했습니다.
const onDeleteTodo = (id) => { setTodos({ myType: "delete", data: id }); };
 
다음의 코드에서는 action을 myData로 변경해 보겠습니다. 그럼, 우리가 이전에 사용했던 action.type과 action.payload 대신에 myData.myType, myData.data로 변경됩니다. 이렇게 이러한 변수들을 임의로 설정할 수 있다는 것을 알아두시길 바랍니다.
set: ({ set }, myData) => { set(todoList, (prevState) => { switch (myData.myType) { case 'delete': { const filteredTodos = prevState.todos.filter((todo) => todo.id != myData.data); return { todo: filteredTodos };
 
아래는 주석을 제외한 store/todolist의 전체 코드 입니다.
// store/todolist.js import { atom, selector } from "recoil"; const todoList = atom({ key: "todolist", default: { todos: [] } }); export const filterTodo = atom({ key: "filterTodo", default: "" }); export const getTodoList = selector({ key: "getTodoList", get: ({ get }) => { const { todos } = get(todoList); const filter = get(filterTodo); if (filter === "") return todos; return todos.filter((todo) => todo.content.includes(filter)); }, set: ({ set }, action) => { set(todoList, (prevState) => { switch (action.type) { case "add": { return { todos: [...prevState.todos, action.payload] }; } case "delete": { const filteredTodos = prevState.todos.filter( (todo) => todo.id !== action.payload ); return { todos: filteredTodos }; } case "toggle": { const filteredTodos = prevState.todos.map((todo) => todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo ); return { todos: filteredTodos }; } default: return todoList; } }); }, });
 

2) 컴포넌트 구현

투두리스트의 컴포넌트를 구현하겠습니다.
import React, { useState } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { filterTodo, getTodoList } from '../store/todolist'; export default function TodoList() { const [inputValue, setInputValue] = useState(''); const [search, setSearch] = useRecoilState(filterTodo); const todos = useRecoilValue(getTodoList); const setTodos = useSetRecoilState(getTodoList); ... }
입력받은 투두리스트의 추가될 값이 담길 inputValue와 검색 키워드인 search, 투두리스트 목록인 todos 세 가지 상태와 setter 함수를 정의합니다. 리액트 hook인 useState와 리코일의 useRecoilState, useRecoilValue, useSetRecoilState를 모두 사용하여 투두리스트 프로그램의 상태를 관리하고자 합니다.
 
const onAddTodo = () => { setTodos({ type: "add", payload: { id: todos.length, content: inputValue, isDone: false }, }); }; const onDeleteTodo = (id) => { setTodos({ type: "delete", payload: id }); }; const onToggleTodo = (id) => { setTodos({ type: "toggle", payload: id }); };
onAddTodo 함수는 새로운 할 일을 추가합니다. 입력 필드에 입력된 값을 내용으로 하는 새로운 할 일 객체를 생성하고, 'add' 타입의 액션과 함께 setTodos에 전달하여 todoList 상태를 업데이트합니다. onDeleteTodo 함수는 주어진 id에 해당하는 할 일을 삭제하고 onToggleTodo 함수는 주어진 id에 해당하는 할 일의 완료 상태를 토글 합니다.
 
return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type='text' value={inputValue} placeholder='할 일을 작성해 주세요' onChange={(e) => setInputValue(e.target.value)} /> <button type='submit' onClick={onAddTodo}> 추가 </button> </form> <p>검색</p> <input type='text' value={search} onChange={(e) => setSearch(e.target.value)} /> {todos.map((todo) => ( <section key={todo.id}> <input type='checkbox' onClick={() => { onToggleTodo(todo.id) }} /> <div style={{ textDecoration: todo.isDone ? 'line-through' : 'none' }}> {todo.content} </div> <button onClick={() => onDeleteTodo(todo.id)}>❌</button> </section> ))} </> );
UI 렌더링 부분입니다. form 태그 안에 input 태그로 리스트에 추가될 할 일을 입력받을 수 있고, 값이 변경될 때마다 setInputValue이 실행되어 inputValue가 업데이트됩니다. 이어 위치한 추가 버튼이 클릭 되면 onAddTodo 함수가 실행되어 내용이 추가될 것입니다.
 
검색을 위한 input 태그 또한 값이 변경될 때마다 setSearch로 검색 키워드 상태가 업데이트 되지만 해당 상태는 리코일의 atom인 filterTodo라는 점에서 차이가 있습니다. useRecoilState로 컴포넌트를 상태에 구독해 두었기 때문에 filterTodo 상태가 변경될 때마다 컴포넌트가 리렌더링 되며 검색 키워드에 해당하는 할 일만 걸러지게 됩니다.
 
이하는 할일 목록이 보여지는 부분입니다. 목록 앞에 체크 박스가 놓여있어 클릭 했을 때 onToggleTodo 함수로 todo 객체의 isDone 값이 변경됩니다. 완료로 표시된 할 일 목록의 경우 취소 선이 그어지도록 조건을 넣어줍니다. 삭제 버튼을 누르면 onDeleteTodo 함수가 실행되어 해당 할 일이 지워집니다.
 
아래는 components/Todolist.jsx의 전체 코드입니다.
 
const onAddTodo = () => { setTodos({ type: "add", payload: { id: todos.length, content: inputValue, isDone: false }, }); }; const onDeleteTodo = (id) => { setTodos({ type: "delete", payload: id }); }; const onToggleTodo = (id) => { setTodos({ type: "toggle", payload: id }); };
onAddTodo 함수는 새로운 할 일을 추가합니다. 입력 필드에 입력된 값을 내용으로 하는 새로운 할 일 객체를 생성하고, 'add' 타입의 액션과 함께 setTodos에 전달하여 todoList 상태를 업데이트합니다. onDeleteTodo 함수는 주어진 id에 해당하는 할 일을 삭제하고 onToggleTodo 함수는 주어진 id에 해당하는 할 일의 완료 상태를 토글 합니다.
 
return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type='text' value={inputValue} placeholder='할 일을 작성해 주세요' onChange={(e) => setInputValue(e.target.value)} /> <button type='submit' onClick={onAddTodo}> 추가 </button> </form> <p>검색</p> <input type='text' value={search} onChange={(e) => setSearch(e.target.value)} /> {todos.map((todo) => ( <section key={todo.id}> <input type='checkbox' onClick={() => { onToggleTodo(todo.id) }} /> <div style={{ textDecoration: todo.isDone ? 'line-through' : 'none' }}> {todo.content} </div> <button onClick={() => onDeleteTodo(todo.id)}>❌</button> </section> ))} </> );
UI 렌더링 부분입니다. form 태그 안에 input 태그로 리스트에 추가될 할 일을 입력받을 수 있고, 값이 변경될 때마다 setInputValue이 실행되어 inputValue가 업데이트됩니다. 이어 위치한 추가 버튼이 클릭 되면 onAddTodo 함수가 실행되어 내용이 추가될 것입니다.
 
검색을 위한 input 태그 또한 값이 변경될 때마다 setSearch로 검색 키워드 상태가 업데이트 되지만 해당 상태는 리코일의 atom인 filterTodo라는 점에서 차이가 있습니다. useRecoilState로 컴포넌트를 상태에 구독해 두었기 때문에 filterTodo 상태가 변경될 때마다 컴포넌트가 리렌더링 되며 검색 키워드에 해당하는 할 일만 걸러지게 됩니다.
 
이하는 할일 목록이 보여지는 부분입니다. 목록 앞에 체크 박스가 놓여있어 클릭 했을 때 onToggleTodo 함수로 todo 객체의 isDone 값이 변경됩니다. 완료로 표시된 할 일 목록의 경우 취소 선이 그어지도록 조건을 넣어줍니다. 삭제 버튼을 누르면 onDeleteTodo 함수가 실행되어 해당 할 일이 지워집니다.
 
아래는 components/Todolist.jsx의 전체 코드입니다.입력받은 투두리스트의 추가될 값이 담길 inputValue와 검색 키워드인 search, 투두리스트 목록인 todos 세 가지 상태와 setter 함수를 정의합니다. 리액트 hook인 useState와 리코일의 useRecoilState, useRecoilValue, useSetRecoilState를 모두 사용하여 투두리스트 프로그램의 상태를 관리하고자 합니다.
import React, { useState } from "react"; import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import { filterTodo, getTodoList } from "../store/todolist"; export default function TodoList() { const [inputValue, setInputValue] = useState(""); const [search, setSearch] = useRecoilState(filterTodo); const todos = useRecoilValue(getTodoList); const setTodos = useSetRecoilState(getTodoList); const onAddTodo = () => { setTodos({ type: "add", payload: { id: todos.length, content: inputValue, isDone: false }, }); }; const onDeleteTodo = (id) => { setTodos({ type: "delete", payload: id }); }; const onToggleTodo = (id) => { setTodos({ type: "toggle", payload: id }); }; return ( <> <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={inputValue} placeholder="할 일을 작성해 주세요" onChange={(e) => setInputValue(e.target.value)} /> <button type="submit" onClick={onAddTodo}> 추가 </button> </form> <p>검색</p> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} /> {todos.map((todo) => ( <section key={todo.id}> <input type="checkbox" onClick={() => { onToggleTodo(todo.id); }} /> <div style={{ textDecoration: todo.isDone ? "line-through" : "none" }} > {todo.content} </div> <button onClick={() => onDeleteTodo(todo.id)}>❌</button> </section> ))} </> ); }