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주차] 강다혜 미션 제출합니다. #9

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

Conversation

psst54
Copy link

@psst54 psst54 commented Sep 21, 2024

🌊 결과물

배포 링크 :
https://react-todo-20th-six.vercel.app/

기능 구현

  • Open, In Progress, Done column별로 목표를 확인할 수 있다.
  • 각 Column에서 input field를 통해 새로운 목표를 추가할 수 있다.
  • 목표는 X 버튼을 통해서 삭제할 수 있다.
  • 목표 내에서 input field를 통해 새로운 할 일을 추가할 수 있다.
  • 할 일은 삭제 버튼을 통해서 삭제할 수 있다.
  • 할 일 요소의 체크박스틀 통해 할 일을 완료/해제할 수 있다.

🌊 후기

  • VanillaJS에서는 목표와 할 일 로직을 어느 정도 분리할 수 있었는데, 이번에는 어떻게 분리할 수 있을지 고민하다 useSubject 훅 안에 목표 관련 함수들과 할 일 관련 함수들을 몰아넣었습니다. 대신 hook 사용 시 const { subjectList, addSubject, deleteSubject, ...taskHooks } = subjectHooks;, const { addTaskToSubject, deleteTaskFromSubject, toggleTaskInSubject } = taskHooks;처럼 subjectHook 부분과 taskHook 부분을 각각 묶어서 props로 전달할 때 갯수가 너무 많아지지 않도록 했습니다.
  • 또한 useSubject 훅을 호출하는 컴포넌트와, 호출 결과로 받은 배열을 이용해 실제로 데이터를 표시하는 컴포넌트 사이가 멀어지면서 엄청난 props drilling이 일어나고 말았습니다🫠 이 부분은 다음에 리팩토링하면서 전역 상태 관리 라이브러리를 붙이면 해결될 것 같습니다.
  • 이번주 Key Question이 memo, useMemo, useCallback 등을 이용한 최적화였지만... 저는 최적화는 따로 적용하지 않았습니다😭 투두리스트는 비교적 간단한 앱이라서 최적화를 따로 하지 않아도 될 것 같다는 판단이었는데, 나중에 저런 최적화를 적용하고 성능 측정도 해보고 싶네요😋

🌊 Key Questions

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

Virtual DOM은 UI의 가상적인 표현이 메모리에 저장되고, 브라우저의 실제 DOM과 동기화하는 프로그래밍 개념이다.

장점

  • 브라우저 리소스 절약
    • 실제 DOM을 조작한다면 브라우저에서 render tree 재계산, reflow(레이아웃 재계산), repaint(화면 다시 그리기) 작업이 일어날 수 있기 때문에 리소스가 소모된다.
    • Virtual DOM을 사용한다면 브라우저의 실제 DOM을 조작하기 전에, 메모리상에 존재하는 가상의 DOM을 먼저 조작한 뒤, 변경된 부분만 실제 DOM에 반영한다.
    • 변경 사항이 있을 때 실제 DOM의 모든 부분을 다시 렌더링하지 않고, 변경된 부분만 효율적으로 업데이트할 수 있다.
  • 추상화
    • DOM 조작을 직접 하지 않기 때문에 DOM 관련 복잡성을 관리하지 않아도 된다.
    • 기존에는 아래처럼 DOM을 직접 조작해야 했지만,
      // 명령형 코드
      if (isActive) {
        document.getElementById('example-element').classList.add('active');
      } else {
        document.getElementById('example-element').classList.remove('active');
      }
    • Virtual DOM을 사용하면 이처럼 isActive 상태를 기반으로 class를 적절하게 변경할 수 있다.
      // 선언적 코드: React를 이용하는 경우
      const ExampleComponent = ({ isActive }) => {
        return <div className={isActive ? 'active' : 'inactive'}>예시</div>;
      };

과정

가상 DOM을 실제 DOM에 동기화하는 과정을 재조정(reconciliation) 이라고 하고, 여러 방법이 있다. React에서는 내부적으로 ReactDom이라는 라이브러리를 사용해서 가상 DOM과 실제 DOM을 동기화시킨다. Vanilla JS에서도 Virtual DOM을 이용하고 싶다면 Snabbdom같은 라이브러리를 이용할 수 있다.

 

