StudyForYou / ouahhan-typescript-with-react

우아한 타입스크립트 with 리액트 스터디 레포 🧵
4 stars 0 forks source link

#31 [10장_1] 다음 컴포넌트에서 상태 관리 가이드 법칙을 위반한 곳이 있다면 알려주세요. 그리고 타입스크립트를 적용하면서 이를 리팩토링 해주세요! #49

Open drizzle96 opened 2 months ago

drizzle96 commented 2 months ago

❓문제

다음 컴포넌트에서 상태 관리 가이드 법칙을 위반한 곳이 있다면 알려주세요. 그리고 타입스크립트를 적용하면서 이를 리팩토링 해주세요!

import { useState } from 'react'

const TodoList = () => {
  const [tasks, setTasks] = useState([])
  const [completedTasks, setCompletedTasks] = useState([])

  const addTask = (task) => {
    setTasks([...tasks, task])
  }

  const completeTask = (index) => {
    const task = tasks[index]
    setTasks(tasks.filter((_, i) => i !== index))
    setCompletedTasks([...completedTasks, task])
  }

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {tasks.map((task, index) => (
          <li key={index}>
            {task}
            <button onClick={() => completeTask(index)}>Complete</button>
          </li>
        ))}
      </ul>
      <div>
        <h2>Completed Tasks</h2>
        <ul>
          {completedTasks.map((task, index) => (
            <li key={index}>{task}</li>
          ))}
        </ul>
      </div>
      <input
        type="text"
        placeholder="New Task"
        onKeyDown={(e) => {
          if (e.key === 'Enter') addTask(e.target.value)
        }}
      />
    </div>
  )
}

export default TodoList

🎯답변

qooktree1 commented 1 month ago
  1. input에 변경되는 값을 value로 지정하기 위해 새로운 newState를 useState로 관리
  2. useState를 사용하며 setter 함수를 사용할 때 정확한 이전의 기존 state 값을 사용하기 위해 callback을 사용한다.
  3. onKeyDown의 이벤트의 타입을 명시해준다.
import { useState } from 'react'

const TodoList = () => {
  const [tasks, setTasks] = useState<string[]>([])
  const [completedTasks, setCompletedTasks] = useState<string[]>([])
  const [newTask, setNewTask] = useState('');

  const addTask = (task: string) => {
    setTasks((prev) => ([...prev, task]));
  }

  const completeTask = (index: number) => {
    const task = tasks[index]
    setTasks(prev => prev.filter((_, i) => i !== index))
    setCompletedTasks(prev => [...prev, task])
  }

  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
        addTask(newTask);
        setNewTask('');
    }
  }

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {tasks.map((task, index) => (
          <li key={index}>
            {task}
            <button onClick={() => completeTask(index)}>Complete</button>
          </li>
        ))}
      </ul>
      <div>
        <h2>Completed Tasks</h2>
        <ul>
          {completedTasks.map((task, index) => (
            <li key={index}>{task}</li>
          ))}
        </ul>
      </div>
      <input
        type="text"
        placeholder="New Task"
        onChange={(e) => setNewTask(e.target.value)}
        onKeyDown={handleKeyDown}
      />
    </div>
  )
}

export default TodoList
drizzle96 commented 1 month ago

[상태를 잘 관리하기 위한 가이드]

[상태 관리 법칙을 위반한 곳]

현재 TodoList는 taskscompletedTasks를 상태 값으로 가지고 있습니다. 이 때 completeTask 함수를 보면, 어떤 task가 완료될 때 tasks는 완료된 task를 filter하여 업데이트 되고, completedTasks는 완료된 task를 배열에 추가하여 업데이트하고 있습니다.

즉 두 상태는 서로 연관된 계산으로 업데이트되는데 이는 위 가이드 중 2번 째를 위반한 것입니다. 각 상태는 서로 다른 출처에서 파생되어야 합니다. 각 상태의 출처가 서로 연관되어서는 안됩니다. 같은 출처에서 파생된 값들을 서로 다른 상태로 관리하는 식이 되면 데이터의 정확성과 일관성을 보장하기 어렵습니다.

코드에서 tasks는 "완료되지 않은 일", completedTasks는 "완료된 일"을 의미합니다. 둘을 같은 출처에서 관리하기 위해 "모든 일"을 의미하는 tasks를 상태로 만들고, 두 값은 (새로 만든) tasks 상태로부터 값을 계산하여 변수로 담는 식으로 개선할 수 있습니다. 추가로 Task 타입을 만들어 tasks 상태 값의 타입을 Task[]로 지정해줍니다.

[추가 개선]

[개선 코드]

import { useState } from 'react'

type Task = {
  name: string
  completed: boolean
}

const TodoList = () => {
  const [tasks, setTasks] = useState<Task[]>([])
  const [newTask, setNewTask] = useState('')
  const incompletedTasks = tasks.filter((task) => !task.completed)
  const completedTasks = tasks.filter((task) => task.completed)

  const addTask = (name: string) => {
    setTasks((prevTasks) => [...prevTasks, { name, completed: false }])
  }

  const completeTask = (index: number) => {
    setTasks((prevTasks) => prevTasks.map((task, i) => (i === index ? { ...task, completed: true } : task)))
  }

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      addTask(newTask)
      setNewTask('')
    }
  }

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {incompletedTasks.map((task, index) => (
          <li key={`${index}-${task.name}`}>
            {task.name}
            <button onClick={() => completeTask(index)}>Complete</button>
          </li>
        ))}
      </ul>
      <div>
        <h2>Completed Tasks</h2>
        <ul>
          {completedTasks.map((task, index) => (
            <li key={`${index}-${task.name}`}>{task.name}</li>
          ))}
        </ul>
      </div>
      <input
        type="text"
        placeholder="New Task"
        value={newTask}
        onChange={(e) => setNewTask(e.target.value)}
        onKeyDown={handleKeyDown}
      />
    </div>
  )
}

export default TodoList