Skip to content

[2주차] 구민교 과제 제출합니다.#7

Open
minnngo wants to merge 3 commits intoCEOS-Developers:masterfrom
minnngo:minnngo
Open

[2주차] 구민교 과제 제출합니다.#7
minnngo wants to merge 3 commits intoCEOS-Developers:masterfrom
minnngo:minnngo

Conversation

@minnngo
Copy link
Copy Markdown

@minnngo minnngo commented Mar 20, 2026

베포 링크: https://react-todo-23rd-seven.vercel.app/

기획 단계

이번 과제에서는 새로운 기능을 추가하기보다, 기존 Vanilla Todo의 핵심 흐름을 React 방식으로 재구성하는 데 집중했다. 특히 Daily / Weekly 뷰 전환을 중심으로, 날짜 기반의 할 일 관리 경험을 단순하고 직관적으로 만드는 것을 목표로 했다.

또한 이전 과제에서 받았던 피드백을 반영하며 구조와 사용자 경험을 개선했다. 특히 Weekly 뷰에서 날짜 기준으로 삭제 대상이 달라지는 버그를 수정하고자 하였다.

  • 상단: 앱 제목 + 현재 뷰 상태 버튼
  • 날짜바: 날짜 이동 + 현재 날짜/주 표시
  • 입력 영역: Daily에서만 활성화
  • 통계 영역: 전체 투두 / 완료 투두 / 달성률
  • 목록 영역:
    • Daily는 단일 목록
    • Weekly는 날짜별 그룹 목록
image image2

개발 단계

  1. 상태 구조와 컴포넌트 분리

    화면을 구성하는 요소와 로직을 분리하기 위해, 렌더링 역할은 components로, 재사용 가능한 상태 로직(localStorage 연동, 외부 클릭 감지)은 hooks로, 날짜 계산과 데이터 가공은 utils로 분리했다. 기능별 책임을 명확히 나누는 것이 이후 수정 과정에서 특히 도움이 되었다.

    이번 프로젝트에서는 앱의 핵심 상태를 App 컴포넌트에서 한 번에 관리했다.
    관리한 상태는 현재 날짜(currentDate), 뷰 모드(viewMode), 입력값(inputValue), 메뉴 열림 여부(isMenuOpen), 그리고 날짜별 todo 데이터를 담는 todoData이다.
    또한 전체 투두 수, 완료 수, 달성률처럼 원본 상태로부터 계산할 수 있는 값은 별도의 state로 저장하지 않고 파생 데이터로 처리해 상태를 단순하게 유지했다.

    <상태 구조>

    • currentDate: 현재 보고 있는 날짜
    • viewMode: daily / weekly
    • inputValue: 입력창 값
    • isMenuOpen: 메뉴 열림 여부
    • todoData: 날짜별 todo 데이터

    <컴포넌트>

    • Header: 현재 뷰와 메뉴 전환
    • DateNavigator: 날짜/주 이동
    • TodoInput: 입력과 추가
    • SummaryPanel: 통계 요약
    • TodoList: Daily / Weekly 분기
    • TodoItem: 개별 todo 렌더링
  2. 핵심 기능 구현

    1. Weekly 날짜 그룹화
      • getWeeklyTodoGroups를 통해 주간 날짜 배열을 만들고, 해당 날짜별 todo를 그룹으로 묶었다.
      • Weekly에서는 날짜별 섹션으로 렌더링해 가독성을 높였다.
    2. localStorage 연동
      • 새로고침 후에도 todo가 유지되도록 useLocalStorage 커스텀 훅을 만들었다.
      • 저장과 복원 로직을 컴포넌트에서 직접 쓰지 않고 훅으로 분리해 재사용 가능하게 만들었다.
    3. 삭제/토글 시 dateKey 전달
      • Weekly에서 특정 날짜의 todo를 수정하려면, todo.id만으로는 부족했다.
      • dateKey까지 함께 전달해 각 날짜 배열에서 정확히 수정/삭제되도록 구조를 바꿨다.
    // App.jsx
    const handleToggleTodo = (dateKey, todoId) => {
      setTodoData((prev) => ({
        ...prev,
        [dateKey]: (prev[dateKey] || []).map((todo) =>
          todo.id === todoId ? { ...todo, done: !todo.done } : todo
        ),
      }));
    };
    
    const handleDeleteTodo = (dateKey, todoId) => {
      setTodoData((prev) => ({
        ...prev,
        [dateKey]: (prev[dateKey] || []).filter((todo) => todo.id !== todoId),
      }));
    };
    // TodoList.jsx
    <TodoItem
      key={`${todo.dateKey}-${todo.id}`}
      todo={todo}
      onToggle={() => onToggle(todo.dateKey, todo.id)}
      onDelete={() => onDelete(todo.dateKey, todo.id)}
    />
  3. 1주차 피드백 반영

    • localStorage 추가
    • root CSS 변수로 색상/그림자 관리
    • Weekly에서 Add 버튼도 disabled 처리
    • 날짜 길이에 따라 화살표 위치가 흔들리던 문제 수정
    • 메뉴 외부 클릭 시 닫히도록 개선
    • Weekly에서 날짜가 다른 todo 삭제가 안 되던 버그 수정
    • todo 추가 후 input focus 유지
    • 기능 함수 / 렌더링 파일 분리

