본문 바로가기
프로그래밍/React

# React-Js Master Class #6-3 State Management

by 퐁당당당 2025. 2. 28.

지금부터는 Todo List를 사용자에게 입력받아 이를 세 가지 종류로 분류하여 저장하고 싶다.

종류는 todo, doing, done 세 가지로 하려고 한다.

1) Structure of the Data

우리는 지금 사용자가 입력한 todo와 todo의 상태를 연결하여 생각하고 싶은 것이다. 그렇기에 todo text와 상태를 하나의 요소로 취급하고 싶다.

따라서 우리는 todo의 배열이 아닌, 객체의 배열로 데이터를 저장할 것이다. (연결된 정보를 하나로 처리하기 위해)

interface ITodo{
    text:string,
    category:"todo"|"doing"|"done"
}
  1. IToDo : 하나의 객체의 type을 설명해둔 것이다.
  2. text : 사용자가 입력한 todo이다.
  3. category : 사용자가 선택하게 될 상태이다.
const toDoState=atom<ITodo[]>({
    key:"TODOS",
    default:[]
});

//Call
const [ToDos, setToDos]=useRecoilState(toDoState);
  1. toDoState: todo의 값과, category를 동시에 할 수 있도록 해주는 atom이다.
  2. IToDo[] : 사용자는 IToDo의 형태가 반복되도록 배열을 만들 것이기 때문에 type을 명시해주었다.

* Specifying type

const isDarkAtom=atom({
  key:"isDark",
  default: false
})

기존에는 이런 식으로 아톰을 만들었는데, 왜 지금은 <IToDo[]>라고 형식을 언급하는 것일까?

기존에 만들었던 isDarkAtom은 단순히 boolean을 담는 변수였다. (편하게 변수라고 하겠다) 그렇기에 별도로 type을 명시하지 않아도 default값으로 TS가 type을 유추할 수 있었다.

하지만, 내가 지금 만들고자 하는 atom은 내맘대로 만들어둔 객체를 배열로 갖는 변수다. 그렇기 때문에 Ts에게 설명을 해줄 필요가 있다.

  • 기본 자료형이 아닌 경우 Type을 명시해주어야 한다는 사실과
  • atom을 생성하고 있는 과정임을 인지하면 (= 내가 지금 어떤 작업을 하고 있는지를 확인)

type을 명시해주는 이유를 납득할 수 있다.

 

2) HandleSubmit

지금부터 form에 달려있는 handleSubmit함수를 사용할 것이다. 이 함수는 input값을 call back하는 함수이다.

여기서는 handleSubmit에 할당하는 함수를 구성하는 방법을 중심으로 공부하면 좋을 것 같다.

(1) Call back Variable

const handleValid=({toDo}:IForm)=>{
}
  1. handleSubmitrk input값의 객체를 전달한다. → to handleValid
  2. todo :그 값을 받아올 변수를 지정해야 한다

(2) Type of the input

<input
  {...register("toDo",{
    required:"please write todo"
  })}/>

interface IForm{
  toDo:string;
}
  1. toDo: string: register에서 toDo라는 key로 생성했기 때문에 {toDo : value}와 같이 input이 할당이 될 것임

register의 key를 통해 input의 객체 형태를 예측한 후 → IForm으로 형식을 지정해준다.

(3) logic

