React - atom state값 immutability 지키며 갱신하기

sonnng·2023년 1월 26일
1

React

목록 보기
8/8
post-thumbnail

기존의 atom 구조

export interface IToDo{
 text:string;
 id:number;
 category: "TO_DO" | "DOING" | "DONE";
}

export const toDoState=atom<IToDo[]>({
 key:"toDo",
  default:[].
});


이렇게 설정한 값으로 투두리스트를 만들었을때, "TO_DO", "DOING", "DONE" 카테고리 중 내가 원하는 카테고리를 선택할때마다 해당 투두의 카테고리가 변경되어야 한다.


1. todos 중 변경하기를 희망하는 id키로 todo를 찾는다. todos array의 index를 찾아서 array의 object의 index를 찾기만 하면 된다.

function ToDo({text, category, id}:IToDo){
const setToDos=useSetRecoilState(toDoState);
const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
	  setToDos((oldToDos)=>{
	    const targetIndex=oldToDos.findIndex(toDo=> toDo.id===id)
      console.log(targetIndex);
		})
	}
}

콘솔로 찍어보면서 확인해보면 바꾸고자 하는 target의 경로(인덱스)를 알 수 있다.

2. 해당 투두의 카테고리만을 변경시켜 new todo를 생성해낸다.

function ToDo({text, category, id}:IToDo){
const setToDos=useSetRecoilState(toDoState);
const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
  const {
      currentTarget:{name},
   }=event;
	setToDos((oldToDos)=>{
	const targetIndex=oldToDos.findIndex(toDo=> toDo.id===id)
	const oldToDo=oldToDos[targetIndex]; 
	const newToDo={text, id, category:name};
  console.log(oldToDo, newToDo);
	})
	}
}

3. old todo를 new todo로 바뀐 새로운 todos array로 갱신시켜준다.

배열의 원소를 교체하고 새로운 배열을 만드는 이론은 다음과 같다.

["pizza", "mango", "kimchi", "kimbab"]
->["pizza", "감", "kimchi", "kimbab"]

이렇게 바꾸고 싶다고 하자. 바꾸려면 일단, 바꾸고 싶은 인덱스를 알아야 한다. 위의 1번 코드 처럼 findIndex를 이용한 함수로 인덱스를 추출해낼 수 있다. 이후에는 배열을 바꾸고 싶은 원소를 기준으로 배열을 두 부분으로 나누어야 한다. 여기에서는 mango 전 까지의 배열과 mango 이후부터 끝까지의 배열이 해당될 것이다. 마지막으로는 mango 전까지의 배열과 감, 그리고 mango 이후부터 끝까지의 배열을 합쳐서 useRecoilSetState에서 modifier함수로 넣어주면 된다.

const front=["pizza"]
const back=["kimchi", "kimbab"]
const finalPart=[...front, "감", ...back]

대략적으로 이런 형식이다. 그렇다면 front처럼 바꾸려면 어떻게 해야할까?

array.slice(start, end)를 이용하도록 한다.

const food=["pizza", "mango", "kimchi", "kimbab"]
const targetIndex=1;
food.slice(0,1)
//=food.slice(0,targetIndex);

back처럼 바꾸려면 어떻게 해야할까? 똑같이 slice 를 이용하되, end값을 주지않으면 해당 start 인덱스부터 끝까지를 배열로 반환하는 성질을 이용하면 된다.

const food=["pizza", "mango", "kimchi", "kimbab"]
const targetIndex=1;
food.slice(target+1);

배열을 합쳐보면,

const food=["pizza", "mango", "kimchi", "kimbab"]
const targetIndex=1;
food.slice(target+1);

코드에 적용하기 전에 다시 어떤 과정으로 카테고리가 변경되는지 살펴보면, 원하는 카테고리를 선택하면 onClick event가 적용되고 setState로 기존 toDos 배열을 가져와서 새 원소만을 변경한 후, 다시 새 toDos 배열을 리턴하면, 해당 원소만 카테고리가 변경된 상태가 된다.

function ToDo({text, category, id}:IToDo){
const setToDos=useSetRecoilState(toDoState);
const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
  const {
      currentTarget:{name},
   }=event;
	setToDos((oldToDos)=>{
	const targetIndex=oldToDos.findIndex(toDo=> toDo.id===id)
	const oldToDo=oldToDos[targetIndex]; 
	const newToDo={text, id, category:name};
  return [
   ...oldToDos.slice(0,targetIndex),
   newToDo,
   ...oldToDos.slice(targetIndex+1)  
]
	})
	}
}


기존에는 카테고리 구분 없이, todo를 모두 toDoState 배열에 넣어주어서 배열이 혼잡하게 섞여있고, 그것을 다시 분류를 해주어야했다.
//toDoList.tsx
const toDos=useRecoilValue(toDoState);
...
return(
  ...
{toDos.filter(todo=>todo.category==="TO_DO").map(todo=><ToDo {...todo}/>)}
{toDos.filter(todo=>todo.category==="DOING").map(todo=><ToDo {...todo}/>)}
{toDos.filter(todo=>todo.category==="DONE").map(todo=><ToDo {...todo}/>)}
...
 )

이렇게 atom value를 가져와서 다시 다른 파일에서 value값을 분류하는 것은 비효율적이며 코드도 보기에 쉽지 않다는 문제가 있다. 이를 위해서 recoil value를 원하는 대로 바꾸는 recoil selector를 사용하도록 한다.


selectors

selectors란, state를 입력받아서 그걸 변형해 순수 함수를 거쳐 반환된 state값을 의미한다

→ 다시 말해서 내가 원하는 대로 state를 변경할 수 있다.

export interface IToDo{
   
id:number;
category:"TO_DO" | "DOING" | "DONE";
}

export const toDoState=atom<IToDo[]>({
 key:"toDo",
 default:[]
});

toDoState 배열에는 카테고리 구분없이 모두 하나의 state에서 관리되기에 혼잡한 코드가 된다. selectors를 이용해서 category를 분류하고, 이를 다른 파일에서 바로 활용한다면, 더 보기 쉽고 좋은 코드가 될 수 있다.

//atom.tsx
export default toDoSelector=selector({
 key:"toDoSelector",
 get:({get})=>{
 const toDos=get(toDoState);
 return toDos.length;
}
});

//ToDoList.tsx
const toDos=useRecoilValue(toDoState);
const selectorOutput=useRecoilValue(toDoSelector);
console.log(selectorOutput);

투두를 생성하면 toDoState의 배열 길이가 0,1,2,…. 이렇게 증가하기 때문에, selector 도 계속 호출되어 콘솔창에서는 get함수로 리턴한 toDos 배열 길이 값이 0,1,2... 똑같이 출력이 됨을 확인할 수 있다.

selectors get함수로 카테고리 3개 분류

[toDo, doing, done] 이렇게 세 개 배열로 나누어 받고 싶기 때문에 다음과 같이 작성할 수 있다.

//atom.tsx
export default toDoSelector=selector({
 key:"toDoSelector",
 get:({get})=>{
	 const toDos=get(toDoState);
	 return [
	 toDos.filter(toDo=>toDo.category=='TO_DO'),
	 toDos.filter(toDo=>toDo.category=='DOING'),
	 toDos.filter(toDo=>toDo.category=='DONE'),
	];
}
});

//ToDoList.tsx
console.log(toDoSelector)

3개 카테고리에 따라 분류된 배열을 활용해 렌더링 화면 만들기

//toDoList.tsx
const [toDo, doing, done]=useRecoilValue(toDoSelector);

배열 안에 배열이 여러 개인 경우, 각 배열에 이름을 부여해서 편리하게 사용할 수 있다!

0개의 댓글