느낀점 및 배운점

이번 미션을 하면서 가장 크게 느낀 점은, 같은 Todo 앱이라도 Vanilla JS로 구현할 때와 React로 구현할 때 문제를 바라보는 방식이 많이 다르다는 점이었다. 1주차에는 날짜가 바뀌거나 뷰가 전환될 때마다 어떤 부분을 직접 다시 그려야 하는지 계속 신경 써야 했다면, 이번에는 상태를 기준으로 화면이 자동으로 바뀌는 구조로 바꾸면서 React가 왜 필요한지 직접 느낄 수 있었다.

리액트를 이번에 처음 접하다 보니 useState, useRef, forwardRef 같은 훅들을 어떤 상황에서 써야 하는지 이해하고, 각 상태와 요소들을 연결해 관리하는 과정이 가장 헷갈리고 어렵게 느껴졌다. 특히 이번에는 이전 과제에서 받았던 피드백을 실제 코드 구조에 반영하는데 집중하였는데, 메뉴 버튼처럼 단순해 보이는 기능도, 바깥 영역을 클릭했을 때 닫히도록 구현하려면 ref로 영역을 잡고 이벤트를 따로 감지해야 해서 생각보다 까다로웠다. 어떻게 해야 상태와 흐름을 효율적이고 구조적으로 관리할 수 있는지 고민하는 시간이 길었던 것 같다.

가장 많은 시간을 투자한 부분은 Weekly 뷰에서 날짜가 다른 todo가 삭제되지 않던 버그를 수정하는 과정이었는데, 처음에는 삭제 로직이 현재 보고 있는 날짜(currentDate)의 todo 배열만 기준으로 동작하도록 구현되어 있었기 때문에, Weekly처럼 여러 날짜의 todo를 한 화면에 모아 보여주는 경우에는 현재 날짜에 해당하지 않는 todo를 눌러도 삭제가 되지 않았다. 화면상으로는 같은 리스트 안에 보이는데 실제로는 참조하는 데이터가 달라서 생기는 문제였기 때문에, 처음에는 왜 특정 todo만 삭제가 안 되는지 파악하기가 힘들었다.

문제는 Weekly 뷰에서는 todo.id만으로는 데이터에 정확하게 접근할 수 없다는 점이었다 . 그래서 삭제와 토글 로직에 todo.id뿐 아니라 해당 todo가 속한 날짜의 dateKey까지 함께 전달하는 방식으로 구조를 바꾸었고, Weekly에서도 날짜가 다른 todo를 정상적으로 수정하고 삭제할 수 있었다. 이 과정을 통해 단순히 한 화면만 보고 기능을 테스트하는 것에는 한계가 있으며, 그 화면이 실제로 어떤 데이터 구조와 연결되어 있는지까지 생각해야 한다는 것을 알았다.

또한 컴포넌트를 역할별로 나누고, hooks와 utils를 분리해 보면서 유지보수성의 중요성도 배울 수 있었다. 처음에는 파일이 많아지는 것이 복잡하게 느껴졌지만, 수정이 많아질수록 어느 파일에서 어떤 역할을 맡고 있는지가 분명해서 편리하다는 것을 알 수 있었다. 특히 localStorage 처리나 outside click 감지처럼 반복 가능한 로직을 커스텀 훅으로 분리해두니, 화면 코드를 깔끔하게 관리할 수 있었다.

