프레임워크\라이브러리/React

1113 리액트로 Todolist 만들기 - useState 활용

thinktank911 2025. 11. 13. 00:56

리액트에서 컴포넌트 만드는 방식

클래스형 컴포넌트

  • render() 메소드 선언 : 화면 반환하겠다는 뜻
import { Component, ReactNode } from "react";

// 컴포넌트 클래스 상속받기
class ClassCom extends Component{
    // render 메소드 선언 : 화면 반환하겠다.
    render(): ReactNode {
        return (
            <div>
                클래스형 컴포넌트
            </div>
        )
    }
}

// 다른 파일에서 컴포넌트 사용하기 위해 외부 export
export default ClassCom;
import ClassCom from './ClassCom';

function App() {
  return (
    // 최상위 부모 태그가 반드시 있어야 <></>
    <div className="container">
      <ClassCom></ClassCom>
    </div>
  );

 

함수형 컴포넌트

  • 가독성, 코드 형식 간단함
  • 리턴 시 랜더링된다.
import React from "react";  // 리액트 모듈 임포트하기

function FuncCom(){
    // 바로 리턴하면 랜더링됨
    return (
        <div>
            함수형 컴포넌트
        </div>
    )
}

export default FuncCom;

state 사용하기

useState 훅 정의와 사용법

  • 데이터를 동적으로 감시해서 변경되면 즉시 화면 반영
  • [데이터, 변경함수] = useState('초기값');
    ➡️ 객체지향 철학 중 캡슐화 철학에 의해 데이터는 선언된 변경함수로 접근 및 변경할 수 있다.
  • 배열 데이터 뿌려주기
import React, { useState } from "react";

// React.FC : 펑션 컨포넌트 약자. props의 타입을 명시해주는 기능
// 코드를 명확하고 가독성있게 해준다.
const TodoList : React.FC = () => {
    const title : string = "오늘 할 일";
    // state는 데이터를 동적으로 감시해서 변경되면 즉시 화면 반영
    // 구조분해할당
    const [todos, setTodos] = useState<string[]>(['공부하기', '잠자기', '미팅하기']);

    return (
        <div>
            <h1>{title}</h1>
            <p></p>
            <div className="container">
                <div className="board">
                    <ul>
                        <li>{todos[0]}</li>
                        <li>{todos[1]}</li>
                        <li>{todos[2]}</li>
                    </ul>
                </div>
            </div>
        </div>
    )
}

export default TodoList;

 

※ React.FC : 펑션 컨포넌트 약자. props의 타입을 명시해주는 기능

 


데이터 반복 처리하기 - Map 함수 사용

직접 타입 만들기

  • type으로 사용자 정의 타입 지정
// 사용자정의 타입 지정
type Todo = {
    id : number;
    text : string;
    isChecked : boolean;
};

 

Map 사용하기

  • key 할당 : 동적으로 생성된 엘리먼트의 변화를 효율적으로 추적하고 관리하기 위해 고유 식별자를 지정해준다.
const [todos, setTodos] = useState<Todo[]>([
    {id : 1, text : '공부하기', isChecked : false}, 
    {id : 2, text : '잠자기', isChecked : false}, 
    {id : 3, text : '미팅하기', isChecked : false}
]);

return (
    <div>
        <h1>{title}</h1>
        <p></p>
        <div className="container">
            <div className="board">
                <ul>
                    {
                        todos.map((todo, idx)=>(
                            <li key={idx}>{todo.text}</li>
                        ))
                    }

 

체크박스 기능 추가

  • 체크박스 UI에 onChange이벤트 추가
<input type="checkbox" onChange={()=>{
    handleCheckedChange(todo.id);
}} />
  • set함수 이용해 이벤트 함수 정의
    • setTodos의 이전 값들을 prevItems로 정의하고 map 돌려서 id값을 넘겨준 id값과 비교해 일치하는 값을 찾는다. 
    • 체크 시 데이터 isChecked가 토글되도록정의한다.
      const handleCheckedChange = (itemId : number) => {
          setTodos((prevItems)=>
            prevItems.map((item) => 
                item.id === itemId ? {...item, isChecked : !item.isChecked} : item
            )
          )
      }
  • 체크박스 체크 시 취소 표시선 표시되도록 UI 구현
    • isChecked 데이터 값으로 삼항연산자 조건에 의해 UI 변화
      <span>
      {
        todo.isChecked ? 
        <del>{todo.text}</del>
        : <span>{todo.text}</span>
      }
      

새로운 게시물 추가하기

부트스트랩 추가

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
  integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"
  crossorigin="anonymous"
/>

<title>React App</title>

 

새로운 게시물 추가

  • 새 todo 넣기 useState 선언
// 새 todo 넣기
const [newTodo, setNewTodo] = useState<string>('');
  • todo 추가 함수 정의
// todo 추가 함수
const addTodo = () => {
    if(newTodo.trim() !== ''){
        setTodos([...todos, {id : Date.now(), text : newTodo, isChecked : false}]);
        setNewTodo(''); // 입력부 비워주기
    }
}
  • 새 todo 입력 후 추가하는 ui 생성 및 함수 셋팅
<input type="text" 
    placeholder="할일 입력"
    style={{marginRight : '10px', writingMode : 'horizontal-tb'}}
    onChange={(e)=>setNewTodo(e.target.value)}  // newTodo 데이터에 담기
/>
<Button variant="warning" onClick={addTodo}>추가</Button>
  • 스프레드 연산자 이용
    • ...은 배열의 새로운 복사본을 만들고 무장해제
    • 이것을 이해하려면 참조형에 대해 이해해야 한다.
    • 독립적인 메모리 공간이 생김
      ➡️ 깊은 복사 (주소값이 다름)
[이슈]
- 문제상황 : 입력부 비워주는 기능으로 setNewTodo('')를 넣었는데 입력부가 안 비워진다.
- 원인파악 : <input> 요소가 제어 컴포넌트가 아니기 때문. 즉, value 속성이 상태(newTodo)와 연결되어 있지 않아서
                React가 입력값을 관리하지 못하는 상태
- 해결방안 : input에 value 속성을 {newTodo}로 추가해 연결한다.

 

※ 추가 설명
1) 일반 HTML input은 스스로 값을 관리한다.
HTML에서 <input>은 본래 자기 자신이 값을 기억하는 DOM 요소. 사용자가 입력하면 브라우저가 내부적으로 값을 기억하고 표시한다. 즉, React가 없어도 DOM 자체가 값(state)을 가지고 있는 구조.
2) React는 상태(state)를 기준으로 화면을 그린다.
상태가 바뀔 때마다 화면을 다시 그린다.
3) 제어 컴포넌트란 React의 state가 입력값(value)을 직접 관리하는 input을 말한다. 즉, input의 value가 React state와 완전히 동기화되어 있는 구조. input은 React가 시키는 대로만 값이 바뀐다. React가 완전히 input을 제어한다고 해서 제어 컴포넌트라고 부른다.

 


시계 추가하기

setInterval(콜백함수, 시간주기) 활용

  • 타이머 만들기
import { useState } from "react";

const Timer : React.FC = () => {
    const [seconds, setSeconds] = useState<number>(0);

    return(
        <div>
            <h1>타이머 : {seconds}초</h1>
            <button onClick={
                ()=>{
                    setInterval(()=>{
                        setSeconds((prev)=> prev + 1);
                    }, 1000)
                }
            }>시작</button>
        </div>
    )
}

export default Timer;
  • 현재 시간 추가
// 현재 시간 추가
const Clock : React.FC = () => {
    const [time, setTime] = useState(new Date());

    setInterval(()=>{
        setTime(new Date());
    }, 1000);

    return(
        <div>
            현재 시간 : {time.toLocaleTimeString()}
        </div>
    )
}

export default Clock;

게시물 삭제하기

// 게시물 삭제
const removeTodo = (id : number) => {
    setTodos(todos.filter((todo)=> todo.id !== id))
};

<button
    onClick={()=>removeTodo(todo.id)}
    className="delbutton"
>
    삭제
</button>

상세정보 모달창 구현

// 상세정보 보임 여부
const [showDetail, setShowDetail] = useState<boolean>(false);
// 선택된 todo
const [selectedTodo, setSelectedTodo] = useState<Todo | null>(null);

// 상세정보 보기
const handleTodoClick = (todo : Todo) => {
    setShowDetail(true);
    setSelectedTodo(todo);
}

// 상세정보 창 닫기
const handleCloseDetail = (todo : Todo) => {
    setShowDetail(false);
}

<span onClick={()=>handleTodoClick(todo)}>
    {
        todo.isChecked ? 
        <del>{todo.text}</del>
        : <span>{todo.text}</span>
    } 
</span>