Skip to content

[2주차] 이윤서 과제 제출합니다.#4

Open
yiyoonseo wants to merge 17 commits intoCEOS-Developers:masterfrom
yiyoonseo:yiyoonseo
Open

[2주차] 이윤서 과제 제출합니다.#4
yiyoonseo wants to merge 17 commits intoCEOS-Developers:masterfrom
yiyoonseo:yiyoonseo

Conversation

@yiyoonseo
Copy link
Copy Markdown

@yiyoonseo yiyoonseo commented Mar 20, 2026

배포 링크

🔗투두 리스트

파일 구조

image

기획 단계

레이아웃 구조

image 초기 구조
image 최종 구조

 

주요 포인트는 두 개 입니다.

  • Calendar-less Design: 초기 기획에서는 캘린더 포함을 고려했으나, 불필요한 정보 과부하를 방지하기 위해 과감히 삭제했습니다. 사용자의 작업량이 방대하지 않다면, 복잡한 월간 뷰보다는 현재 할 일에 집중할 수 있는 미니멀한 구성이 더 효율적이라고 판단했습니다.
  • Due-Date Centric: 할 일 관리에서 가장 본질적인 데이터인 '마감 기한(Due Date)'을 핵심 기능으로 배치했습니다. 언제 하느냐보다 '언제까지 끝내야 하는가'에 초점을 맞출 수 있는 직관적인 레이아웃을 구성했습니다.

이 포인트들을 바탕으로 레이아웃을 구성했습니다.

개발 단계

1. 컴포넌트화

① 일반성(Generality)을 고려한 공통 컴포넌트 설계 (common/)

  • 일반적인 속성을 갖는 컴포넌트들을 common 폴더로 분리
  • Input.tsx, Header.tsx 등은 Todo 로직에 종속되지 않고 재사용 가능한 보편적인 인터페이스를 갖도록 설계

② 복잡성을 고려한 역할의 분리 (todo/)

  • 하나의 컴포넌트가 너무 많은 역할을 갖지 않도록 기능을 세분화
  • TodoList.tsx: 데이터의 흐름과 섹션 분류라는 구조적 역할
  • TodoItem.tsx: 개별 할 일의 상태 표시
  • 이를 통해 컴포넌트 간 결합도를 낮추고, 특정 기능 수정 시 다른 기능에 미치는 영향을 최소화

 

2. 기능

① 기간 기반 노출 로직 (isVisibleOnDate)

  • 기존 방식: 등록한 당일에만 할 일이 보임.
  • 현재 방식: createdAt(생성일)부터 dueDate(마감일)까지의 모든 날짜 페이지에 해당 할 일이 계속 나타납니다.

② 타입 안정성 확보 및 유틸리티 분리 (date.ts)
코드의 유지보수성을 위해 any를 제거하고 로직을 분리했습니다.

③ TypeScript
Todo 인터페이스를 정교하게 다듬어 isImportant, dueDate 등 데이터 누락으로 인한 런타임 에러를 차단했습니다.

 

3. 리팩토링

1주차 PR 리뷰를 바탕으로 useMemo와 reducer를 사용해 리팩토링을 진행했습니다.

DailyProgress.tsx

  1. useMemo를 통한 연산 결과 캐싱 -> 의존성 배열인 [todos, selectedDate]의 값이 변하지 않는 한, 이전 계산 결과를 재사용하도록 최적화

  2. reduce 메서드를 활용한 단일 순회 로직 구현 -> 한 번의 순회로 total과 completed 카운트를 동시에 추출하는 집계 로직을 구현