Review Questions

  1. Virtual-DOM은 무엇이고, 이를 사용함으로서 얻는 이점은 무엇인가요?

    • Virtual DOM: 실제 브라우저 DOM을 메모리상에 가볍게 표현한 가상 객체React는 state나 props가 변경되면 실제 DOM을 바로 수정하지 않고, 먼저 새로운 Virtual DOM을 만든 뒤 이전 Virtual DOM과 비교하여 변경된 부분만 실제 DOM에 반영한다.

      실제 DOM 조작은 비용이 큰데, DOM이 변경되면 브라우저는 레이아웃 계산, 리페인트, 리플로우 등의 작업을 수행할 수 있기 때문에, 불필요한 DOM 조작이 많아질수록 성능에 부담이 생긴다. 따라서 Virtual DOM은 이러한 부담을 줄이기 위해 변경 사항을 먼저 가상으로 계산한 뒤 필요한 부분만 실제로 반영한다.

    • 이점:

      • 불필요한 실제 DOM 조작을 줄일 수 있다.
      • 변경된 부분만 업데이트하므로 렌더링을 효율적으로 관리할 수 있다.
      • 개발자는 DOM을 직접 조작하기보다 상태에 따라 UI가 어떻게 보여야 하는지 선언적으로 작성할 수 있다.
      • 복잡한 화면에서도 UI 갱신 로직을 비교적 단순하게 유지할 수 있다.
  2. React.memo(), useMemo(), useCallback() 함수로 진행할 수 있는 리액트 렌더링 최적화에 대해 설명해주세요. 다른 방식이 있다면 이에 대한 소개도 좋습니다.

    React는 state나 props가 바뀌면 컴포넌트를 다시 렌더링한다. 이 과정 자체는 자연스럽지만, 불필요한 재렌더링이 반복되면 성능이 저하될 수 있다. 따라서 이를 줄이기 위해 React.memo(), useMemo(), useCallback() 같은 도구를 사용할 수 있다.

    • React.memo(): 컴포넌트의 렌더링 결과를 메모이제이션하는 기능이다. 같은 props가 전달되면 해당 컴포넌트를 다시 렌더링하지 않고 이전 결과를 재사용한다. 부모 컴포넌트가 재렌더링되더라도, 자식 컴포넌트의 props가 바뀌지 않았다면 자식의 불필요한 렌더링을 막기 위해 사용한다.
      • 장점: 자식 컴포넌트의 불필요한 재렌더링을 줄일 수 있고, 무거운 UI 컴포넌트나 반복 렌더링이 많은 리스트에서 효과적이다.
      • 주의점: props가 객체나 함수일 경우, 매 렌더링마다 새로운 참조값이 만들어지면 memo 효과가 줄어들며, 모든 컴포넌트에 적용하면 오히려 비교 비용이 늘어날 수 있다.
    • useMemo(): 계산된 값을 메모이제이션하는 훅이다. 의존성 값이 바뀌지 않으면 이전에 계산한 값을 재사용한다. 계산 비용이 큰 연산을 반복하지 않기 위해 사용하며, 객체나 배열의 참조값을 유지하여 불필요한 렌더링을 줄일 때도 활용한다.
      • 장점: 무거운 연산의 반복 실행을 줄일 수 있으며, 하위 컴포넌트에 전달하는 값의 참조를 안정적으로 유지할 수 있다.
      • 주의점: 단순한 연산에는 큰 효과가 없을 수 있으며, 무분별하게 사용하면 코드가 복잡해질 수 있다.
    • useCallback(): 함수를 메모이제이션하는 훅이다. 의존성 값이 바뀌지 않으면 같은 함수 참조를 유지한다. 컴포넌트가 렌더링될 때마다 새로운 함수가 생성되는 것을 막아, 함수 props 때문에 자식 컴포넌트가 불필요하게 다시 렌더링되는 상황을 줄이기 위해 사용한다.
      • 장점: 함수 참조를 유지할 수 있으며, React.memo()와 함께 사용할 때 효과가 크다. 또한 자식 컴포넌트가 함수 props 때문에 다시 렌더링되는 문제를 완화할 수 있다.
      • 주의점: 의존성 배열 관리가 중요하며, 잘못 사용하면 오히려 가독성이 떨어질 수 있다.
  3. React 컴포넌트 생명주기에 대해서 설명해주세요.

    React 컴포넌트 생명주기란 컴포넌트가 생성되고, 변경되고, 제거되는 전체 과정을 의미한다. 보통 Mounting, Updating, Unmounting 3단계가 있다.

    • Mounting: 컴포넌트가 처음 화면에 나타나는 단계이다. 이 단계에서는 컴포넌트 함수가 실행되고, JSX가 반환되며, 그 결과가 실제 DOM에 반영되어 화면에 표시된다. 보통 이 시점에는 초기 데이터 요청, 이벤트 리스너 등록, 외부 구독 시작, 타이머 설정과 같은 작업을 수행한다. 함수형 컴포넌트에서는 주로 useEffect(() => { ... }, [])를 사용하여 컴포넌트가 처음 마운트된 뒤 한 번만 실행되어야 하는 로직을 처리한다.
    • Updating: 컴포넌트가 다시 렌더링되는 단계이다. 이 단계는 state가 변경되거나 props가 변경될 때, 또는 부모 컴포넌트가 재렌더링될 때 발생할 수 있다. 업데이트가 일어나면 React는 새로운 UI를 다시 계산하고, 이전 렌더링 결과와 비교한 뒤, 실제로 바뀐 부분만 DOM에 반영한다. 이 과정에서 값의 변화에 따른 새로운 데이터 요청, 특정 props 변경에 대한 후속 처리, 화면 상태 갱신 등의 작업이 이루어진다. 함수형 컴포넌트에서는 useEffect(() => { ... }, [의존성]) 형태를 사용하여 특정 값이 바뀔 때마다 실행되는 로직을 작성할 수 있다.
    • Unmounting: 컴포넌트가 화면에서 제거되는 단계이다. 이 단계에서 중요한 점은 컴포넌트가 사라지기 전에 이전에 등록했던 작업들을 반드시 정리해야 한다는 것이다. 이를 수행하지 않으면 메모리 누수나 예기치 않은 동작이 발생할 수 있다. 따라서 이벤트 리스너 제거, 타이머 해제, 웹소켓 연결 종료, 구독 해제와 같은 정리 작업이 필요하다. 함수형 컴포넌트에서는 useEffect의 cleanup 함수를 통해 이러한 정리 작업을 처리한다.

};