재조정은 아래와 같은 방식으로 진행된다.

  1. 상태 변경(State Change): 상태가 변경될 때마다 (setState 등을 통해) DOM 트리가 다시 생성된다. 즉, 메모리에는 두 개의 가상 DOM 트리가 동시에 존재하게 된다.

  2. 비교(Diffing): 전의 Virtual DOM과 새로운 Virtual DOM을 비교하여 어떤 부분이 변경되었는지 확인한다.

  3. 리렌더링(Re-render / Patching): 변경 사항이 실제 DOM에 적용된다. 이때 React는 실제 DOM을 업데이트하는 데 필요한 최소한의 작업 수를 찾아내고, 변경 사항을 한 번에 모아서 처리한다.

React.memo(), useMemo(), useCallback() 함수로 진행할 수 있는 리액트 렌더링 최적화에 대해 설명해주세요. 다른 방식이 있다면 이에 대한 소개도 좋습니다.

memo(), useMemo(), useCallback() 을 이용하면 컴포넌트의 불필요한 렌더링을 방지함으로써, 성능을 높일 수 있다.

memo

memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우, 리렌더링을 건너뛸 수 있다.

React는 일반적으로 부모 컴포넌트가 리렌더링될 때, 자식 컴포넌트도 함께 리렌더링된다. 하지만 자식 컴포넌트로 전달되는 props가 이전과 동일하다면, 자식 컴포넌트는 리렌더링되지 않도록 할 수 있다.

호출

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • SomeComponent: 첫 번째 인자로는 메모제이션하고자 하는 컴포넌트를 넘겨준다.
  • arePropsEqual: 필요한 경우, 두 번째 인자로는 사용자 정의 비교 함수를 넘겨줄 수 있다.
    • props가 동일해 리렌더링이 필요하지 않은 경우 true를, 아닌 경우 false를 반환한다.

주의점

memo는 기본적으로 props를 얕은 비교(shallow comparison)로 비교한다. 따라서 객체나 배열을 props로 전달한 경우, 내용이 같더라도 참조가 변경되면 다시 렌더링이 발생할 수 있다.

이런 경우, 위에서 언급한 사용자 정의 비교 함수를 통해 이전 배열과 새로운 배열을 비교하도록 정할 수 있다.

useMemo

useMemo는 리렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook이다.

호출

useMemo(calculateValue, dependencies);
  • calculateValue: 캐싱할 값을 계산하는 함수이다.
    • 초기 렌더링 시에 calculateValue함수가 호출된다.
    • 이후 dependencies가 변경되지 않았다면 다음 렌더링에서는 calculateValue 함수 호출 없이 동일한 값을 반환하도록 값을 저장한다.
  • dependencies: calculateValue 함수 안에서 참조된 모든 값들의 목록이다. 해당 값들이 변경된 경우, 다음 렌더링에서는 새로 calculateValue 함수를 호출해 값을 재계산하게 된다.

주의점

  • useMemo는 Hook이기 때문에 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출할 수 있다.
  • 복잡한 계산이 있는 경우나 값이 자주 변경되지 않는 경우에 사용하면 좋다.

useCallback

리렌더링 사이에 함수 정의를 캐싱할 수 있게 해주는 React Hook이다.

호출

useCallback(fn, dependencies);
  • fn: 캐싱할 함수이다.
    • 초기 렌더링 시에 fn 함수를 반환한다. (호출하는 것은 아니다.)
    • 이후 dependencies가 변경되지 않았다면 다음 렌더링에서는 같은 함수를 다시 반환한다.
    • 변경되었다면 함수를 새로 반환하고, 재사용할 수 있도록 저장한다.
  • dependencies: fn 함수 안에서 참조된 모든 값들의 목록이다.

주의점

  • useCallback는 Hook이기 때문에 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출할 수 있다.

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

컴포넌트의 생명주기(LifeCycle)이란, 컴포넌트가 생성되고 제거되기까지의 여러 단계를 이른다.

각 생명주이게서는 특정한 메소드가 호출되며, 이러한 메소드들을 오버라이딩할 수도 있다.

Mouting (생성 단계)

  • constructor
    • 컴포넌트가 생성될 때 호출되는 생성자 메소드
    • 초기 상태의 설정 및 이벤트 핸들러의 바인딩이 주로 이루어진다.
  • static getDerivedStateFromProps
    • props로부터 상태를 동기화하기 위해 호출되는 메소드
    • React 16.3부터 도입됨
  • render
    • 컴포넌트의 UI를 렌더링한다
  • componentDidMount
    • 컴포넌트가 실제 DOM에 삽입된 후 호출되는 메소드
    • 초기 데이터 로딩 등의 작업에 사용된다.

Updating (업데이트 단계)

  • static getDerivedStateFromProps
    • props로부터 상태를 동기화하기 위해 호출된다.
  • shouldComponentUpdate
    • 컴포넌트의 리렌더링 여부를 결정한다.
  • render
    • UI를 렌더링한다.
  • getSnapshotBeforeUpdate
    • 컴포넌트가 업데이트되기 직전에 호출된다.
  • componentDidUpdate
    • 컴포넌트의 업데이트가 완료된 후 호출된다.

Unmouting (제거 단계)

  • componentWillUnmount
    • 컴포넌트가 제거되기 직전에 호출된다.
    • 리소스 정리나 이벤트 해제 등의 작업을 수행한다.

Error Handling

  • static getDerivedStateFromError
    • 자식 컴포넌트의 렌더링 중에 오류가 발생했을 때 호출된다.
  • componentDidCatch
    • 자식 컴포넌트에서 오류가 발생했을 때 호출된다.

함수 컴포넌트에서는 Hook을 이용하여 상태 및 생명주기 기능을 사용할 수 있다.

  • useState
    • state를 생성한다.
    • 현재 state값과 해당 state를 업데이트할 수 있는 setFunction을 반환한다.
  • useEffect
    • 함수 컴포넌트에서 부수 효과(side effect)를 수행할 때 사용한다
  • useContext
    • React의 context를 사용할 때 사용한다. 컴포넌트 트리 전체에서 전역적인 값을 공유할 때 유용하다.
  • useReducer
    • 복잡한 상태 로직을 효과적으로 관리하기 위해 사용한다.
    • 상태 업데이트 로직을 외부 함수로 분리할 수 있다.
  • useMemo
    • 계산 비용이 많이 드는 함수의 결과값을 기억한다.
    • 의존성 배열에 있는 값이 변경될 때만 해당 값을 다시 계산한다.
    • 불필요한 연산을 방지하고 성능을 최적화할 수 있다.
  • useCallback
    • 메모제이션된 콜백 함수를 생성한다.
    • 자식 컴포넌트가 불필요하게 다시 렌더링되는 것을 방지한다.
  • useRef
    • 상태 업데이트를 트리거하지 않는 값을 생성하거나, DOM 요소에 대한 참조를 생성한다.
  • useLayoutEffect
    • 렌더링이 발생하기 전에 동기적으로 실행된다.
    • 브라우저의 레이아웃을 읽어오고, 그에 따른 사이드 이펙트를 처리한다.

참고

Copy link

@hae2ni hae2ni left a comment

Choose a reason for hiding this comment

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

체고!! 많이 고민하신게 느껴지는 것 같아요

@@ -0,0 +1,17 @@
import { Title } from './styles';

