Skip to content
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

[2주차] 송유선 미션 제출합니다. #7

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

s-uxun
Copy link

@s-uxun s-uxun commented Sep 21, 2024

2주차 미션: React-Todo


🪄 결과물

🩵 구현 기능

  • 기존 vanilla-todo의 기능들 전부 리액트로 리팩토링

  • 투두리스트 항목 클릭해서 수정할 수 있는 기능 추가

  • 캘린더 기능 추가 (날짜별로 투두 저장 및 조회 가능, todo 있는 날은 dot 표시)

  • 진행률 표시 기능 추가 (완료한 투두 / 전체 투두의 비율 퍼센트로 계산)

  • word-break 문제 수정

🩵 느낀 점

바닐라 JS만으로 작업해야 했던 1주차 과제와 비교하면 확실히 훨씬 더 효율적으로 코드를 관리할 수 있었다. 바닐라 JS로 작성할 때는 함수들을 하나의 파일에 모두 넣다 보니 기능을 추가할 때마다 코드가 복잡해지고, 원하는 부분을 찾는 것도 힘들었다. 반면, 리액트로는 컴포넌트를 기능별로 분리하고 구조를 깔끔하게 만들 수 있었기 때문에 코드의 가독성이 좋아지고, 새로운 기능을 추가하는 것도 훨씬 수월했다. 특히 react-calendar 라이브러리를 사용해서 이전에 바닐라 JS로 구현하지 못했던 캘린더 기능을 빠르게 추가할 수 있었다. 다만 이번에 상태 관리 라이브러리를 사용하지 못해서 모든 상태를 props로 전달해야 했었는데, (프로젝트의 규모가 크지 않아 별 문제는 없었지만) 그래도 살짝 번거로웠다. 그리고 모바일 반응형 디자인같은 디테일 요소를 시간 부족으로 인해 구현하지 못했는데, 이 부분도 약간의 아쉬움으로 남았다.

💡Key Questions

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

Virtual-DOM은 말 그대로 가상 DOM, 즉 실제 DOM의 사본이다. 이 사본은 JS 객체 형태로 메모리 안에 저장되어 있다. 실제 DOM을 조작하는 건 보통 브라우저가 html을 탐색해 특정 element를 찾고, 해당 element와 자식 element들을 dom에서 제거한 뒤 새롭게 수정된 elememt로 교체하는 작업으로 이루어진다. 그러나 실제 DOM을 직접 조작하는 것은 비효율적이다. DOM 조작이 작은 트리의 정보를 업데이트한다던가, 빠른 알고리즘을 사용한다던가 하는 조간 하에서는 크게 무리가 있는 작업은 아니지만, 이를 계속 반복적으로 수행한다면 충분히 무거운 작업이 될 수 있다. UI 업데이트가 빈번하게 일어나는 서비스 같은 경우에는 DOM을 직접 조작하는 방식을 지속적으로 사용하면 페이지가 느려질 수 있다.

이런 문제를 해결하기 위해서 리액트는 항상 두 개의 가상 DOM 객체를 가지고 있다. 하나는 렌더링 이전 화면 구조를 나타내는 가상 DOM이고, 다른 하나는 렌더링 이후에 보이게 될 화면 구조를 나타내는 가상 DOM이다.

Virtual-DOM

리액트 컴포넌트가 렌더링될 때 가상 DOM이 업데이트 되는데, 이때 실제 UI가 변경되는 것이 아니라, 메모리 상의 가상 DOM이 업데이트된다. 그리고 변경된 가상 DOM과 이전의 가상 DOM을 비교하는데, 이를 Diffing이라고 한다. Diffing은 효율적인 알고리즘을 사용해 진행되기 때문에 어떤 요소에 차이가 있는지 신속하게 파악할 수 있다. 리액트는 파악한 정보를 토대로 차이가 발생한 부분만을 실제 돔에 적용한다. 이 과정을 Reconciliation이라고 한다. Reconciliation은 Batch Update 방식을 사용하는데, 이는 변경된 모든 요소들을 집단화시켜 한 번에 실제 DOM에 적용시키기 때문에 매우 효율적이라고 할 수 있다. (element들을 별개로 하나하나 그려주는 게 아니라 한 번에 받아와 실제 DOM에 적용시켜준다는 것)

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

  • React.memo() : React.memo()는 Higher-Order Component(컴포넌트를 인자로 받아서 새로운 컴포넌트를 return해주는 구조의 함수)로, props가 변경되지 않는 한 컴포넌트를 다시 렌더링하지 않도록 하는 역할을 한다. 기본적으로 리액트는 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 모두 리렌더링한다. 이때 자식 컴포넌트가 동일한 props를 받고 있음에도 불구하고 불필요하게 리렌더링되는 경우가 발생하는데, React.memo()는 이를 방지하여 성능을 향상시킨다. => 자주 변경되지 않는 데이터나 상태를 가진 컴포넌트에 적합함! 그러나 props 비교를 위한 추가적인 메모리와 연산이 필요하기 때문에 무조건 모든 컴포넌트에 사용하는 것은 오히려 성능 저하를 유발할 수 있다. 최적화가 필요한 컴포넌트에만 선택적으로 사용해야 한다.

  • useMemo() : useMemo()는 특정 연산의 결과를 메모이제이션하여, 불필요한 재계산을 방지하는 역할을 한다. 컴포넌트가 리렌더링될 때마다 계산 비용이 높은 작업이 반복적으로 수행되는 경우, 이 작업의 결과를 캐시하여 이전 값을 재사용하도록 돕는다. 이는 복잡한 연산이나 데이터 변환 작업이 포함된 코드에서 성능 최적화에 유용하다. 그러나 메모이제이션 자체에도 메모리와 연산 비용이 들기 때문에 너무 간단한 연산에서는 오히려 사용하지 않는 것이 성능에 더 유리할 수 있다. => 리렌더링될 때마다 동일한 값을 유지해야 하는, 최적화가 필요한 연산에만 사용하는 것이 좋음!

  • useCallback() : useCallback()은 함수를 메모이제이션하여, 불필요한 함수 재생성을 방지하는 역할을 한다. 컴포넌트가 리렌더링될 때마다 함수가 새로 생성되는 것을 막고, 동일한 함수를 재사용할 수 있도록 한다. 특히 자식 컴포넌트에 함수형 props를 전달할 때, 부모 컴포넌트의 리렌더링으로 인해 자식 컴포넌트가 불필요하게 리렌더링되는 걸 막는 데 유용하다. 그러나 이를 남용할 경우 오히려 코드가 복잡해지고 메모리 사용량이 증가할 수 있다. => 자식 컴포넌트에 전달하는 함수가 자주 변경되지 않을 때 사용하는 것이 좋음!

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

React 생명주기

Mounting(생성) - Updating(업데이트) - Unmounting(제거) 단계로 나뉜다.

  • Mounting (컴포넌트가 생성될 때): 컴포넌트가 처음 생성되어 DOM에 삽입될 때 실행되는 단계이다.

    • constructor() : 컴포넌트 클래스의 생성자로, 컴포넌트가 만들어질 때마다 호출된다. 주로 초기 state를 설정하거나 메서드를 바인딩할 때 사용된다.
    • getDerivedStateFromProps() : props에서 파생된 state를 업데이트하는 역할을 한다. 컴포넌트가 마운트되기 전에 호출되며, props에 따라 state를 설정할 수 있다.
    • render() : 해당 함수 내 리턴 코드를 통해 DOM의 기능과 모양 정보가 담긴 React 요소를 반환한다. 컴포넌트의 UI를 정의하는 부분으로, 필수적으로 구현해야 한다.
    • componentDidMount() : 컴포넌트 생성부터 렌더링(종료)까지 진행된 후 호출되는 함수이다. 이 시점에서는 DOM에 접근할 수 있으며, 외부 데이터 요청이나 이벤트 리스너 등록 등의 작업을 수행할 수 있다.
  • Updating (컴포넌트가 업데이트될 때) : 컴포넌트의 props 또는 state가 변경되어 리렌더링될 때 실행되는 단계이다.

    • getDerivedStateFromProps() : props에서 파생된 state를 업데이트하는 역할을 한다. 컴포넌트가 업데이트되기 전에 호출되며, props에 따라 state를 설정할 수 있다.
    • shouldComponentUpdate() : 컴포넌트가 업데이트될지 여부를 결정하는 함수로, true를 반환하면 리렌더링이 진행되고 false를 반환하면 렌더링을 중단한다. 성능 최적화를 위해 주로 사용된다.
    • render() : 해당 함수 내 리턴 코드를 통해 DOM의 기능과 모양 정보가 담긴 React 요소를 반환한다. props나 state의 변화에 따라 UI를 갱신하는 함수로, 필수적으로 구현해야 한다.
    • getSnapshotBeforeUpdate() : DOM이 업데이트되기 직전에 호출되며, 이전 상태나 스크롤 위치 등의 정보를 캡처할 수 있다. 이 메서드에서 반환된 값은 componentDidUpdate()에서 사용된다.
    • componentDidUpdate() : 컴포넌트가 업데이트된 후 호출된다. DOM 조작, 외부 데이터 요청 등 업데이트 후 필요한 작업을 수행할 수 있다.
  • Unmounting (컴포넌트가 DOM에서 제거될 때): 컴포넌트가 DOM에서 완전히 제거될 때 실행되는 단계이다.

    • componentWillUnmount() : 컴포넌트가 DOM에서 제거되기 직전에 호출된다. 주로 타이머 정리, 이벤트 리스너 해제, 네트워크 요청 취소 등 정리 작업을 수행하는 데 사용된다.

"react": "^18.3.1",
"react-calendar": "^5.0.0",
Copy link

Choose a reason for hiding this comment

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

이번 코드 리뷰를 하며 라이브러리의 활용의 필요성을 뼈저리게 느끼고 있습니다... ㅠㅠ!!!!! 저는 calendar 라이브러리를 활용할 생각을 하지 못했을까요 🥺🥺

그래도 유선 님과 희원 님의 훌륭한 코드를 보며 제 코드를 리팩토링 할 수 있게 되어 기쁩니다!!

우선 리뷰 시작에 앞서 디자인이 너무나 아름다운 것 같습니다... 취향 저격이에용 ㅠㅠ 진행률 bar가 매끄럽게 늘어났다 줄어들었다 하는 것도 인상적입니다... 👍🏻👍🏻

Copy link
Collaborator

@jinnyleeis jinnyleeis left a comment

Choose a reason for hiding this comment

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

안녕하세요 유선님, 디자인적 측면으로도 코드적 측면으로도 너무나 퀄리티있는 과제 잘 봤습니다!!!
캘린더와 프로그레스바, 그리고 애니메이션도 적용하신게 ux적인 측면에서 정말 최고의 투두 리스트를 개발하신 것 같아 너무 인상깊었어요!!
useMemo()까지 적용하신 것도 보니, 리엑트에 대한 이해가 깊으신 것 같아 많이 배워갑니다 ㅎㅎㅎㅎ
몇가지 제안드릴 부분 리뷰로 남겨놓았으니, 참고하시면 좋을 것 같아요!!
이번 과제도 정말 수고 많으셨습니다!!!!

text-shadow: 0px 0px 5px #ff3898;

&::before {
content: "${(props) => (props.done ? "♥" : "♡")}";
Copy link
Collaborator

Choose a reason for hiding this comment

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

styled-components의 &::before로 하트를 관리하셨는데, 이런식으로 관리하면 JSX 내부의 상태 변화를 즉시 반영하지 않기 때문에 다른 부분을 클릭하지 않으면, 특정할일의 done이 true에서 -> 다시 false인 상태로 바뀌어도 하트에는 이부분이 반영되지 않는 것 같습니다!!
image (3)

따라서, styled-components의 &::before를 사용하는 것보다 하트가 즉시 상태 변화에 따라 "♥"와 "♡" 사이에서 변경될 수 있게끔 JSX에서 직접 하트를 표시하는 방식으로는 것이 어떨까 제안드려요!!
<DoneBtn done={todo.done} onClick={toggleTodo}> {todo.done ? "♥" : "♡"} </DoneBtn>
이런식으로 관리할 수 있게끔 DoneBtn 컴포넌트를 수정하는 것을 제안드려봐요!!

Copy link
Author

Choose a reason for hiding this comment

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

헙 생각하지 못했던 부분이에요! 알려주셔서 감사합니다 🤍

const [todos, setTodos] = useState([]);
const [date, setDate] = useState("");

useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

로컬 스토리지와의 연동을 useEffect 훅을 사용하여 유려하게 잘 구현하신 것 같아요! 훅을 잘 이해하고 사용하고 계신것 같아 멋져요ㅎㅎ

Choose a reason for hiding this comment

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

원투 훅

@@ -6,6 +6,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"moment": "^2.30.1",
Copy link
Collaborator

Choose a reason for hiding this comment

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

moment 라이브러리를 사용해서 날짜 관리를 하셨네요!!
moment 라이브러리도 정말 좋은 라이브러리이지만, 용량적인 측면에서 리엑트 돔보다 2.3배 뷰보다 3.6배가 더 크다고 합니다. 또한, Moment.js 공식문서에서도 이 라이브러리를 레거시하다고 해서 신규 개발은 하지 않을 예정이라고 하네요..! dayjs 같은 더 가벼운 날짜 관리 라이브러리를 사용하는 것을 추천드려봐요!!

https://velog.io/@hamjw0122/%EC%9A%B0%EB%A6%AC%EA%B0%80-moment.js-%EB%8C%80%EC%8B%A0-day.js%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

Copy link
Author

Choose a reason for hiding this comment

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

검색하다가 dayjs랑 moment 둘 다 봤는데 아무 생각 없이 그냥 moment 썼었거든요..!! 이런 측면이 있었네요! 다음엔 dayjs로 써볼게요😚

showNeighboringMonth={false}
minDetail="year"
tileContent={({ date }) => {
const existTodos = todos.some(
Copy link
Collaborator

Choose a reason for hiding this comment

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

tileContent에서 todos.some()을 통해 모든 날짜마다 todos 배열을 순회해 일치하는 항목을 찾는 방식으로 구현되어 있는데,이러한 방식보다, useMemo()를 사용해서 todos 배열을 미리 날짜별로 매핑하는 방식으로 변경하는 것은 어떨지 추천드려요! 그래서 todos가 변경될 때만 날짜별 투두 항목을 매핑을 업데이트 하면, 더욱 효율적일 것 같단 생각입니다!!!

Copy link
Author

@s-uxun s-uxun Sep 24, 2024

Choose a reason for hiding this comment

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

와..! 제가 봐도 더 효율적인 방법같아요..!!😂👍🏻 다음엔 이런 방식으로도 써볼게요!

column-gap: 40px;
`;

const FloatingButton = styled.button`
Copy link

Choose a reason for hiding this comment

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

이 달력 버튼이 fixed로 고정 되어있어 화면 전체 크기를 줄였을 때 TodoContainer은 반응형으로 줄어들지만 달력 버튼이 밖으로 벗어나지게 됩니다 🥺🥺 또 안의 달력 요소가 밖으로 삐져나오게 되는 부분 수정이 필요할 것 같습니다!!

width: 100%와 column-gap: 40px 때문에, 요소들이 가로로 나열되며 충분한 공간을 차지하는데, 화면 크기가 줄어들면 가로로 배치된 요소들이 더 이상 화면에 맞지 않게 되고, 그 결과 삐져나오는 현상이 발생하는 것 같습니다 ㅠㅠ

flex-wrap 속성을 추가하여 작은 화면에서는 세로 배치로 전환되게 하거나 미디어쿼리를 사용해 보시는 것은 어떨까요?!

더 니은 해결방법이 있다면 공유 부탁드립니다 👍🏻👍🏻

image

Copy link
Author

Choose a reason for hiding this comment

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

제 코드를 열심히 봐주신 것 같아요🥹 사실 저도 달력을 포함해서 그냥 반응형 자체를 이번에 많이 구현하지 못했어서.. 너무 아쉬웠답니다... 다음엔 꼭 민재님 말씀처럼 미디어쿼리 써서 반응형으로 해볼게요👍🏻


// 전역 스타일 설정 (#root, 애니메이션, 스크롤바)

const GlobalStyle = createGlobalStyle`
Copy link

Choose a reason for hiding this comment

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

전역 스타일을 설정하기 위해 createGlobalStyle을 사용하셨군요!!
저는 App 컴포넌트 내부에 선언 후 사용했어서 코드의 가독성이 떨어진다고 생각하는데, 앞으로는 이렇게 컴포넌트화 함으로써 유지보수 및 가독성을 높이는 데 힘 써야겠습니다 🔥🔥🔥


return (
<Form onSubmit={handleSubmit}>
<input
Copy link

Choose a reason for hiding this comment

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

현재 input 요소가 한 글자 한 글자 입력할 때마다 리렌더링이 발생하고 있습니다!
저도 똑같은 현상을 겪었어서 (사실 굳이 최적화 할 필요는 없을 것 같지만) 리렌더링 최적화를 공부하는 겸 해결해 봤는데요,상태로 입력 값을 관리하기에 한 글자 입력할 때마다 onChange 이벤트 탓에 상태가 계속 업데이트 되는 게 문제였어서 useRef를 이용해 상태 업데이트 없이 input 값을 추척해 보았습니다!!

저의 코드를 리뷰해 주셔서 제 트러블 슈팅 노션 페이지를 확인하실 수 있으실 텐데 한 번 훑어보신 후, 더 나은 방법이 있거나 제 방법에 피드백 주실 게 있다면 언제든 연락 주시면 감사하겠습니다!! 🔥👍🏻

Copy link

@westofsky westofsky left a comment

Choose a reason for hiding this comment

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

안녕하세요 호랑이 조교 배성준입니다.
코드를 보기 좋게 작성해주셔서 리뷰하기 편했던 것 같아요~~~~
비지니스 로직을 좀 더 분리해보면 좋을 것 같아 몇 가지 리뷰 남겨봅니다.... 참고만 해주세요
컴포넌트의 역할에 대해서도 고민해보면 좋을 것 같아요!!!!!!!
과제 고생하셨습니다~~~~~~~~~~~

Comment on lines +5 to +13
const ProgressBar = ({ progress }) => {
return (
<ProgressBarWrapper>
<Progress style={{ width: `${progress}%` }}>
{progress > 0 && progress < 100 && <Tooltip>{`${progress}%`}</Tooltip>}{" "}
</Progress>
</ProgressBarWrapper>
);
};

Choose a reason for hiding this comment

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

아주 아주 사소하지만 코드를 딱 봤을 때 한 눈에 알아보기 쉽게 로직을 살짝 분리해도 좋을 것 같아요!

Suggested change
const ProgressBar = ({ progress }) => {
return (
<ProgressBarWrapper>
<Progress style={{ width: `${progress}%` }}>
{progress > 0 && progress < 100 && <Tooltip>{`${progress}%`}</Tooltip>}{" "}
</Progress>
</ProgressBarWrapper>
);
};
const ProgressBar = ({ progress }) => {
const isTodoing = progress > 0 && progress < 100;
return (
<ProgressBarWrapper>
<Progress style={{ width: `${progress}%` }}>
{isTodoing && <Tooltip>{`${progress}%`}</Tooltip>}
</Progress>
</ProgressBarWrapper>
);
};

Comment on lines +11 to +14
const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, "0"); // YYYY-MM-DD 형식과 일치시키기 위해 한 자리 수면 앞에 0 붙임
const day = String(selectedDate.getDate()).padStart(2, "0"); // YYYY-MM-DD 형식과 일치시키기 위해 한 자리 수면 앞에 0 붙임
const formattedDate = `${year}-${month}-${day}`;

Choose a reason for hiding this comment

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

moment에 format도 있습니당~~~~

Suggested change
const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, "0"); // YYYY-MM-DD 형식과 일치시키기 위해 한 자리 수면 앞에 0 붙임
const day = String(selectedDate.getDate()).padStart(2, "0"); // YYYY-MM-DD 형식과 일치시키기 위해 한 자리 수면 앞에 0 붙임
const formattedDate = `${year}-${month}-${day}`;
const formattedDate = moment(selectedDate).format('YYYY-MM-DD');

Copy link
Author

Choose a reason for hiding this comment

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

앗... 바닐라js 코드를 리팩토링 때 거의 그대로 쓴 다음에 달력 넣었더니 이걸 생각 못 했네요... 알려주신 것처럼 moment 쓰니까 코드가 엄청 간단해지는..😂 담엔 꼭 쓸게여

Comment on lines +33 to +35
return existTodos ? (
<StyledDot key={moment(date).format("YYYY-MM-DD")} />
) : null;

Choose a reason for hiding this comment

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

간단하게 표시해도 좋을 것 같아요

Suggested change
return existTodos ? (
<StyledDot key={moment(date).format("YYYY-MM-DD")} />
) : null;
return existTodos && <StyledDot />

Comment on lines +33 to +42
{edit ? ( // 클릭해서 내용 수정할 수 있도록 함 (span이다가 click -> (setEdit) -> input)
<input
value={editedText}
onChange={(e) => setEditedText(e.target.value)}
onBlur={handleSave}
/>
) : (
<span onClick={handleEdit}>{todo.text}</span>
)}
<DelBtn onClick={deleteTodo}>×</DelBtn>

Choose a reason for hiding this comment

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

edit모드일 때 input 길이때문에 원래 content가 보이지 않아서 width값을 조절하거나
줄바꿈이 지원되지 않는 input대신 textarea 사용해도 좋을 것 같아요~~~

Copy link
Author

Choose a reason for hiding this comment

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

테스트할 때 보통 짧게만 todo를 적어서 엄청 긴 텍스트의 경우를 고려하지 못한 거 같아요..! 다음엔 textarea를 활용해보겠습니다!!

const [todos, setTodos] = useState([]);
const [date, setDate] = useState("");

useEffect(() => {

Choose a reason for hiding this comment

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

원투 훅

Comment on lines +26 to +35
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem("todos")) || [];
setTodos(storedTodos);
}, []);

useEffect(() => {
if (todos.length > 0) {
localStorage.setItem("todos", JSON.stringify(todos));
}
}, [todos]);

Choose a reason for hiding this comment

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

todo 관련해서 useEffect가 두번 사용되는데 useTodos 만들어서 사용해도 재밌을 것 같아요~~

const useTodos = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    const storedTodos = JSON.parse(localStorage.getItem("todos")) || [];
    setTodos(storedTodos);
  }, []);

  useEffect(() => {
    if (todos.length > 0) {
      localStorage.setItem("todos", JSON.stringify(todos));
    }
  }, [todos]);

  return [todos, setTodos];
};

Copy link
Author

Choose a reason for hiding this comment

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

오 커스텀 훅으로 만들면 더 깔끔하고 재사용하기도 간편할 거 같아요..! 연습할게요 🔥


export default TodoCalendar;

const CalendarWrapper = styled.div`

Choose a reason for hiding this comment

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

프론트는 디자인이 생명인데 ..
image
여기까지할게요

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
Author

@s-uxun s-uxun Sep 24, 2024

Choose a reason for hiding this comment

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

아니 내비게이션까지 다 볼 줄 몰랐는데...

Comment on lines +9 to +28
const toggleTodo = () => {
setTodos((todos) =>
todos.map((t) => (t.id === todo.id ? { ...t, done: !t.done } : t))
);
};

const deleteTodo = () => {
setTodos((todos) => todos.filter((t) => t.id !== todo.id));
};

const handleEdit = () => {
setEdit(true);
};

const handleSave = (e) => {
setTodos((todos) =>
todos.map((t) => (t.id === todo.id ? { ...t, text: editedText } : t))
);
setEdit(false);
};

Choose a reason for hiding this comment

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

�TodoItem은 말 그대로 아이템이라서 혼자만의 역할을 해도 좋을 것 같아요 setTodos는 Todo 전체를 감싸는 친구의 역할이라서 TodoItem에선 id값만을 넘겨주고 전체 Todo 배열을 관리하는 App.js에서 제어해도 좋지 않을까요 ??
고민해볼만한것같네요~~~~

Copy link
Author

Choose a reason for hiding this comment

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

오왕.. 이런 방식으로 써도 좋을 거 같아요..!! 전 다 todoitem에서 했는데 이렇게 쓰는 방법도 고민해 볼게요!

const todoCount = useMemo(() => {
const doneCount = todos.filter((todo) => todo.done).length;
const progress =
todos.length === 0 ? 0 : Math.round((doneCount / todos.length) * 100); // 나중에 진행률 바에 표시할 때 너무 길어지면 안 되니까 반올림함

Choose a reason for hiding this comment

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

별거아니지만 그래도 한번 분리해주면 가독성이 좋아질 것 같아요!!!!!!!!!!!!!

const getTodoRate = (todos) => {
  return todos.length === 0 ? 0 : Math.round((doneCount / todos.length) * 100);
}

const progress = getTodoRate(todos);

<input
value={editedText}
onChange={(e) => setEditedText(e.target.value)}
onBlur={handleSave}

Choose a reason for hiding this comment

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

onBlur로 저장시킨 점 좋습니다!!!!!!!!!!!!!!!!!!!!!!!!!! 엔터눌러도 저장하고싶어요

Copy link
Member

Choose a reason for hiding this comment

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

모든 사용자의 취향을 고려할 순 없습니다.

Copy link

@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.

과제하느라 수고 많으셨습니다~!
캘린더 구현하신 거, 할 일 있는 날짜에는 dot표시 하신 부분이 너무 실용적인 거 같아요!
ui도 예뻐서 감탄했습니다!
코드리뷰를 마저 하지 못해서 나머지는 오늘 스터디 끝나고 하겠습니다 ㅠ

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.

6 participants