return (
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 타입스크립트를 사용하실때 이벤트의 타입도 정의해야 할텐데, 현재 React.FormEvent가 사용 비권장 사양이 되었기 때문에 React.SubmitEvent로 정의하셔야 안전하다는 걸 미리 말씀드립니다.

https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events/

<TodoInput
ref={inputRef}
value={inputValue}
onChange={setInputValue}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 구조를 보면 App 컴포넌트에서 inputValue 상태를 관리하고 있어서, 할 일 입력창에 글자를 하나하나 입력할 때마다 App이 리렌더링되고 하위 자식 컴포넌트들도 리렌더링되고 있어요.
최적화 측면에서 입력 상태를 App에서 관리하기보다는 더 작은 컴포넌트로 내려서 입력 시 해당 컴포넌트만 리렌더링되도록 개선할 수 있을 것 같습니다!

@@ -0,0 +1,26 @@
export default function SummaryPanel({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SummaryPanel 컴포넌트는 원시값 props만 받기 때문에 React.memo 적용하면 불필요한 리렌더링을 줄여볼 수 있을 것 같습니다!

Copy link
Copy Markdown

@ryu-won ryu-won left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 훅 분리, 유틸 함수 분리, 주간/일간 보기 전환 기능 등 짧은 시간인데 높은 완성도를 보여주셨네용~! 너무 고생하셨습니다! 디자인도 너무 예쁜 투두네요

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자주사용되는 함수는 훅으로 관리해주셨네요!

<ul className="mt-6 space-y-3">
{todos.map((todo) => (
<TodoItem
key={`${todo.dateKey}-${todo.id}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key로 index가 아닌 고유 id로 하신점 좋습니다~!

Comment on lines +95 to +100
const handleDeleteTodo = (dateKey, todoId) => {
setTodoData((prev) => ({
...prev,
[dateKey]: (prev[dateKey] || []).filter((todo) => todo.id !== todoId),
}));
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제 후 빈 배열이 localStorage에 남아요! 빈 배열이 되면 키 자체를 지워주는 처리가 있으면 더 좋을 것 같습니다~!

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한국어 서비스면 "ko"가 적절할 것 같습니다~!

Comment on lines +13 to +16
useOutsideClick(menuRef, () => {
setIsMenuOpen(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setIsMenuOpen 함수는 리렌더링시에 계속 새로 만들어져서 useCallback으로 감싸서 전달해도 좋을 것 같아요~!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants