Skip to content

[2주차] 송아영 미션 제출합니다. #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

gustn99
Copy link

@gustn99 gustn99 commented Mar 18, 2025

배포 링크

https://react-todo-21th-git-gustn99-gustns-projects.vercel.app?_vercel_share=RvvoG4gHVQaEFeMoMKzee2mvcEXFBxmz

과제를 하며. . .

지난 과제에서 잘 모듈화된 코드를 보고 오히려 리액트를 사용할 때 state 관리 이슈로 코드가 더 복잡해질 수 있겠다 싶었는데요, 실제로 그 일이 일어난 것 같습니다. . . state를 공유하는 다수의 이벤트 핸들러와 또 jsx 반환문까지 함께 있다 보니 바닐라보다 코드가 지저분해 보이기도 하네요 ㅠㅠ 여러분들은 리액트 프로젝트 구조를 어떻게 잡고 사용하실지!! 이번에도 코드를 열심히 읽어보겠습니다.

그런 한편 매번 document.createElemet를 사용하지 않아도 쉽게 원하는 요소를 반환할 수 있다는 것은 큰 장점이겠죠! 사실은 react도 내부적으로 document.createElement를 수행하고 있다는 점이 재미있는 것 같습니다.

첫 번째 미션을 계속해서 업데이트하는 멋진 분들이 계신데요, 저는 받았던 피드백들을 이번 미션에 적용해서 trim을 사용한 유효성 검사, isComposing 검사 등의 로직을 추가했습니다. 또 모달 UI 가 불필요하다고 느껴 제거하고, 깃허브의 잔디처럼 투두를 완료한 날들을 심을 수 있는 Grass 컴포넌트를 추가해 봤습니다!

Key Questions

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

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

라고 리액트 공식 문서에는 나와 있습니다. 간략하게 말하면 리액트는 리렌더링이 일어날 때 가상 돔을 재구성합니다. 그리고 브라우저 렌더링 시 가상 돔과 실제 돔을 비교하여 변경된 요소들만 수정합니다.

최근에 리액트 공식 문서를 보며 벨로그에 기록하기 시작했는데요, 부끄럽지만 혹시 참고가 될까 하여. . . 첨부해 봅니다 ㅎㅎ. 정확히 가상 돔에 대해 다루는 내용은 아니지만, 리액트의 렌더링 전반에 대한 이해를 도울 수 있으리라 예상합니닷. 사실은 제 글보다도 리액트 공식 문서 자체적으로 한국어 번역을 지원하고 있으니 읽어보시면 많은 도움이 될 것 같습니다!

2. React.memo(), useMemo(), useCallback() 함수로 진행할 수 있는 리액트 렌더링 최적화에 대해 설명해주세요.

React.memo()

  • props의 변경이 없을 때 컴포넌트를 캐싱합니다.
  • styled-components를 사용할 때에는 개별 styled-component를 memo하는 방식으로도 사용할 수 있습니다.
  • props가 변경될 때 자동으로 재렌더링됩니다.

useMemo()

  • 렌더링 시마다 발생할 수 있는 크고 복잡한 연산에 대해 결과를 캐싱합니다.
  • 의존성 배열을 설정하여 특정 값이 변할 때 재연산하도록 할 수 있습니다.
  • 의존하는 값이 자주 바뀌지 않거나, 초기 렌더링 이후로는 연산할 필요가 없는 경우 useMemo를 사용할 수 있습니다.

useCallback()

  • 함수 또한 객체이기 때문에 리렌더링이 될 때마다 새로운 함수가 생성된다는 문제가 있습니다.
  • 따라서 useCallback으로 리렌더링 시에도 함수를 재생성하지 않도록 메모이제이션하면, 이벤트 핸들러를 props로 넘겨주는 등의 상황에서 불필요한 렌더링을 줄일 수 있습니다.

저도 최적화에 굉장히 취약한 편인데요, 어쩌다 보니 이번 미션을 진행하면서 세 가지 함수를 모두 사용했습니다! 오용 또는 남용된 곳이 있을 수 있으니 발견하시면 코드리뷰 남겨주세요!

3. React 컴포넌트 생명주기에 대해서 설명해주세요.

마운트

  • 컴포넌트가 렌더링된 시점입니다.
  • 마운트 시점에서의 동작을 조작하기 위해 주로 useEffect를 사용합니다.