const handleValid=({todo}:IForm)=>{
    setToDos(oldTodos=>[...oldToDos, {text:todo, category:"TODO"})
}
  1. setToDos : 아까 만들었던 atom을 수정하는 setToDos를 이용
  2. [...oldToDos, {text:todo, category:"TODO"} : oldToDos모두 가지고 와서 → 새로 만든 객체를 배열의 요소로 추가

이렇게 무언가 추가를 할 때는 기존에 있는 것을 모두 나열하고, 뒤에 하나씩 붙이는 형태로 진행함.

이렇게 수정된 toDos를 출력하려면 map함수를 활용해주어야 하는데, 그려면 배열마다 고유의 id가 있어야 한다.

(4) Refactoring (to use map func)

  •  

3) Sum

이 단원은 생각의 순서를 다음과 같이 할 것이다.

  1. 목적 : category에 따라 todo를 달리 저장할 것임
  2. 수단 : 한 배열 안에서 하려면 객체 배열을 만들어야 함.
  3. handleSubmit함수 관련 요소
    • input의 형태는 누가 결정하는가? → register
    • input의 형식을 어떻게 반영하는가 → IForm Interface
    • logic은 어떻게 할 것인가

6.12 Refactoring

리팩토링은 페이지를 여러 개 나누면서 요소들을 분리하는 과정을 통해 시행해보자.

1) createToDo.tsx

일단 form을 createToDo.tsx를 만들어 대체한다.

function CreateToDo(){
  return(
    <form onSubmit={handleSubmit(handleValid)}>
      <input
        {...register("toDo",{
          required:"please write todo"
        })}
        placeholder="write a todo"
      />
      <button>Add</button>
    </form>
      )
}

아까 있던 form그대로 복붙한다.

 

(1) handleSubmit, register

handleSubmit, register에서 문제가 발생했다. useForm에서 끌고왔던 함수들이니 다시 선언한다.

 

(2) handleValid

handleValid라는 함수가 안돌아왔음을 알 수 있다. 그 함수까지 복붙해보자.

const {register, handleSubmit}=useForm();//1

const handleValid=({toDo}:IForm)=>{//2
  setToDos((oldTodos)=>
    [{text:toDo, id:Date.now(), category:"TO_DO" }, ...oldTodos]
  );
  setValue("toDo","")
}
  1. setValue : useForm에서 하나 더 끌고 오면 됨
  2. setToDos : 전에는 useRecoilState를 활용하여 만들던건데 → recoil특성상, 대상을 atoms에 만들어놓고, useRecoilState or value로 값을 끌고 오면 되는 것이다.
  3.  

2) atoms.tsx

따라서 지금부터는 atoms.tsx를 만들어 atom을 모아두려고 한다.

export interface ITodo{
  text:string,
  id:number,
  category:"TO_DO"|"DOING"|"DONE"
}

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

todostate를 끌고와서 사용해야 하는 곳에서, 이 값의 형식을 알고 싶을 수도 있으니 ITodo도 설정해둔다.

 

IForm을 atom에 안넣고 그냥 따로 뺀 이유는, form의 형식이니 create에서만 쓸 것이기 때문이다. (범용성이 떨어짐)

 

다시 createtodo로 돌아와서. todostate를 import 해준 이후, setToDos를 사용해야 했던 것이니, 이를 끌고 와준다.

//Call
import { toDoState } from "./atoms";
const setToDos=useSetRecoilState(toDoState);

 

6.13 Categories

지금부터는 각 todo에 현재 상테를 제외한 button을 달아줄 것이고, 그 버튼이 눌릴 때 마다 state가 변하도록 코드를 짜보려고 한다.

0) Final purpose

이 기능을 수행하려면 버튼이 눌린 상황에서

  1. 어떤 todo가 눌렸는지
  2. 어떤 button이 눌렸는지

두 가지를 알아야 한다. 일단 우린 toDo.tsx로 하나의 todo를 분리를 하고 시작하자. (나중에 map으로 todo모아주면 되기 때문임)

 

Categories단원에서는 우선 어떤 버튼이 눌렸는지 까지만 확인하는 코드를 짜보려고 한다.

 

1) 1st Method

function ToDo({text, category}:ITodo){

  const onClick=(newCategory:String)=>{

  }
  return (
  <li>
    <span>{text}</span>
    {category!=="TO_DO"&&<button onClick={()=>onClick("TO_DO")}>todo</button>}
    {category!=="DOING"&&<button onClick={()=>onClick("DOING")}>doing</button>}
    {category!=="DONE"&&<button onClick={()=>onClick("DONE")}>done</button>}
  </li>);
}
  1. 버튼이 눌렸을 때는 onClick함수를 버튼의 이름과 함께 호출해준다.
  2. newCategory : onClick함수는 newCategory를 받아 눌린 버튼에 대한 정보를 알 수 있도록 한다.

 

(1) Tips for allocating func with parameter

참고로 함수의 parameter와 함께 실행되도록 함수를 할당하려면

onClick={onClick(”TO_DO”)}

라고 하면 안된다. 이렇게 되면 onClick()자체가 할당되는 것이 아니라 실행되는 것이기 때문이다.

따라서

onClick={()=>onClick("TO_DO")

과 같이 새로운 함수의 결과 값으로 할당해야 한다.

하지만, 이렇게 되면 매 번 onClick을 호출할 때 마다 직접 카테고리를 함께 작성해주어야 하기 때문에 다소 불편하다

 

 

(2) Tips -2

newCategory의 값은 string이긴 한데 이렇게 적는 것 보다는,

const onClick=(newCategory:IToDo["category"])

라고 적는 것이 조금 더 낫다.

 

2) 2nd Method

지금부터 사용할 방법은 onClick함수를 그냥 할당하면 되는 방식이다. 즉 버튼마다 다른 parameter를 불러오지 않아도 된다.

function ToDo({text, category, id}:ITodo){
  const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
    const {currentTarget:{name}}=event;//same as event.currentTarget.name
  }
  return (
  <li>
    <span>{text}</span>
    {category!=="TO_DO"&&<button name={"TO_DO"} onClick={onClick}>todo</button>}
    {category!=="DOING"&&<button name={"DOING"} onClick={onClick}>doing</button>}
    {category!=="DONE"&&<button name={"DONE"} onClick={onClick)}>done</button>}
  </li>);
}

버튼에 name을 부여하고, 그 값을 onClick에서 끌어올 수 있다.

이 방법을 통해 학습할 점은 여러 개의 버튼 중 누굴 눌렀는 지 확인하기 위해서는

  1. name부여
  2. onClick에서 event.currentTarget.name 을 끌어오면 된다.

 

3) Sum

지금까지 어떤 버튼을 눌렀는지 확인이 된 상태다. 이제부터는 이 버튼을 실제로 바꿔주는 작업을 해주면 된다.

 

6.14 Immutability part One, Two

일단 눌렀을 때 지금 어떤 todo를 누른 것인지 알아야 한다.

이를 구체화 시키면 id로 배열을 탐색하는 것으로 이해할 수도 있지만, 누른 배열의index를 찾는 방법이라 해석하고 이를 알아보자.

index를 찾은 후 최종적으로 새로운 배열을 반환하기 까지의 여정은 다음과 같다

  1. index찾기
  2. 새로운 element만들기 (category만 바꾼 채)
  3. old Element → new Element로 바꿔주기

 

1) Find Index

const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
    const {currentTarget:{name}}=event;//same as event.currentTarget.name

    setToDos((oldToDos)=>{
      const targetIndex=oldToDos.findIndex(toDo=> toDo.id=== id)
      return oldToDos;
    })
 }
  1. oldToDos : 지금 우린 배열을 추가하는 과정을 보는 게 아니라 이미 있는 배열을 보고 진행하는 것이다. 따라서 oldToDos를 끌고 온다.
  2. targetIndex : 지금 찾고 있는 todo의 index를 찾을 것임.
    • toDo ⇒ toDo.id : 하나씩 돌아가면서 요소를 보고, 그들의 id를 비교해볼 것임
    • id : 저건 어디서 온 값이냐면
function ToDo({text, category, id}:ITodo){
	const onClick=(event:React.MouseEvent<HTMLButtonElement>)=>
	{ const {currentTarget:{name}}=event;//same as event.currentTarget.name } 
    
    return ( <li> </li>); 
}

 

우리가 현재 ToDo에서 움직이고 있으므로 이렇게 되는 것임. 그래서 toDo를 계속 돌려서 toDo.id === id 인 조건문이 참이되는 요소의 index를 targetIndex에 넣는 것이다.

 

 

정리하면,

버튼을 누른다 → 함수가 실행된다

그 함수는 → 자기가 속해있는 ToDo의 id를 기반으로 본인의 index를 array에서 찾아낸다.

⇒ 수정한다.

이걸 이해하는 데 중간에 조금 해멨는데,

2) Swtching Element

내가 기존에 생각했던 중간 요소 수정 방법에 대해 우선 정리해보려 한다.

(1) 삽질

내생각엔 지금 onClick함수에서 → name 즉 어떤 버튼을 눌렀는지를 확인해둔 상태이기 때문에, 일단

  1. 새로운 element를 만들어두고 → element(=객체)의 기존 property들은 그대로 받고, name property값만 지금 받은 놈으로 수정해서
  2. index를 이용하여 element를 대체해주기. (map을 이용하여, 가져온 놈의 index==currentIndex 인 경우 → 기존 todo ⇒new todo)

결국 index값을 이용하여 map으로 새로운 배열을 생성해주는 것을 기대한 것 같다.

(2) Example

간단한 예제 하나만 생각해보자.

  1. 만약 내가 저 가운데 4를 10으로 바꾸고 싶다고 가정하자
  2. the index of the element 4 is 3
conse arr=[1,2,3,4,5,6,7];

const front=arr.slice(0,3)//[1,2,3]
const back=arr.slice(3+1)//[5,6,7]
const newArr=[...front, 10, ...back]

이런식으로 수정하고 싶은 부분을 중심으로 앞뒤를 찢어서 앞 + new + 뒤 이런식으로 새로운 식을 만들면 수정된 배열을 만들 수 있다.

(3) Implement

const {currentTarget: { name },} = event;

setToDos((oldToDos) => {
  const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
  const newTodo = { text, id, category: name as any };// create new todo
  return [
    ...oldToDos.slice(0, targetIndex),
    newTodo,
    ...oldToDos.slice(targetIndex + 1),
  ];
}
  1. newTodo : text, id는 그대로 category는 내가 아까 가져온 name으로 바꿔주면 된다.

사실 이 단원을 공부하기 전에는 무언가 수정한다고 하면 내가 수정하고 싶은 부분만 꺼내어 수정한다고 생각했다. 하지만 실제로 수정을 하기 위해서는 수정 대상을 찾는 과정부터, 수정하고자 하는 부분에 접근하는 것 까지 꽤나 복잡할 수 있기 때문에,

그냥 새로운 요소를 만들어서 새로운 요소를 이용한 또다른 배열을 만드는 것이 더 효율적일 수도 있다.

6.16 Selectors part 1,2

0) Intro

지금부터는 selectors라는 기능을 알아보려고 한다. 지금까지 파악한 바에 의하면 selectors는 atom형태로 저장된 데이터를 내가 원하는 대로 수정하는 역할을 한다.

 

예를 들어 우리가 한 반의 학생들에 대한 정보가 담긴 파일이 있다고 가정하자. 그 정보에는 학생들의 나이, 키, 몸무게가 들어있다. 이렇게 모든 데이터가 원본 그대로 담겨있는 것이 atom이다.

 

그런데, 우리가 학생들 키의 분포를 알려면 ‘키’data만 배열로 모으면 될 것이고

몸무게의 분포만 알려면 ‘몸무게’data를 배열로 모아두면 될 것이다. 그런데, 두 가지 목적을 수행해야 하는데 원본 데이터를 수정할 수도 없는 상황이다.

 

 

 

따라서 원본 데이터를 수정하지 않고, 필요한 목적에 따라서 가공한 후 사용하는 것이 selectors의 역할이다.

1) Example

지금 코드를 확인해보면 현재 상태(todo, done, doing)에 상관없이 무작정 나열해서 보여주고 있다. 이 코드를 상태에 따라 분류하는 형태를 취하도록 하고 싶다.

위와 같이 나열하고 싶은 것이다.

(1) Selectors

//atom.tsx

export const toDoSelector= selector({
  key:"toDoSelector",
  get:({gettter})=>{}
});

selectors는 atom을 만들었을 때와 같이 함수 안에 객체를 함께 적어 호출한다.

이 객체 안에는 두 가지 요소가 필수적으로 들어가야 한다

  • key : atom에서와 마찬가지로 이 selector의 이름이자 설명이다.
  • get : getter함수를 할당하는 부분이다.

여기서 get값의 의미와, 함수의 특성을 알아보자.

get: ({ getter }) => {
  const toDos=getter(toDoState);
  return null;
},
  1. getter : get에 할당된 함수가 parameter로 받는 첫 번째 값은 getter함수다.
  2. getter(toDoState) : getter함수는 atom의 이름과 함께 호출하여 그 data를 끌고올 수 있다.

따라서 현재 toDos안에는 toDoState atom안에있을 값이 들어가있는 것이다. 

 

 

(2) approach

이렇게 만들어둔 selector는 useRecoilValue를 이용하여 접근할 수 있다.

const selelctorOutput=useRecoilValue(toDoSelector);

 

 

2) Modifying + displaying Data

지금부터는 [[array of todos], [array of doings], [array of dones]] 와 같이 한 배열 안에 세 개의 요소가 있고, 각각의 요소는 하나의 카테고리로 이루어진 배열이 오도록 selector를 조정하고 싶다.

 

(1) Modify(filter func)

return [
  toDos.filter((todo) => todo.category === "TO_DO"),
  toDos.filter((todo) => todo.category === "DOING"),
  toDos.filter((todo) => todo.category === "DONE"),
]
  1. toDos.filter : toDos배열을 대상으로 fileter함수를 적용할 것임
  2. todo ⇒ todo.category === “TO_DO” : 배열의 요소를 하나씩 꺼내와서 그 category가 TO_DO인 것들만 모아 새로운 배열을 만든다.

즉 새로운 배열을 만들고 싶으면, 조건문을 구성해서 filter함수를 사용해주는 방법이 있다.

 

(2) Display (nested map)

const categorizedTodos =useRecoilValue(toDoSelector);

const categories = ["TO_DO", "DOING", "DONE"];

function TodoList({ categorizedTodos }) {
  return (
    <div>
      {categories.map((category, index) => (
        <div key={category}>
          <h2>{category}</h2>
          <hr />
          {categorizedTodos[index].map((todo) => (
            <p key={todo.id}>{todo.title}</p>
          ))}
        </div>
      ))}
    </div>
  );
}
  1. categories.map : categories 3개를 중심으로 map을 돌린다.
  2. categorizedTodos[index].map : catogorizedTodos의 element(=각 카테고리별로 분류된 todo배열)을 중심으로 map을 돌린다.
((todo) => (<p key={todo.id}>{todo.title}</p>)) : todo를 받아서 그 값의 title이 보이도록 한다.

3) dividing

지금까지는 분류해둔 todo들을 한 화면에서 보여줬다. 지금부터는 selector-option을 사용하여 사용자가 고른 값에 따라 하나의 카테고리 할 일들만 보여주려고 한다.

(1) selector - option

<select onInput={onInput}>
  <option value="ToDo">TO DO</option>
  <option value="Doing">DOING</option>
  <option value="Done">DONE</option>
</select>

select를 만들어주고, option을 달아주었다. option은 이벤트를 감지하지 못하니 외곽에 있는 select에 onInput 함수를 달아주려고 한다.

(2) onInput

const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
    const current=event.target.value;
};

onInput함수에서 사용자가 누른 버튼의 값을 알 수있는 방법은 event.target.value이다.

지금부턴 category라는 atom을 만들어 전역적으로 사용해보려고 한다.

(3) Category atom

//atom.tsx
export const categoryState = atom<ITodo["category"]>({
  key: "category",
  default: "TO_DO",
})

이 atom은 현재 사용자가 어떤 페이지를 선택했는지 확인하기 위해 만들었다.

const [category, setCategory] = useRecoilState(categoryState);
const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
    const current=event.target.value;
  setCategory(current);
};

onInput함수에 setCategory를 이용하여 카테고리를 바꿀 수 있도록 해준다.

그리고 ToDo.tsx를 만들어서 ⇒ category별로 탭을 분리할 수도 있다.

{category === "TO_DO" && todo.map((todo) => <ToDo {...todo} />)}
{category === "DOING" && doing.map((todo) => <ToDo {...todo} />)}
{category === "DONE" && done.map((todo) => <ToDo {...todo} />)}

(4) Refactoring

현재는 cagory의 값에 따라서 → 어떤 배열의 map을 돌릴지(todo, doing, done) 결정한다. 그런데 애초에 todo에서 사용자가 선택한 하나의 카테고리를 가진 배열만 반환하면 어떨까?

즉 출력하는 곳에서 달리 출력하는 것이 아니라, 처음부터 data를 수정해서 만들어두자는 것이다.

따라서 selector를 이용해서 만들었던 todoState의 값을

이런 방식으로 만들어야 한다. category atom안에 들어있는 값을 확인한 다음에, filter를 사용해서 값을 변경하는 것이다. 그렇다면 category변수의 목적은 ‘분류’가 아닌, ‘선택에 따른 결과’가 될 것이다.

get: ({ get }) => {
  const toDos = get(toDoState);
  const category = get(categoryState);
  return toDos.filter((toDo) => toDo.category === category);
}

4) Sum

흐름을 정리하면

  1. selector에서 원하는 state의 option을 선택
  2. 선택에 따라 category atom의 값이 변경
  3. 변경된 atom의 값에 따라 아래 보이는 투두 리스트 수정

이렇게 되는 것이다. 여기서는 selector를 사용하는 방법을 아는 것도 중요하지만, 어떤 상황에서 selector를 사용하는지에 집중하여 복습해야 한다.

6.18 Enums

enum은 custom type이라고 생각하면 편할 것 같다. 우린 1,2,10,100이것들 모두를 number라는 데이터타입으로 인식한다. type과 다르게 enum은 구체적으로 어떤 값들을(값의 그룹을) type으로 지정할 것인지 결정한다.

enum이 왜 필요한지 생각해보자.

//todoList.tsx

function ToDoList() {
  const todos = useRecoilValue(toDoSelector);
  const [category, setCategory] = useRecoilState(categoryState);
  const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
    setCategory(category);
  };
  return (
    <div>
      <select value={category} onInput={onInput}>
        <option value="ToDo">TO DO</option>
        <option value="Doing">DOING</option>
        <option value="Done">DONE</option>
      </select>
      <CreateToDo />
      {todos.map((todo) => (
        <ToDo {...todo} />
      ))}
    </div>
  );
}

export default ToDoList;

현재 todoList의 코드다. 이 때 Ts는 value에 있는 값이 단순히 문자열이라고만 인식하지, 이게 모두

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

todo 인터페이스의 카테고리 값들이라고 인지하지 못하기 때문에 귀찮은 일들이 발생한다 (매번 모든 type들을 설명해줘야 함)
그래서 이를 개선하기 위해 enum을 선언해보려고 한다.

// api.ts
export enum Categories{
  "TO_DO",
  "DOING",
  "DONE"
}
export const categoryState = atom<Categories>({
  key: "category",
  default: "TO_DO",
});

저게 에러가 뜰 것이다. 저길 수정하는 법도 확인해보자

default: "TO_DO", -> default: Categories.TO_DO,

enum은 그냥 사용해야 하는 이유와, 선언 방법 → 불러오는 방법 이렇게만 알고 있으면 될 거 같다.