const stats = useMemo(() => {
  const dailyOnes = todos.filter((t) => isVisibleOnDate(t, selectedDate));

  // 한 번 순회하면서 total, completed 동시에 카운트
  const { total, completed } = dailyOnes.reduce(
    (acc, t) => {
      const isRelevant = t.dueDate === selectedDate || t.dueDate === null;
      if (!isRelevant) return acc;
      return {
        total: acc.total + 1,
        completed: acc.completed + (t.completed ? 1 : 0),
      };
    },
    { total: 0, completed: 0 },
  );

 

TodoList.tsx

  1. 정렬 로직 분리 -> 컴포넌트가 리렌더링될 때마다 함수가 새로 만들어지는 걸 막음
const sortTimedTodos = (currentPageDate: string) => (a: Todo, b: Todo) => {
  if (a.completed !== b.completed) return a.completed ? 1 : -1;
  const aIsDDay = checkIsDDay(a.dueDate, currentPageDate);
  const bIsDDay = checkIsDDay(b.dueDate, currentPageDate);
  if (aIsDDay !== bIsDDay) return aIsDDay ? -1 : 1;
  return b.id - a.id;
};

const sortByCompletionThenRecent = (a: Todo, b: Todo) => {
  if (a.completed !== b.completed) return a.completed ? 1 : -1;
  return b.id - a.id;
};
  1. 공통 props 묶기
const commonItemProps = { currentPageDate, onToggle, onDelete };

✨Review Question

1. Virtual DOM

Virtual DOM (VDOM)은 UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념입니다. 이 과정을 재조정이라고 합니다.

이 접근방식이 React의 선언적 API를 가능하게 합니다. React에게 원하는 UI의 상태를 알려주면 DOM이 그 상태와 일치하도록 합니다. 이러한 방식은 앱 구축에 사용해야 하는 어트리뷰트 조작, 이벤트 처리, 수동 DOM 업데이트를 추상화합니다.

image 이점
  • 배치 업데이트 (Batching): 데이터가 변할 때마다 실제 화면을 매번 새로 그리는 게 아니라, 가상 DOM에서 변경 사항을 먼저 다 계산한 뒤 딱 한 번만 실제 DOM에 반영합니다.

  • 효율적인 비교 (Diffing): '이전 가상 DOM'과 '새로운 가상 DOM'을 비교하여 정확히 바뀐 부분만 찾아내 업데이트합니다. 이를 통해 브라우저의 연산 비용을 획기적으로 줄입니다.

2. React.memo(), useMemo(), useCallback() 함수로 진행할 수 있는 리액트 렌더링 최적화

  1. React.memo() (컴포넌트 메모이제이션)
    역할: 고차 컴포넌트(HOC)로, 컴포넌트의 Props가 변하지 않았다면 리렌더링을 건너뛴다.
    언제 쓰나: 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 해줄 수 있다.

  2. useMemo() (값의 메모이제이션)
    역할: 복잡한 계산 결과(값)를 메모리에 저장해두고, 의존성 배열이 바뀔 때만 다시 계산한다.
    장점: 함수 호출 시간도 세이브할 수 있고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지할 수 있다.

  3. useCallback() (함수의 메모이제이션)
    역할: 함수 자체를 메모리에 저장합니다. 리액트 컴포넌트 내부에서 선언된 함수는 리렌더링될 때마다 새로 생성되는데, 이를 방지합니다.
    주의점: 자식 컴포넌트에게 함수를 Props로 넘겨줄 때, 자식의 React.memo가 깨지지 않게 하려고 주로 함께 사용합니다.

  4. 추가적인 방법

  • useState의 함수형 업데이트
    • 기존의 useState를 사용하며, 대부분 setState시에 새로운 상태를 파라미터로 넣어준다.
      setState를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데, 이렇게 하면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 값을 넣어주지 않아도 된다.
  const onRemove = useCallback(id => {
  setTodos(todos => todos.filter(todo => todo.id !== id));
  }, []);

3. React 컴포넌트 생명주기 (Lifecycle)

image image

리액트 컴포넌트는 생성(Mount) -> 업데이트(Update) -> 제거(Unmount)의 단계를 거칩니다.

1) 마운트 (Mounting)
컴포넌트가 처음 브라우저 화면에 나타나는 단계입니다.
클래스형: constructor -> render -> componentDidMount
함수형: useEffect(() => { ... }, []) (의존성 배열이 빈 배열일 때)
주요 작업: API 호출, 이벤트 리스너 등록, 외부 라이브러리 초기화.

2) 업데이트 (Updating)
Props나 State가 바뀌어 컴포넌트가 다시 그려지는 단계입니다.
클래스형: componentDidUpdate
함수형: useEffect(() => { ... }, [deps]) (의존성 배열의 값이 바뀔 때)
주요 작업: 바뀐 값에 따른 데이터 동기화, 조건부 로직 실행.

3) 언마운트 (Unmounting)
컴포넌트가 화면에서 사라지기 직전 단계입니다.
클래스형: componentWillUnmount
함수형: useEffect 내부의 return () => { ... } (Clean-up 함수)
주요 작업: 이벤트 리스너 제거, 타이머 중단(clearTimeout), 메모리 정리.

Comment on lines +11 to +18
// 2. 특정 날짜 페이지에 노출 여부 결정
export const isVisibleOnDate = (todo: Todo, targetDate: string): boolean => {
const start = todo.createdAt;
const end = todo.dueDate;

if (!end) return start === targetDate;
return targetDate >= start && targetDate <= end;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

안녕하세요 윤서님~! 2주차 과제하시느라 고생 많으셨습니당!
전체적으로 함수명이 직관적이라서 코드를 읽을 때 목적을 확인하기에 좋았습니다.
그리고 코드 구조도 깔끔하고 컴포넌트 분리도 잘 되어 있어서 읽기에 편했습니다!

Comment on lines +16 to +43
/* 2. 미디어 쿼리 (시스템 다크 모드 대응) */
/* 기본값 아래에 있어야 다크 모드일 때 값을 덮어씁니다. */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1c2c;
--text: #e0e0ff;
--container: #252a41;
--btn: #3f51b5;
--accent: #6874ad;
--delete: #a180ad;
--delete-hover: #7b5e8c;
--add: #67beb5;
--add-hover: #409c93;
}
}

/* 3. 특정 테마 클래스 (수동 전환용) */
[data-theme="blueberry"] {
--bg: #1a1c2c;
--text: #e0e0ff;
--container: #252a41;
--btn: #3f51b5;
--accent: #6874ad;
--delete: #a180ad;
--delete-hover: #7b5e8c;
--add: #67beb5;
--add-hover: #409c93;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 코드에서 미디어 쿼리 기반 다크모드와 [data-theme ] 기반 수동 테마를 함께 사용하여 시스템 설정과 사용자 선택 테마를 모두 대응할 수 있도록 설계된 점이 인상적이었습니다! 또한 :root 에서 색상 값을 변수로 관리하여 UI 전반에서 일관된 스타일을 유지할 수 있도록 하신 점이 좋았습니다. 추후 확장 시에도 최소한의 수정으로 대응할 수 있는 구조라고 생각했습니다!

Copy link
Copy Markdown
Member

@chaeyoungwon chaeyoungwon 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
Member

Choose a reason for hiding this comment

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

지난 주에 드렸던 피드백을 반영해서 리드미를 정말 꼼꼼하게 작성해주셨군요.. 짱 ~ 🥹👍

--color-btn: var(--btn);
--color-delete: var(--delete);
--color-delete-hover: var(--delete-hover);
--color-bg: var(--bg);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

변수명이 -text, -bg처럼 다소 포괄적인 이름으로 되어 있어, 추후 확장이나 유지보수 시 혼란이 생길 수 있을 것 같습니다

현재는 규모가 작은 프로젝트라 큰 문제는 없지만, 협업을 고려해 네이밍을 조금 더 명확하게 가져가보셔도 좋을 것 같아요!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

사용하지 않는 파일은 제거해도 괜찮을 것 같아용

onClick={() => setIsImportant(!isImportant)}
className={`text-xl text-add `}
>
{isImportant ? "★" : "☆"}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

오호 이런 추가 기능 구현 좋습니다~

Comment on lines +60 to +67
<input
type="text"
value={todoText}
onChange={(e) => setTodoText(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
className="flex-1 bg-transparent text-text focus:outline-none py-2"
placeholder="오늘의 할 일은?"
/>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

현재 한글 입력 시 값이 두 번씩 등록되는 현상이 있는 것 같습니다.
이전에 말씀드렸던 KeyboardEvent.isComposing을 활용해보시면 좋을 것 같아요!

참고 링크: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

formatDateToISO와 formatDate 함수가 이름만 다르고 동일한 기능을 하는 것으로 보이네요 🧐
하나의 함수로 통일해 사용하는 것이 좋을 것 같습니다!

Comment on lines +19 to +22
<button
onClick={onToggle}
className={`w-14 h-8 rounded-full p-1 transition-colors duration-500 relative flex items-center bg-accent`}
>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

승연님 코드리뷰에도 남겨드렸는데, 버튼 요소에는 cursor-pointer를 적용해주시는 걸 추천드립니다!
모든 버튼에 한 번에 적용하고 싶으시면 index.css에서 button에 스타일로 넣어주셔도 좋아요!


function App() {
return (
<div className="App">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

현재 App 클래스가 따로 정의되어 있지 않은 것 같은데 맞을까요 ?-?

Comment on lines +18 to +21
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
setShowDatePicker(!showDatePicker);
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

우클릭으로 마감 기한을 설정하는 로직을 구현하신 점 좋습니다!
다만, 사용자가 우클릭으로 기한을 설정할 수 있다는 점을 인지하기 어려울 수 있어,
별도의 버튼으로 기능을 노출하거나 안내 문구를 추가해주시면 더 좋은 UX가 될 것 같습니다 😊


days.push({
fullDate,
dayName: ["일", "월", "화", "수", "목", "금", "토"][date.getDay()],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이런 배열은 상수로 분리하셔도 좋을 것 같아요-!

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.

3 participants