개발/React&Redux

[React] React로 Todo List 만들기

xuwon 2024. 11. 6. 03:34

JavaScript로 Todo List를 만들었으니, 이번엔 React로 만들어 봤습니다.

완성본입니다.

- 기본적인 TodoList 기능 (추가, 삭제, 수정, 전체삭제)
- 랜덤 명언 표시
- 스톱워치 기능

이렇게 기능을 구현하였습니다.

음악 재생은 모양만 내봤습니다... ㅠㅠ
(제가 좋아하는 노래들이에요)

// main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import Box from './Box.jsx'
import Music from './Music.jsx'

createRoot(document.getElementById('root')).render(
  <>
    <App />
  </>
)

createRoot(document.getElementById('box')).render(
  <>
    <Box />
  </>
)

createRoot(document.getElementById('music-box')).render(
  <>
    <Music />
  </>
)

우선 main.jsx 파일입니다.

Todo List를 표시할 App 컴포넌트, 스톱워치를 표시할 Box 컴포넌트(네이밍이...),
마지막으로 음악 재생 박스를 표시할 Music 컴포넌트로 구성되어 있습니다. 



App.jsx 파일 먼저 보도록 합시다.

 

Todo Input

 

function TodoInput({ setTodoLists, todoLists }) {
  const [inputValue, setInputValue] = useState('');

  return (
    <div className='todo-input'>
      <h1 className='todo-input-text'>
        TODO LIST
      </h1>
      <div className='todo-input-buttons'>
        <input
          value={inputValue}
          onChange={(event) => setInputValue(event.target.value)}
          className='todoInput'
          style={{ width: "270px" }}
        />
        <button onClick={() => {
          if (inputValue.trim()) {
            const newTodoList = { content: inputValue, isDone: false }
            setInputValue('') // 빈 값으로 초기화

            fetch("http://localhost:3001/todo", { // 아이디는 알아서 만들어줌
              method: "POST", // url에 데이터 보내기
              body: JSON.stringify(newTodoList)
            }).then(res => res.json())
              .then(res => setTodoLists((prev) => [...prev, res]))
          }
        }}
        className='main-button'
        >ADD</button>
        <button onClick={() => {
          Promise.all(todoLists.map(todo =>
            fetch(`http://localhost:3001/todo/${todo.id}`, {
              method: "DELETE"
            })
          )).then(() => {
            // 상태를 비워서 전체 삭제된 상태를 반영
            setTodoLists([]);
          });
        }}
        className='main-button'
        >DELETE</button>
      </div>
    </div>
  )
}

요 부분입니다!

 