export default function Header() {
Copy link

Choose a reason for hiding this comment

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

jsx 문법의 react component는 파일 확장자가 jsx인게 조금더 다른 순수 js랑 나중에 구분하기 편할 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

헉 그렇네용 다음 플젝부터는 jsx를 명시해야겠어요!!

<Container>
<Header>
<h3>
{subject.title}{' '}
Copy link

Choose a reason for hiding this comment

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

요런 {' ' } 친구는 지워주셔도 좋을 것 같아용!

import TaskList from 'components/TaskList';
import { Container, Header, TaskCount } from './styles';

export default function Subject({ state, subject, deleteSubject, taskHooks }) {
Copy link

Choose a reason for hiding this comment

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

보니까 subject props가 object 형식인 것 같은데,
const { id, taskList, title } = subject
처럼 구조분해할당으로 시작하셔도 좋을 것 같아요!

Copy link
Author

@psst54 psst54 Sep 23, 2024

Choose a reason for hiding this comment

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

앗 구조 분해 할당을 사용하면 코드가 더 깔끔해질 것 같네요. 감사합니다!


function onAdd(event) {
event.preventDefault();
if (!inputRef.current.value) {
Copy link

Choose a reason for hiding this comment

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

input부분 ref로 쓰신거 진짜 자잘한 건데 많이 고민하신 것 같아요! 쨩쨩

const inputRef = useRef(null);

function onAdd(event) {
event.preventDefault();
Copy link

Choose a reason for hiding this comment

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

아 맨날 까먹는 이것,,ㅠㅠㅠ

} from './styles';

export default function KanbanBoard() {
const subjectHooks = useSubject();
Copy link

Choose a reason for hiding this comment

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

커스텀훅을 사용하신 것 같네여!!!

Comment on lines +63 to +66
const currentSubjectList = subjectList[state]; // state in which current state is located
const currentSubject = currentSubjectList.find(
(subject) => subject.id === subjectId
);
Copy link

Choose a reason for hiding this comment

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

Suggested change
const currentSubjectList = subjectList[state]; // state in which current state is located
const currentSubject = currentSubjectList.find(
(subject) => subject.id === subjectId
);
const currentSubject =subjectList[state]?.find(
(subject) => subject.id === subjected
);
if(!currentSubject) return;

혹시나 해서 넣어보았습니당

Copy link

@jiwonnchoi jiwonnchoi left a comment

Choose a reason for hiding this comment

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

코드 짜신 방식이 저와 달라 새로워서 흥미롭게 보았습니다! subject 내에 또다시 task를 추가하는 형식이 복잡하셨을텐데 구현하시느라 수고많으셨습니다!👍🔥

Comment on lines +10 to +11
"htmlWhitespaceSensitivity": "css",
"cssWhitespaceSensitivity": "css"

Choose a reason for hiding this comment

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

프리티어 속성 찾아보면서 못 봤던 속성값인데 덕분에 찾아보고 알아갑니다👍👍

Comment on lines +14 to +22
gap: 1rem;

padding: 2rem;
width: 100%;
max-width: 1200px;

@media (max-width: 1000px) {
max-width: 600px;
}

Choose a reason for hiding this comment

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

rem 단위와 px 단위를 혼용하신 이유가 특별히 있는지 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

미디어 쿼리는 개발자 도구에서 화면을 조정할 때 나타나는 px 값을 참고해서 사용하다 보니 혼용된 것 같아요! 미디어 쿼리에서는 보통 px 단위를 많이 사용하는 것 같은데, rem을 사용하는 것도 여러 장점이 있다고 하네요.

저는 이 링크를 읽어봤어요! : https://onlydev.tistory.com/128

지원님도 px과 rem을 혼용하셨던데 지원님은 어떤 이유로 같이 사용하고 계신가요?

Choose a reason for hiding this comment

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

저는 rem 단위로 모두 통일하고자 했는데 혹시 px을 발견하셨다면 제가 깜빡 익숙한 px로 잘못 쓴게 아닐까..싶습니다 😅😅 달아주신 링크도 잘 읽어보았습니다! 감사합니다

Comment on lines +4 to +12
return (
<header>
<Title>{getCurrentDate()}</Title>
</header>
);
}

function getCurrentDate() {
return new Intl.DateTimeFormat('ko-KR', {

Choose a reason for hiding this comment

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

return문을 먼저 작성하고 그 뒤에 함수를 정의하는 방식이 특이한 것 같아 찾아보았는데, 간단한 컴포넌트이고 규모가 작아서 UI구조를 바로 볼 수 있다는 점에서 return문을 먼저 작성하는 방식도 좋은 것 같아요! 그치만 복잡한 컴포넌트나 여러 함수가 필요하게 된다면 함수를 먼저 정의하는 것이 가독성, 유지보수 측면에서 유리하다고 합니다!

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 +134 to +139
const newSubjectList = {
...subjectList,
[state]: subjectList[state].map((subject) =>
subject.id !== subjectId ? { ...subject } : newSubject
),
};

Choose a reason for hiding this comment

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

Suggested change
const newSubjectList = {
...subjectList,
[state]: subjectList[state].map((subject) =>
subject.id !== subjectId ? { ...subject } : newSubject
),
};
const newSubjectList = {
...subjectList,
[state]: subjectList[state].map((subject) =>
subject.id == subjectId ? { ...subject } : newSubject
),
};

배포된 링크에서 subject 내에 task 추가가 안되고 있어서 이래저래 디버깅해보다가 위와 같이 바꾸니 제대로 뜨는 것을 확인했습니다! 새로 추가할 task의 subject id가 동일한 해당 subject 내의 태스크를 업데이트 해야한다고 저는 생각해서 수정해보았는데 처음 코드 짜신 의도까진 파악이 안되어서 한 번 더 직접 디버깅 해보시면 좋을 것 같습니다! 😊

Copy link
Author

Choose a reason for hiding this comment

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

🤦🤦 사실 목표 subject의 id를 찾아서 변경하는거니 원래 코드대로 subject.id !== subjectId ? { ...subject } : newSubject (이것도 subject.id !== subjectId ? subject : newSubject)로 쓰면 되긴합니당,,)라고 쓰면 되는데

더 엄청난 문제가 있었네요... 해당 함수의 인자로 subjectId를 넘겨주지 않아서 subjectId = undefined인 채로 조건문이 돌아 변경이 안됐습니다🤣🤣🤣

꼼꼼히 확인해주셔서 감사합니다😭😭😭😭

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.

안녕하세요 다혜님!!!
미완료 / 진행중 / 완료로 subject를 분리해서 task를 관리하는 todo list 방식이 정말 인상깊었습니다!!
그리고 커스텀 훅까지 작성하신 것으로 보아, 체계적인 투두 관리를 위해 고민하신 흔적이 느껴졌어요!! bb
image (2)

할일을 추가/삭제해도 상태 업데이트가 제대로되고 있지 않는 문제가 있는 것 같은데 이 부분만 수정하시면 될 것 같아요! 이번 과제도 수고 많으셨습니다!


setSubjectList(newSubjectList);
localStorage.setItem(KEY, JSON.stringify(newSubjectList));
toggleSubjectState(state, newState, currentSubjectList, newSubject);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분에서, subjectId를 전달하지 않아서 상태 업데이트가 제대로 이루어지지 않는 것 같습니다!! 이 부분이 누락되어 있으면,
할일을 삭제 버튼을 눌러도 삭제된 상태가 반영이 안되는 것 같아요!!

toggleSubjectState(state, newState, currentSubjectList, newSubject,subjectId); 

이런식으로 수정이 필요할 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

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

헉 디버깅 하시느라 너무고생하셨어요...!🥹🥹🥹


setSubjectList(newSubjectList);
localStorage.setItem(KEY, JSON.stringify(newSubjectList));
toggleSubjectState(state, newState, currentSubjectList, newSubject);
Copy link
Collaborator

Choose a reason for hiding this comment

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

할일 삭제와 마찬가지로 이부분에서도 subjectid가 전달이 안되어서 할일을 추가해도 반영이 안되는 것 같습니다!!
toggleSubjectState(
state,
newState,
currentSubjectList,
newSubject,
subjectId
);

이런식으로 수정해야할 것 같습니다!


export default function useSubject() {
const [subjectList, setSubjectList] = useState([]);

const addSubject = (newSubjectTitle) => {
const newSubject = {
id: uuidv4(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

고유한 ID를 생성하기 위해 설계된 라이브러리가 있는 줄 처음 알았어요! 배워갑니다!

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