
TypeScript를 공부해서 프로젝트에 적용하려는 현재 상황에서, 기존에 쓰던 react JS 문법들을 TS로 어떻게 바꾸고 앞으로 어떤 식으로 코드 블럭을 만들어가야할지 고민을 하고 있다. 레퍼런스를 여럿 찾아보면서 모양을 굳혀나가려고 하고 있는데, 다양한 JS 패키지들에 대한 TS를 적용했을 때 달라지는 내용들도 미리 알아둬야 할 것이다.
Recoil은 React에 최적화된 상태 관리 라이브러리로 그 기능은 그냥 props를 전역으로 전달할 수 있도록 coil을 깔아주는 것이라 생각하면 상상하기 쉽다. tutorial을 수행하기 전에 Recoil 패키지가 일반적으로 어떤 역할을 하는 지 구체적으로 살펴보자.
React 프로젝트를 작성하는 데 있어서 꽤 높은 depth, 서로 다른 width에서 렌더링되는 component들이 같은 state variable을 공유해야하는 상황을 생각해보자.
원시적인 형태로 코드를 짜자면, 두 component에게 공통적으로 부모가 되는 상위 component까지의 모든 component를 통해 props으로 순차적으로 전달시킬 수 있을 것이다. 굉장히 번거로운 일이다.
const ComponentA = () => {
const [STATE, setSTATE] = useState("")
return (
<>
<ComponentB STATE={STATE}/>
<ComponentC STATE={STATE}/>
</>
)
}
const ComponentB = ({STATE}) => {
return <ComponentD STATE={STATE}/>
}
const ComponentC = ({STATE}) => {
return <ComponentE STATE={STATE}/>
}
const ComponentD = ({STATE}) => {
return <ComponentF STATE={STATE}/>
}
const ComponentE = ({STATE}) => {
return <ComponentG STATE={STATE}/>
}
...
모든 컴포넌트가 같은 state를 props를 통해 공유하고 있다고 생각해보자. 심지어 가장 낮은 height, leaf에 위치해 있는 component에서만 이 값을 렌더링하고 있고, 중간의 props를 전달받는 모든 component는 단순히 빨대의 역할만 해주고 있다. 추가로 하나의 prop을 더 상속해주어야 한다면? 끔찍하다.
그럼 이런 state들을 모듈화하여 component 코드의 밖에서 전역으로 관리해줄 순 없을까?
이를 도와주는 것이 Recoil 패키지이다. 일반적으로 recoil을 처음 접하는 이유는 위에서 설명한 것과 같은 상황 때문일 것이다. 하지만 위의 상황 뿐만이 아니라 상태 관리에 있어서 react의 기본 state hook보다 더욱 편리한 기능을 수행할 수 있도록 도와준다. recoil 공식 문서에 제공되어 있는 recoil의 사용 동기는 다음과 같다.

Recoil을 사용하면 다음과 같은 코드 작성이 가능해진다.
//recoil.js
export const recoilState = atom({
key: "recoilState",
default: "initialized value"
})
//app.js
import { recoilState } from "./recoil"
const LeafComponentA = () => {
const RecoilStateValue = useRecoilValue(recoilState);
return <>{RecoilStateValue}</>
}
const LeafComponentB = () => {
const RecoilStateValue = useRecoilValue(recoilState);
return <>{RecoilStateValue}</>
}
특정 state를 렌더링 하려는 component가 component tree의 얼마나 높거나 낮은 level에 존재하는지 상관 없이 부모에게서 어떤 형태로 state를 물려 받는 것이 아니라, component tree의 외부에 존재하는 recoil state를 구독(subscription)하여 이를 공유하는 형태로 작동한다. 해당 recoil state는 프로젝트 최상위 디렉토리에 모듈화되어 작성될 수 있을 것이다.
recoil 패키지에는 atom과 selector라고 하는 두 가지 주요 개념이 존재한다. 주요 개념에 대한 설명은 recoil의 공식 문서를 참고하자.