1. 목록 입력창

  const [inputValuesetInputValue= useState('');  
우선 input 창에 입력하는 값은 useState로 관리해 주었습니다.

  onChange={(event=> setInputValue(event.target.value)}  
그리고 change 이벤트가 발생할 때마다, inputValueevent.target.value로 업데이트 해주었습니다.

 

2. ADD 버튼


ADD 버튼에는 onClick을 사용해서 click 이벤트가 발생했을 때 리스트에 항목을 추가하도록 넣었습니다.

그 전에, 저는 todoList를 서버로 관리하기 위해서 json server를 사용했습니다!

const [todoLists, setTodoLists] = useState([]);

useEffect(() => { // 서버에서 데이터 받아오기 
  fetch("http://localhost:3001/todo")
   .then(res => res.json())
   .then(res => setTodoLists(res))
}, [])

db.json 파일을 3001 port에서 열어주고, 그 안에서 todo를 불러왔습니다.

fetch 요청으로 JSON 서버(http://localhost:3001/todo)에서 데이터를 받아오고,
이 데이터를 todoLists에 업데이트합니다.

그리고 처음 렌더링 시에만 데이터를 받아올 수 있도록 useEffect를 사용하고, 의존성 배열로 빈 배열을 추가해주었습니다.

 

그럼 ADD 버튼을 다시 보겠습니다.

<button onClick={() => {
  if (inputValue.trim()) {
    const newTodoList = { content: inputValue, isDone: false };
    setInputValue('');

    fetch("http://localhost:3001/todo", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(newTodoList)
    })
    .then(res => res.json())
    .then(res => setTodoLists((prev) => [...prev, res]));
  }
}} className="main-button">ADD</button>

우선 입력된 값(inputValue)의 양쪽 공백을 없앴을 때(trim()) 빈 값이 아닐 때만 값이 추가되도록 조건문을 걸어주었고,

newTodoList를 생성해서 content 속성엔 입력된 값을, 그리고 isDone 속성엔 false를 주어 객체를 만들었습니다.
ADD 버튼이 클릭된 후에는 input창도 초기화 시켜줬습니다.

이제 이 newTodoList를 아까 언급한 서버에 보내주면 됩니다.

fetch에 첫번째 매개변수로 똑같이 주소를 넣고, 두번째 매개변수로 객체를 전달합니다.
객체에는 method 속성과 body 속성이 있습니다!

데이터를 url로 보내야 하므로 POSTmethod로 주고,
body에는 newTodoListJSON 형태로 변환하여 넣어줍니다.

그리고 해당 데이터를 받아와서, setTodoLists로 업데이트 해주면 UI 업데이트까지 끝입니다.

  setTodoLists((prev) => [...prev, res])  
원래 있던 배열을 spread 연산자로 가져와서, 그 뒤에 res를 넣어주면 됩니다.

 

3. DELETE 버튼

다음은 DELETE 버튼입니다.

<button onClick={() => {
  Promise.all(todoLists.map(todo =>
    fetch(`http://localhost:3001/todo/${todo.id}`, {
      method: "DELETE"
    })
  )).then(() => {
    setTodoLists([]); // 상태를 비워서 전체 삭제된 상태를 반영
  });
}} className="main-button">DELETE</button>

DELETE 버튼을 클릭했을 때 todoLists에 있는 모든 할 일 항목을 서버에서 삭제하고,
todoLists 상태를 비워 전체 목록을 업데이트합니다.

Promise.all()은 여러 개의 비동기 작업을 병렬로 실행하고,
모든 작업이 완료될 때까지 기다린 다음 .then()으로 넘어갑니다.

todoLists.map()을 사용해 todoLists 배열의 각 항목에 대해 fetch를 호출하여 삭제 요청을 보내면 됩니다.

fetch 요청에서 URL은 각 항목의 id를 포함하여 http://localhost:3001/todo/${todo.id}로 설정되고,
이 요청은 REST API에서 특정 항목을 삭제하는 DELETE 요청으로 처리됩니다.
이렇게 되면 각 항목들이 json server에서 삭제되겠죠.

 

모든 DELETE 요청이 완료되면, .then 콜백이 실행됩니다!

그리고
setTodoLists([])todoLists 상태를 빈 배열로 설정하여, 화면에서 모든 항목이 삭제된 상태를 UI에 표시해 줍니다.

이렇게 되면 전체 삭제 버튼도 구현이 완료됩니다. :)

 


다음으로 Todo List를 보여주는 부분을 보겠습니다.

 

TodoList

 

function TodoList({ todoLists, setTodoLists }) {
  return (
    <ul className='todoList'>
      {todoLists.map((todoList) => <Todo
        key={todoList.id}
        todoList={todoList}
        todoLists={todoLists}
        setTodoLists={setTodoLists} />
      )}
    </ul>
  )
}

TodoList 컴포넌트는 todoLists 배열을 순회하며 각 항목에 대해 Todo 컴포넌트를 렌더링합니다.

 

Todo
function Todo({ todoLists, todoList, setTodoLists }) {
  const [isDone, setIsDone] = useState(false);
  const [isUpdate, setIsUpdate] = useState(false); 

  const doneTodo = () => {
    setIsDone(!isDone);

    const incompletedTodoList = (todoLists.filter((el) => el.id !== todoList.id)); 
    // 완료되지 않은 리스트
    const completedTodo = { 
    	id: todoList.id, 
        content: todoList.content, 
        isDone: !todoList.isDone 
    }; 
    // 완료된 항목

    setTodoLists([...incompletedTodoList, completedTodo])
  };

  useEffect(() => {
    const incompletedTodoList = todoLists.filter((el) => !el.isDone);
    const completedTodoList = todoLists.filter((el) => el.isDone);

    // 기존 상태와 비교하여 리스트가 변경되었을 때만 상태 업데이트
    const newTodoLists = [...incompletedTodoList, ...completedTodoList];

    // 상태가 변경된 경우에만 업데이트 (불필요한 업데이트 방지)
    if (JSON.stringify(newTodoLists) !== JSON.stringify(todoLists)) {
      setTodoLists(newTodoLists);
    }
  }, [todoLists, setTodoLists]);

  const deleteTodo = () => {
    fetch(`http://localhost:3001/todo/${todoList.id}`, { 
      method: "DELETE", // url에 데이터 보내기
    }).then((res) => res.json());

    setTodoLists(todoLists.filter((el) => el.id !== todoList.id))
  }


  return (
    <div className='todo'>
      <li style={{ listStyle: "none", display: "flex" }}>
        <div className={`${isDone ? "done" : ""}`} onClick={() => setIsUpdate(true)}>
          {isUpdate ?
            <UpdateTodo 
            	todoList={todoList} 
                todoLists={todoLists} 
                setTodoLists={setTodoLists} 
                setIsUpdate={setIsUpdate}
            /> 
            : (todoList.content)}
        </div>
        <div style={{ display: "flex", gap: "10px" }}>
          <button onClick={() => { doneTodo() }} className={isDone ? "done-button" : ""}>
          	Done
          </button>
          <button onClick={() => { deleteTodo() }}>Delete</button>
        </div>
      </li>
    </div>
  )
}

 

 

 

Todo 컴포넌트는 각 할 일 항목을 렌더링하고, 항목을 완료로 표시하거나 삭제하는 등의 기능을 제공합니다.


  const [isDone, setIsDone] = useState(false);  
  const [isUpdate, setIsUpdate] = useState(false);  

먼저 할 일이 완료 상태인지 아닌지를 관리하는 isDone과, 수정 중인지 아닌지를 관리하는 isUpdate를 추가합니다.

 

1. doneTodo

const doneTodo = () => {
  setIsDone(!isDone);

  const incompletedTodoList = (todoLists.filter((el) => el.id !== todoList.id)); 
  // 완료되지 않은 리스트
  const completedTodo = { id: todoList.id, content: todoList.content, isDone: !todoList.isDone }; 
  // 완료된 항목

  setTodoLists([...incompletedTodoList, completedTodo])
};

할 일 목록을 완료/미완료 상태로 토글하는 함수입니다.
  todoLists.filter((el) => el.id !== todoList.id)  

filter 메소드를 사용해서 원래 있던 투두리스트들의 id와 해당 항목의 id를 비교하여 일치하지 않는 것들만 return 합니다.
즉, 완료되지 않은 항목을 incompletedTodoList에 배열로 담는 것입니다.

그리고 완료된 항목은 completedTodo에 객체 형태로 따로 담아줍니다.

마지막으로
  setTodoLists([...incompletedTodoList, completedTodo])  

TodoList를 업데이트하면 끝입니다!
저는 완료된 항목들을 맨 밑으로 빼기 위해 이렇게 진행하였습니다.

 

2. 리스트 상태 업데이트

useEffect(() => {
    const incompletedTodoList = todoLists.filter((el) => !el.isDone);
    const completedTodoList = todoLists.filter((el) => el.isDone);

    // 기존 상태와 비교하여 리스트가 변경되었을 때만 상태 업데이트
    const newTodoLists = [...incompletedTodoList, ...completedTodoList];

    // 상태가 변경된 경우에만 업데이트 (불필요한 업데이트 방지)
    if (JSON.stringify(newTodoLists) !== JSON.stringify(todoLists)) {
      setTodoLists(newTodoLists);
    }
}, [todoLists, setTodoLists]);

useEffect는 완료된 항목이 다시 미완료 항목으로 들어갈 때를 대비하여 만들었습니다!
(미완료가 되면 다시 위로 올라가야 하기 때문입니다.)

isDone 속성을 활용해서 완료된 투두리스트와, 완료되지 않은 투두리스트로 구분합니다.

newTodoLists 배열을 생성하여 incompletedTodoListcompletedTodoList를 결합하고,
기존 todoLists와 비교하여 다를 경우에만 setTodoLists를 호출하여 불필요한 업데이트를 방지합니다.

JSON 형태로 변환하여 비교하는 이유는, 객체의 참조가 아닌 값을 비교하기 위해서입니다!!
isDone 속성의 값을 비교해야 하기 때문이죠.

 

그리고 useEffect의 의존성 배열에 [todoLists, setTodoLists]를 넣어 줬는데,
setTodoLists는 넣을 필요가 없다네요...

제가 이거 만들 당시에 todoLists만 의존성 배열로 줬을 때 어떤 문제가 발생했던 것 같은데 ㅠㅠ.


이렇게 되면 isDone에 의한 변경에도 유연하게 리스트가 변경되게 됩니다!

 

3. deleteTodo

const deleteTodo = () => {
    fetch(`http://localhost:3001/todo/${todoList.id}`, { 
      method: "DELETE", // url에 데이터 보내기
    }).then((res) => res.json());

    setTodoLists(todoLists.filter((el) => el.id !== todoList.id))
}

DELETE

버튼입니다.
fetch를 통해 각 항목에 DELETE 요청을 보내면 되는데,
URL todoList.id를 포함하여 특정 항목을 삭제하도록 서버에 요청합니다.

블로그를 작성하며 알게 된 사실인데,
DELETE 요청의 경우 서버에서 반환되는 데이터가 특별한 의미가 없을 수 있어서
res.json()을 호출하지 않아도 된다고 합니다.

res.json()은 서버 응답을 JSON 형태로 파싱하지만,
DELETE 요청에선 주로 응답 데이터가 없거나 간단한 메시지일 뿐이라네요.


아무튼.
이제 DELETE 버튼이 클릭된 항목을 제외한 나머지 todoLists들을 필터링하여 TodoLists에 업데이트 해주면 됩니다.

 


이렇게 하면 Todo가 끝이납니다.

 

UpdateTodo

function UpdateTodo({ todoList, todoLists, setTodoLists, setIsUpdate}) {
  const [updateTodoInput, setTodoUpdateInput] = useState(todoList.content);

  const modifyTodo = () => {
    const updatedTodos = todoLists.map((el) =>
      el.id === todoList.id ? { ...el, content: updateTodoInput } : el
    );
    const modifyTodos = updatedTodos.find((el) => el.id === todoList.id)

    fetch(`http://localhost:3001/todo/${todoList.id}`, { // 아이디는 알아서 만들어줌
      method: "PUT", // url에 데이터 보내기
      body: JSON.stringify(modifyTodos)
    }).then((res) => res.json());

    setTodoLists(updatedTodos);  // 상태 업데이트 후 리렌더링 발생
    setIsUpdate(false);  // 수정 모드 종료
  };

  return (
    <div style={{
      display: 'flex',
      alignItems: 'center'
    }}>
      <input
        value={updateTodoInput}
        className='modifyInput'
        onChange={(event) => setTodoUpdateInput(event.target.value)}
      />
      <button onClick={(event) => {
        modifyTodo();
        event.stopPropagation(); // 부모 요소의 클릭 이벤트 막기
        setIsUpdate(false)
      }}>Modify</button>
    </div>
  )
}

텍스트를 클릭하면 나오는 입력창과 수정 버튼입니다.

  const [updateTodoInput, setTodoUpdateInput] = useState(todoList.content);  
우선 이 새로운 입력창 역시 useState로 상태를 관리합니다.

 

1. modifyTodo

 

const modifyTodo = () => {
    const updatedTodos = todoLists.map((el) =>
      el.id === todoList.id ? { ...el, content: updateTodoInput } : el
    );
    const modifyTodos = updatedTodos.find((el) => el.id === todoList.id)

    fetch(`http://localhost:3001/todo/${todoList.id}`, { // 아이디는 알아서 만들어줌
      method: "PUT", // url에 데이터 보내기
      body: JSON.stringify(modifyTodos)
    }).then((res) => res.json());

    setTodoLists(updatedTodos);  // 상태 업데이트 후 리렌더링 발생
    setIsUpdate(false);  // 수정 모드 종료
};

수정할 항목을 선택하여 updateTodoInput에 값을 입력했을 경우,
todoLists에서 해당 항목과 일치하는 id를 찾아 해당 항목의 content 속성을 입력한 값으로 바꿔줍니다.
...el을 통해 나머지 항목은 원래 속성으로 놔둡니다.

일치하지 않는 항목들은 el 그대로 놔두면 되겠죠.

  const modifyTodos = updatedTodos.find((el) => el.id === todoList.id)  
modifyTodos는 이제 업데이트가 된 todoLists에서 수정한 항목만을 찾은 것입니다.

 

이제 서버도 바꿔줘야 합니다 ㅠㅠ!

fetch(`http://localhost:3001/todo/${todoList.id}`, { // 아이디는 알아서 만들어줌
  method: "PUT", // url에 데이터 보내기
  body: JSON.stringify(modifyTodos)
}).then((res) => res.json());

fetch를 통해 PUT 요청을 보내는데,

URL

todoList.id를 포함하여 해당 항목의 bodymodifyTodos로 수정해줍니다.

 

setTodoLists(updatedTodos);  // 상태 업데이트 후 리렌더링 발생
setIsUpdate(false);  // 수정 모드 종료

그리고 TodoLists를 업데이트 하고, isUpdatefalse로 바꿔 수정 모드를 종료합니다.

 

 return (
    <div style={{
      display: 'flex',
      alignItems: 'center'
    }}>
      <input
        value={updateTodoInput}
        className='modifyInput'
        onChange={(event) => setTodoUpdateInput(event.target.value)}
      />
      <button onClick={(event) => {
        modifyTodo();
        event.stopPropagation(); // 부모 요소의 클릭 이벤트 막기
        setIsUpdate(false)
      }}>Modify</button>
    </div>
)

input창과 modify 버튼을 표시하는 return 부분입니다.

여기서 정말 오래 걸렸던게, modify 버튼을 아무리 눌러도 isUpdatefalse가 되질 않았습니다...
그 이유는 바로, text가 속한 div 태그를 눌렀을 때 수정 모드가 되도록 만들고,

그 안에서 UpdateTodo 컴포넌트를 호출하는 바람에
UpdateTodo 컴포넌트에 속한 button을 눌렀을 때 부모 요소인 div 태그까지 클릭이 되어
isUpdatetrue가 되어버리는 것이었죠.

2시간동안 헤맸는데... ㅠㅠ
아무튼

  event.stopPropagation();  
로 부모 요소의 클릭 이벤트를 막아주면 정상적으로 작동됩니다.

 

UpdateTodo 컴포넌트는 Todo 컴포넌트에서 호출됩니다!

<div className={`${isDone ? "done" : ""}`} onClick={() => setIsUpdate(true)}>
    {isUpdate ?
      <UpdateTodo 
          todoList={todoList} 
          todoLists={todoLists} 
          setTodoLists={setTodoLists} 
          setIsUpdate={setIsUpdate}
       /> 
       : (todoList.content)}
</div>

propstodoList, todoLists, setTodoLists, setIsUpdate를 내려줘서 UpdateTodo에서도 사용할 수 있게 합니다.

이렇게 되면 텍스트를 누를 경우 isUpdatetrue로 바뀌면서 그 자리에 UpdateTodo 컴포넌트가 렌더링 되고,
modify 버튼을 눌러 수정을 완료할 경우 todoList.content가 표시됩니다.
(div 태그 안에 UpdateTodo 컴포넌트가 있는 거 보이시죠 ㅠㅠ)

 


오래 걸렸던 UpdateTodo였습니다.
다음은 그냥 랜덤으로 명언을 표시해주는 간단한 Footer입니다.

 

Footer

 

function Footer() {
  const [text, setText] = useState(null);

  useEffect(() => {
    fetch("https://korean-advice-open-api.vercel.app/api/advice")
      .then(res => res.json())
      .then(res => setText(res))
  }, [])

  return (
    <div className='footer'>
      {text &&
        <div className='footer-text'>
          <p>{text.message}</p>
          <p>- {text.author} -</p>
        </div>
      }
    </div>
  )
}

text에 데이터를 담아 표시할 것이기 때문에, 마찬가지로 상태로 관리합니다.

fetch로 데이터를 받아와서 setText로 업데이트합니다.
의존성 배열로 빈 배열을 넣어줘서 처음 렌더링 시에만 데이터를 받아오도록 useEffect를 사용해주면 끝!

text에 데이터가 있을 때 textmessageauthor 속성을 받아와서 렌더링 해주면 됩니다.

 


이렇게 하면 App.jsx가 끝이 납니다!

휴우...
다음으로는 스톱워치 기능을 제공하는 Box.jsx를 보도록 할게요.

// Box.jsx

import React, { useEffect, useState } from 'react'

const formatTime = (seconds) => {
    // seconds / 3600 -> 시간
    // (seconds % 3600) / 60 -> 분
    const timeString = `${String(Math.floor(seconds / 3600)).padStart(2, "0")} : 
    ${String(Math.floor((seconds % 3600) / 60)).padStart(2, "0")} : 
    ${String(seconds % 60).padStart(2, "0")}`

    return timeString;
}

const Box = () => {
    const [currentTime, setCurrentTime] = useState(new Date());
    const [isOn, setIsOn] = useState(false);
    const [stopWatch, setStopWatch] = useState(0);
    const today = new Date();

    const year = today.getFullYear();       // 년도
    const month = today.getMonth() + 1;     // 월 (0부터 시작하므로 +1 필요)
    const day = today.getDate();            // 일

    console.log(`${year}-${month}-${day}`); // 예: 2024-10-28

    useEffect(() => {
        // 1초마다 현재 시간을 업데이트하는 타이머 설정
        const timer = setInterval(() => { // 1초마다 반복
            setCurrentTime(new Date());
        }, 1000);

        // 컴포넌트가 언마운트될 때 타이머를 정리
        return () => clearInterval(timer);
    }, []); // 빈 배열을 사용하여 컴포넌트가 마운트될 때만 실행

    useEffect(() => {
        if (isOn) {
            // 1초마다 시간 증가
            const timer = setInterval(() => {
                setStopWatch(prev => prev + 1)
            }, 1000)

            return () => clearInterval(timer);
        }
    }, [isOn])

    return (
        <div className='box'>
            <div className='stopWatch-text'>Stopwatch</div>
            <div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
                <div>{`${year}/${month}/${day}`}</div>
                <div className='currentTime'>{currentTime.toLocaleTimeString()}</div>
            </div>
            <img src='src\images\pudding.jpg' />
            <div className='stopWatch'>
                {formatTime(stopWatch)}
                <div style={{ display: 'flex', gap: '10px' }}>
                    <button onClick={() => setIsOn(!isOn)}>{isOn ? "정지" : "시작"}</button>
                    <button onClick={() => {
                        setIsOn(false);
                        setStopWatch(0);
                    }}>리셋</button>
                </div>
            </div>
        </div>
    )
}

export default Box

현재 날짜와 시간을 보여주고
시작 버튼과 리셋 버튼을 통해 스톱워치를 조작합니다.
흔히 말하는 순공시 시간을 관리하기 위해 만든 기능입니다!

 

formatTime

 

시간을 HH:MM:SS 형태로 format 해주는 함수입니다.

const formatTime = (seconds) => {
    // seconds / 3600 -> 시간
    // (seconds % 3600) / 60 -> 분
    const timeString = `${String(Math.floor(seconds / 3600)).padStart(2, "0")} : 
    ${String(Math.floor((seconds % 3600) / 60)).padStart(2, "0")} : 
    ${String(seconds % 60).padStart(2, "0")}`

    return timeString;
}

 

먼저, Math.floor(seconds / 3600)는 전체 초를 3600(1시간)으로 나누어 시간을 계산합니다.

그런 다음
Math.floor((seconds % 3600) / 60)로 전체 초에서 시간을 제외한 나머지 초(seconds % 3600)를 60으로 나누어
분을 계산합니다.

마지막으로 seconds % 60으로 전체 초를 60으로 나눈 나머지를 계산하여 초를 구합니다.

그리고는 각 값이 두 자리 숫자가 되도록 padStart(2, "0")을 사용해 한 자리일 경우 앞에 0을 추가합니다.

마지막으로 이 timeStringreturn 해주면 됩니다.

 

Box
const Box = () => {
    const [currentTime, setCurrentTime] = useState(new Date());
    const [isOn, setIsOn] = useState(false);
    const [stopWatch, setStopWatch] = useState(0);
    const today = new Date();

    const year = today.getFullYear();       // 년도
    const month = today.getMonth() + 1;     // 월 (0부터 시작하므로 +1 필요)
    const day = today.getDate();            // 일

    useEffect(() => {
        // 1초마다 현재 시간을 업데이트하는 타이머 설정
        const timer = setInterval(() => { // 1초마다 반복
            setCurrentTime(new Date());
        }, 1000);

        // 컴포넌트가 언마운트될 때 타이머를 정리
        return () => clearInterval(timer);
    }, []); // 빈 배열을 사용하여 컴포넌트가 마운트될 때만 실행

    useEffect(() => {
        if (isOn) {
            // 1초마다 시간 증가
            const timer = setInterval(() => {
                setStopWatch(prev => prev + 1)
            }, 1000)

            return () => clearInterval(timer);
        }
    }, [isOn])

    return (
        <div className='box'>
            <div className='stopWatch-text'>Stopwatch</div>
            <div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
                <div>{`${year}/${month}/${day}`}</div>
                <div className='currentTime'>{currentTime.toLocaleTimeString()}</div>
            </div>
            <img src='src\images\pudding.jpg' />
            <div className='stopWatch'>
                {formatTime(stopWatch)}
                <div style={{ display: 'flex', gap: '10px' }}>
                    <button onClick={() => setIsOn(!isOn)}>{isOn ? "정지" : "시작"}</button>
                    <button onClick={() => {
                        setIsOn(false);
                        setStopWatch(0);
                    }}>리셋</button>
                </div>
            </div>
        </div>
    )
}

 

 

 

const [currentTime, setCurrentTime] = useState(new Date());
const [isOn, setIsOn] = useState(false);
const [stopWatch, setStopWatch] = useState(0);
const today = new Date();

const year = today.getFullYear();       // 년도
const month = today.getMonth() + 1;     // 월 (0부터 시작하므로 +1 필요)
const day = today.getDate();            // 일

우선 현재 시간을 나타내기 위해 currentTime을 상태로 관리합니다.
스톱워치의 on/off를 관리하는 isOn도 상태로 관리하고,
stopWatch의 시간 또한 상태로 관리합니다.

 

useEffect(() => {
    // 1초마다 현재 시간을 업데이트하는 타이머 설정
    const timer = setInterval(() => { // 1초마다 반복
        setCurrentTime(new Date());
    }, 1000);

    // 컴포넌트가 언마운트될 때 타이머를 정리
    return () => clearInterval(timer);
}, []); // 빈 배열을 사용하여 컴포넌트가 마운트될 때만 실행

현재 시간을 업데이트하는 useEffect입니다.
setInterval 함수를 사용하여 1000ms마다 currentTime을 현재 시간으로 업데이트 합니다.

useEffect는 처음 렌더링 될 때만 실행되면 되므로, 의존성 배열로 빈 배열을 추가해줍니다.

메모리 누수를 방지하기 위해 cleanup 함수를 추가하여, 언마운트 시 타이머가 정리되도록 해줍니다.

 

useEffect(() => {
    if (isOn) {
        // 1초마다 시간 증가
        const timer = setInterval(() => {
            setStopWatch(prev => prev + 1)
        }, 1000)

        return () => clearInterval(timer);
    }
}, [isOn])

스톱워치의 시간을 변경해주는 useEffect입니다.

isOntrue일 때, 즉 시작 버튼을 눌렀을 때
1초마다 stopWatch를 1씩 증가시킵니다.

isOn의 값에 따라 변하므로, 의존성 배열로 isOn을 넣어줍니다.
똑같이 메모리 누수 방지를 위해 cleanup 함수를 추가해주면 끝.

 

전부 return으로 렌더링하면 돼서 return 부분은 적지 않겠습니다.


이렇게 Box.jsx까지 끝이 났습니다.

Music.jsx의 경우 그냥 모양만 낸 거여서... 따로 설명을 적지 않겠습니다.
CSS도 따로 안 적을게용

 

이렇게 React로 Todo List 만들기가 끝이 났습니다!
시간이 없어서 코드가 좀 지저분한 편이라, 아쉬움이 남습니다. ㅠㅠ

https://github.com/xuuwon/TodoList-React

 

GitHub - xuuwon/TodoList-React

Contribute to xuuwon/TodoList-React development by creating an account on GitHub.

github.com