개발새발개발/React

[React+Typescript] Todos 앱 만들기

birdsfoot 2025. 2. 11.

 

Todos 앱 만들기

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';


const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

 

 

 

App.tsx

import { useRef, useState, useEffect } from 'react';
import { Todo } from './Type';
import './App.css';

import Editor from './components/Editor';
import TodoItem from './components/TodoItem';


function App() {

  const [todos, setTodos] = useState<Todo[]>([])


  const idRef = useRef(1)

  const onClickAdd = (text:string) => {
    setTodos([
      ...todos,
      {
        id : idRef.current++,
        content : text
      },
    ])
  }

  const onClickDelete = (id : number) => {
    setTodos(todos.filter((todo)=> todo.id !== id))
  }

useEffect(()=> {
  console.log(todos)
},[todos])

  return (
    <div className='App'>
      <h1>Todo</h1>
      <Editor onClickAdd = {onClickAdd}/>
      <div>
        {todos.map((todo)=> (
          <TodoItem key={todo.id} {...todo} onClickDelete={onClickDelete}/>
        ))}
      </div>
    </div>
  );
}

export default App;

 

 

 

 

Editor.tsx

import { useState } from "react"

interface Props {
    onClickAdd : (text: string) => void
}

export default function Editor(props:Props) {

  const [text, setText] = useState("")

  const onChangeInput = (e:React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value)
  }

  const onClickButton = () => {
    props.onClickAdd(text)
    setText("")
  }

    return (
    <div>
      <input type="text" value={text} onChange={onChangeInput}/>
      <button onClick={onClickButton}>추가</button>
    </div>
    )
}

 

 

자세히보기

 

  const [todos, setTodos] = useState<Todo[]>([])

- todo를 담을 todo 배열

 

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

- Ctrl을 눌러 들어가보면 제네릭인걸 확인할 수 있다

 

  const idRef = useRef(1)

- id 값에 사용할 useRef (1번부터 시작)

 

 

추가 및 삭제

  // todo 추가
  const onClickAdd = (text:string) => {
    setTodos([
      ...todos,  // 이전의 todos는 유지하고, 아래 새로운 todo를 추가
      {
        id : idRef.current++,  // 현재값을 1씩 증가
        content : text
      },
    ])
  }


// todo 삭제
  const onClickDelete = (id : number) => {
    setTodos(todos.filter((todo)=> todo.id !== id))
  } // 삭제하려는 id와 다른 todo를 담은 리스트 반환

 

 

변경사항 감지

useEffect(()=> {
  console.log(todos)
},[todos])

 

 

렌더링

  return (
    <div className='App'>
      <h1>Todo</h1>
      <Editor onClickAdd = {onClickAdd}/>
      <div>
        {todos.map((todo)=> (
          <TodoItem key={todo.id} {...todo} onClickDelete={onClickDelete}/>
        ))}
      </div>
    </div>
  );

 

- map을 사용하여 todo를 하나씩 TodoItem에 props함

- todo의 프로퍼티와 삭제 버튼에 대한 함수를 props

 

Type.ts

export interface Todo {
    id : number;
    content : string;
  }

 

- Todo는 여러 컴포넌트에서 사용할 interface이므로 Type.ts에 따로 생성

 

import { Todo } from './Type';

 

- 다른 곳에서 사용할 땐 위와 같이 import 하면 됨

 

 

 

TodoItem.tsx

import { Todo } from "../Type";

interface Props extends Todo {
    onClickDelete : (id: number) => void
}


export default function TodoItem (props : Props) {
    const onClickButton = () => {
        props.onClickDelete(props.id)
    }
        
    

    return (
        <div>
            {props.id}번 : {props.content}
            <button onClick={onClickButton}>삭제</button>
        </div>
    )
}

 

 

 

자세히보기

interface Props extends Todo { }