한 번 튜토리얼을 진행하면서 이 둘이 어떤 역할을 하고, 얼마나 우리를 편리하게 만들어줄 지 살펴보아야겠다.
프로젝트에서 애용하고 있던 전역 상태 관리 패키지인 recoil에 대해서, TS를 적용함에 있어서 단순히 이전의 사용법을 그대로 가져다 쓸 것이 아니라 더 TS 다운 형태의 recoil 활용이 어떻게 하면 가능해질까를 고민해보았다.
또한 이전에 Recoil 패키지를 처음 적용할 때 속성으로 그냥 가져다 썼었는데, 생각보다 프로젝트 안의 너무 많은 곳에서 Recoil을 남발하고 있는 모습을 보며 이렇게 쓸 것이 아니라 더 자세히 Recoil에 대해 알고 이해한 뒤에 써야 할 것 같다는 걱정도 있었다.
그래서 오늘은 Recoil 공식 문서에서 제공하는 Tutorial을 TypeSciprt를 활용하여 작성해볼 생각이다. Recoil 공식 문서에서는 TS 형태로 코드를 제공해주고 있지 않지만, 최대한 내가 알고 있는 TS의 기능을 활용하면서 공식 문서에 있는 코드를 TS로 작성해보자. TS로 react 코드 작성하는 연습과 Recoil을 더 깊게 공부할 수 있는 기회인 것 같다.
//App.tsx
import {
RecoilRoot
} from 'recoil';
import TodoList from './TodoList';
function App() {
return (
<RecoilRoot>
<TodoList/>
</RecoilRoot>
);
}
export default App;
//interface.ts
export interface TodoItemInterface {
id: number,
text: string,
isComplete: boolean
}
//recoil.ts
import { atom, selector } from "recoil";
import { TodoItemInterface } from "./interface";
export const todoListState = atom<TodoItemInterface[]>({
key: 'todoListState',
default: [],
})
export const todoListFilterState = atom<string>({
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: TodoItemInterface[] = get(todoListState);
const totalNum: number = todoList.length;
const totalCompletedNum: number = todoList.filter((item) => item.isComplete).length;
const totalUnCompletedNum: number = totalNum - totalCompletedNum;
const percentCompleted: number = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
return {
totalNum,
totalCompletedNum,
totalUnCompletedNum,
percentCompleted,
}
}
})
//Todolist.tsx
import { ReactElement } from "react"
import { useRecoilValue } from "recoil"
import TodoItem from "./TodoItem"
import TodoItemCreator from "./TodoItemCreator"
import TodoListFilters from "./TodoListFilters"
import TodoListStats from "./TodoListState"
import { filteredTodoListState } from "./recoil"
const TodoList = ():ReactElement => {
const todoList = useRecoilValue(filteredTodoListState)
return (
<>
<TodoListStats/>
<TodoListFilters/>
<TodoItemCreator/>
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
)
}
export default TodoList;
//TodoListState.tsx
import { ReactElement } from "react";
import { useRecoilValue } from "recoil";
import { todoListStatsState } from "./recoil";
const TodoListStats = ():ReactElement => {
const {
totalNum,
totalCompletedNum,
totalUnCompletedNum,
percentCompleted,
} = useRecoilValue(todoListStatsState);
const formattedPercentCompleted = Math.round(percentCompleted * 100);
return (
<ul>
<li>Total items: {totalNum}</li>
<li>Items completed: {totalCompletedNum}</li>
<li>Items not completed: {totalUnCompletedNum}</li>
<li>Percent completed: {formattedPercentCompleted}</li>
</ul>
)
}
export default TodoListStats;
//TodoListFilters.tsx
import { ReactElement } from "react";
import { useRecoilState } from "recoil";
import { todoListFilterState } from "./recoil";
const TodoListFilters = ():ReactElement => {
const [filter, setFilter] = useRecoilState(todoListFilterState);
const updateFilter = (event:React.ChangeEvent<HTMLSelectElement>):void => {
setFilter(event.target.value);
}
return (
<>
Filter:
<select value={filter} onChange={updateFilter}>
<option value="Show All">All</option>
<option value="Show Completed">Completed</option>
<option value="Show Uncompleted">Uncompleted</option>
</select>
</>
)
}
export default TodoListFilters;
//TodoItemCreator.tsx
import { ReactElement, useState } from "react";
import { useSetRecoilState } from "recoil";
import { todoListState } from "./recoil";
const TodoItemCreator = ():ReactElement => {
const [inputValue, setInputValue] = useState<string>('');
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue('');
}
const onChange = (event:React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
}
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
)
}
let id = 0;
const getId = () => {
return id++;
}
export default TodoItemCreator;
//TodoItem.tsx
import { ReactElement } from "react";
import { useRecoilState } from "recoil";
import { TodoItemInterface } from './interface';
import { todoListState } from "./recoil";
interface ItemProps {
item: TodoItemInterface
}
const TodoItem = ({ item }: ItemProps):ReactElement => {
const [todoList, setTodoList] = useRecoilState(todoListState);
const index = todoList.findIndex((listItem) => listItem === item);
const editItemText = (event:React.ChangeEvent<HTMLInputElement>) => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
text: event.target.value,
});
setTodoList(newList);
}
const toggleItemCompletion = () => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
isComplete: !item.isComplete,
});
setTodoList(newList);
}
const deleteItem = () => {
const newList = removeItemAtIndex(todoList, index);
setTodoList(newList);
}
return(
<div>
<input type="text" value={item.text} onChange={editItemText} />
<input
type="checkbox"
checked={item.isComplete}
onChange={toggleItemCompletion}
/>
<button onClick={deleteItem}>X</button>
</div>
)
}
const replaceItemAtIndex = (arr: TodoItemInterface[],index:number, newValue: TodoItemInterface):TodoItemInterface[] =>{
return [...arr.slice(0,index), newValue, ...arr.slice(index + 1)];
}
const removeItemAtIndex = (arr: TodoItemInterface[], index: number):TodoItemInterface[] =>{
return [...arr.slice(0,index), ...arr.slice(index + 1)];
}
export default TodoItem;




나중에 시간 남을 때 디자인이나 한 번 해보아야겠다.
튜토리얼 진행해보니 확실히 셀렉터가 가진 강점이 무엇인지 알 수 있을 것 같다. recoil의 극단적인 이점은 React Component 외부에서 state를 선언하고 관리할 수 있다는 점이다. 비동기적으로 데이터를 관리할 수 있다고도 하니, recoil 문서에서 API와 연동하여 동기/비동기적 상태 관리를 하는 내용도 한 번 읽어보고 예제를 만들어서 수행해보아야겠다.