업데이트

  • state나 props의 변경(재할당)으로 재렌더링되는 시점입니다.

언마운트

  • 컴포넌트가 DOM에서 제거되는 시점입니다.
  • useEffect 내부 콜백함수의 return문을 사용해 언마운트 시점에서의 동작을 조작할 수 있습니다.
  • 주로 이벤트 리스너를 해제합니다.

@BeanMouse
Copy link

deploy Protection 풀어주세용

Copy link

@kkys00 kkys00 left a comment

Choose a reason for hiding this comment

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

컴포넌트별로 폴더를 나누고 스타일드 컴포넌트는 style에 모아두셨네요! 디렉토리 구조 잘 보고 갑니다. 과제 수고하셨어요😊!!

Copy link

Choose a reason for hiding this comment

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

현재 한 달 전 객체를 만든 후 setDate(0)으로 전 달의 마지막 날을 구해 한 달 전씩 뒤로 미는 로직인 거 같아요.

createMonthlyCalendar를 시작일과 끝일 두 가지를 인수로 받아 캘린더를 반환하는 방식으로 구현해도 좋을 거 같아요. getTime()을 사용하면 두 date 사이의 일 수를 알 수 있거든요.

export const createGrassCalendar = () => {
    const date = new Date() // 오늘
    const twoMonthAgo = new Date(date.getFullYear(), date.getMonth() - 2, 1) // 두 달 전 1일
    ...
const getDaysBetween = (startDate, endDate) => {
    const diffTime = Math.abs(endDate.getTime() - startDate.getTime()); // 밀리초 차이
    return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // 밀리초를 일(day)로 변환
}

const createMonthlyCalendar = (startDate, endDate /* or numberOfDays */) => {
    const year = startDate.getFullYear();
    const month = startDate.getMonth();
    const numberOfDays = getDaysBetween(startDate, endDate);
    ...
}

그러면 한 달 전 객체와 이번 달인지 아닌지에 대한 검증 과정이 필요하지 않아 코드가 더 단순해질 거 같아요!

const aMonthAgo = new Date();
aMonthAgo.setDate(0);

const numberOfDays = date.getMonth() === new Date().getMonth() ? new Date().getDate() : date.getDate();

Copy link
Author

Choose a reason for hiding this comment

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

각 달의 1일을 알 때, getDaysBetween을 통해 그 사이의 일수를 구하면 setDate(0)으로 마지막 날짜를 구하지 않더라도 그 달의 일수를 구할 수 있다는 뜻인 걸까요?

const data = localStorage.getItem(date);
const todos: TodoDto[] = JSON.parse(data || '[]');
const hasDoneTodo =
date === formatDate(new Date()) ? false : !!todos.filter((todo) => todo.isDone === true).length;
Copy link

Choose a reason for hiding this comment

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

아영_grass 표시 기준 오늘의 todo는 완료된 목록이 있어도 무조건 false로 설정하신 이유가 있을까요? 오늘의 todo를 작성하고 체크를 해도 grass에 반영되지 않더라고요..!

Copy link
Author

Choose a reason for hiding this comment

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

잔디가 심어졌다가 삭제되는 경우를 만들고 싶지 않았습니다! 오늘의 투두는 체크했다가 취소하는 등 계속 변화할 수 있으니까요. 깃허브에서도 잔디를 그런 식으로 관리한다고 생각했었고요!

그런데 사실상 완료한 투두를 취소하거나 삭제하는 일이 잘 없겠다는 점, 깃허브에서도 당일 컨트리뷰트를 반영한다는 점, 동일 피드백이 여러 번 들어왔다는 점에서 오히려 사용자 경험을 저해한 것 같다는 생각이 드네요 ㅠㅠ

Copy link

Choose a reason for hiding this comment

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

아영_scroll 처음 들어갔을 때 노트북 뷰에서는 grass의 13번째 줄까지 표시되고 하단이 잘리는 편이라 현재 grass 부분은 보이지 않아서 grass 부분의 용도를 몰랐어요. 하단 부분이 있다는 것을 부모 컴포넌트의 height를 설정하고 overflow로 스크롤을 만들어 알려주어도 좋을 거 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

헉스. 좋은 피드백 감사합니다. 스크롤 테스트를 깜빡해버렸네요.

Comment on lines +84 to +106
<div>
<TodoInputBox>
<TodoInput
value={todo}
onChange={handleTodoChange}
onKeyDown={handleEnterKeyDown}
placeholder="할 일을 입력하세요..."
/>
<Button disabled={!todo.trim()} onClick={handleAppendButtonClick}>
추가
</Button>
</TodoInputBox>

<ul>
{allTodos.map((todo) => (
<TodoItem
key={todo.id}
onChange={handleCheckboxChange}
onClickDeleteButton={handleDeleteButtonClick}
{...todo}
/>
))}
</ul>
Copy link

Choose a reason for hiding this comment

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

아영_스크롤 시 todo가 잘려서 표시

TodoInputBox 컴포넌트가 stikcy라 스크롤 시 ul이 뚫는 거 같아요..!
TodoInputBox의 display는 그대로 두고
ul에 overflow: scroll를 주어도 좋을 거 같아요.

Copy link

@xseojungx xseojungx left a comment

Choose a reason for hiding this comment

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

2주차 과제도 수고하셨습니다! 너무 잘 해주셔서, memo 최적화 사용 등 코드리뷰 하면서 제가 배운점이 엄청 많은 것 같아요!! 많이 배워 갑니다ㅎㅎ


const handleAppendButtonClick = () => {
if (todo.trim()) {
const id = allTodos.length ? allTodos[allTodos.length - 1].id + 1 : 0;

Choose a reason for hiding this comment

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

1부터 시작하는 아이디를 지정하는 방식 말고, 밀리초를 사용한 숫자나, randomUUID같이 고유 id를 사용하는 방법이 더 좋을 것 같습니다! 4번 지우고, 다른 투두를 추가하면 다시 4번이 된다는것보다 고유 id를 갖도록 하는게 더 직관적일 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

좋은 제안 감사합니다!

Choose a reason for hiding this comment

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

image 이거 깃허브처럼 완료하면 해당 날짜에 색깔 채워주는 방식인가요? 제 노트북에서는 왜 grass가 작동 안 할까요ㅠㅠ 이부분 확인해보시면 좋을 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

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

오늘 투두는 반영되지 않습니다!

}
`;

export const TodoContent = memo(styled.label<{ $isDone: boolean; $isHovered: boolean }>`

Choose a reason for hiding this comment

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

여기에 memo 사용하는건 생각도 못했는데 좋은 것 같습니다!

setIsExpended(true);
}}
onMouseLeave={() => {
setTimeout(() => setIsExpended(false), 700);

Choose a reason for hiding this comment

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

여기세어 set time out을 사용하신 이유가 무엇인가요?

Copy link
Author

@gustn99 gustn99 Mar 24, 2025

Choose a reason for hiding this comment

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

hover 시 white-space와 max-height를 조절해 transition을 주었는데요, mouseleave 시 white-space가 먼저 변경되면서 max-height에 대한 transition이 적용되지 않는 문제가 있었습니다. mouseleave 시에 white-space보다 max-height가 먼저 줄어들게 함으로써 transition을 적용시키려고 해 봤습니다!


const totalTodoCount = allTodos.length;
const doneTodoCount = allTodos.filter((todo) => todo.isDone).length;
const doneTodoRatio = `(${doneTodoCount}/${totalTodoCount})`;

Choose a reason for hiding this comment

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

이 코드는 그냥 리턴문 내에서 작성해도 될 것 같아요..!
저는 주로 복잡한 조건문 등 분기가 복잡하거나 긴 함수가 필요한 랜더링용 코드만 return 밖으로 꺼내놓는데, 아영님은 어떤 기준으로 분리하시나요?

Copy link
Author

@gustn99 gustn99 Mar 24, 2025

Choose a reason for hiding this comment

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

말씀하신 대로라면 return문 내에 아래와 같은 코드를 작성하게 되는데요, 가독성을 많이 저하시키는 것 같아 분리하게 되었습니다.

<Title>To Do List ({allTodos.filter((todo) => todo.isDone).length}/{allTodos.length})</Title>

저는 return문 내에 어떤 것들을 넣고 빼는 기준이 있다기보다는 항상 그때 그때의 가독성을 높일 수 있는 방식으로 변수나 함수를 사용하는 것 같습니다! 또한 가독성을 높일 수 있는 가장 좋은 방법이 관심사의 분리와 적절한 변수 및 함수명을 설정하는 것이라고 생각하고요!

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