export default function TodoItem (props : Props) {}    

    return (
        <div>
            {props.id}번 : {props.content}
            <button onClick={onClickButton}>삭제</button>
        </div>
    )
}

- 값을 추가하기 편하도록 Todo 를 extends한 Props 생성하여 props에 적용

- id와 content 렌더링

- 삭제 버튼 생성

- 삭제 버튼을 누르면 onClickButton 이벤트 시행

 

interface Props extends Todo {
    onClickDelete : (id: number) => void
}


export default function TodoItem (props : Props) {
    const onClickButton = () => {
        props.onClickDelete(props.id)
    }

 

- App.tsx에서 onClickDelete 위에 마우스 올리면 `(id: number) => void` 힌트 얻을 수 있음

- Props에 onClickDelete 이벤트핸들러 추가하고 힌트로 얻은 타입 넣어주기

- onClickButton 은 Props로 받아온  onClickDelete를 시행하는 함수

- onClickDelete 본체는 App.tsx에 있음

 

 

 

Reducer 사용하기

App.tsx

import { useRef, useState, useEffect, useReducer } from 'react';
import { Todo } from './Type';
import './App.css';

import Editor from './components/Editor';
import TodoItem from './components/TodoItem';
import { stat } from 'fs';

type Action = {
  type : "CREATE",
  data : {
    id : number,
    content : string,
  }
} | {
  type : "DELETE", 
  id : number
}

function reducer (state:Todo[], action : Action ) {
  switch(action.type) {
    case 'CREATE' : {
      return [...state, action.data]
    }
    case 'DELETE' : {
      return state.filter((it)=> it.id !== action.id)
    }
  }
}

function App() {

  const [todos, dispatch] = useReducer(reducer,[])


  const idRef = useRef(1)

  const onClickAdd = (text:string) => {
    dispatch({
      type:"CREATE",
      data : {
        id : idRef.current++,
        content : text
      }
    })
  }

  const onClickDelete = (id : number) => {  
    dispatch({
      type:"DELETE",
      id : id,
    })
  
  }


useEffect(()=> {
  console.log(todos)
},[todos])

  return (
    <div className='App'>
      <h1>Todo</h1>
      <Editor onClickAdd = {onClickAdd}/>
      <div>
        {todos.map((todo)=> (
          <TodoItem key={todo.id} {...todo} onClickDelete={onClickDelete}/>
        ))}
      </div>
    </div>
  );
}

export default App;

 

- reducer를 사용하면 타입스크립트의 장점을 잘 활용할 수 있음(오류나면 바로 알려주니까!)

 

 

 

자세히보기

  const [todos, dispatch] = useReducer(reducer,[])

- setTodos를 dispatch로 바꾸고 useState도 useReducer로 변경, reducer와 [] 로 변경

 

reducer 정의

function reducer (state:Todo[], action : Action ) {}

- state는 Todo배열로 action은 Action 타입으로 설정

 

type Action = {
  type : "CREATE",
  data : {
    id : number,
    content : string,
  }
} | {
  type : "DELETE", 
  id : number
}

 

- Action 타입은 CREATE와 DELETE 타입을 같은 유니온 타입

 

function reducer (state:Todo[], action : Action ) {
  switch(action.type) {
    case 'CREATE' : {
      return [...state, action.data]
    }
    case 'DELETE' : {
      return state.filter((it)=> it.id !== action.id)
    }
  }
}

 

- CREATE일때는 기존 todos 에  새로운 값을 추가하여 반환

- DELETE일 때는 id 값이 일치하지 않는 todo만 배열에 담아서 반환

 

 

  const onClickAdd = (text:string) => {
    dispatch({
      type:"CREATE",
      data : {
        id : idRef.current++,
        content : text
      }
    })
  }

  const onClickDelete = (id : number) => {  
    dispatch({
      type:"DELETE",
      id : id,
    })
  
  }

- 이벤트 핸들러도 타입에 따라 변경해줌

 

 

결과

댓글