Skip to content

[8팀 김상수] Chapter 1-3. React, Beyond the Basics#34

Open
k-sang-soo wants to merge 16 commits intohanghae-plus:mainfrom
k-sang-soo:main
Open

[8팀 김상수] Chapter 1-3. React, Beyond the Basics#34
k-sang-soo wants to merge 16 commits intohanghae-plus:mainfrom
k-sang-soo:main

Conversation

@k-sang-soo
Copy link

@k-sang-soo k-sang-soo commented Jul 21, 2025

과제 체크포인트

배포 링크

https://k-sang-soo.github.io/front_6th_chapter1-3

기본과제

equalities

  • shallowEquals 구현 완료
  • deepEquals 구현 완료

hooks

  • useRef 구현 완료
  • useMemo 구현 완료
  • useCallback 구현 완료
  • useDeepMemo 구현 완료
  • useShallowState 구현 완료
  • useAutoCallback 구현 완료

High Order Components

  • memo 구현 완료
  • deepMemo 구현 완료

심화 과제

hooks

  • createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
  • useShallowSelector 구현
  • useStore 구현
  • useRouter 구현
  • useStorage 구현

context

  • ToastContext, ModalContext 개선

과제 셀프회고

이번에도 최대한 AI를 사용하지 않고 풀려고 노력했고, 고민하는 시간을 갖고 해결 방법이 떠오르지 않는다면 그 때 AI에게 힌트를 받는 식으로 진행해서 뇌를 최대한 고통스럽게 괴롭혔다.. 괴롭지만 이렇게 뇌를 괴롭혀야 뇌가 발전한다고 어떤 뇌학자의 말씀! 기본과제에서는 막히는 부분이 많았지만 그래도 이해를 하려고 노력한 보람이 있는지 심화과제에서는 AI 도움 없이도 빨리 풀었다. 너무 짜릿했다 도파민 폭발! 그리고 이번에는 최대한 모든 문제해결의 모든 과정들을 문서화하려고 노력했다 향해 3주차에 이제서야 어떻게 공부를 해야될지 사아알짝 감이 잡힌것 같다.

기술적 성장

  • useState, useRef
    useState 를 그냥 아무 생각 없이 상태를 반응형으로 만들려고 사용했고 더 깊이 생각해보질 않았는데, useRef를 구현할 때 useState가 사용된다는 걸 보고, useState를 통해 몰랐던 리액트의 동작 원리들을 알게 돼서 너무 좋았습니다. lazy initialization 에 대해서도 알게 되고 어떻게 사용되는지 lazy initialization를 적용하지 않았을 때는 어떤 차이점이 있는지 알게 됐습니다.

  • useMemo, useCallback
    useMemo를 구현할 땐 useRef를 사용하고, useCallback을 구현할 땐 useMemo를 사용한다는 걸 알게 됐습니다. 리액트는 useStateuseRef가 굉장히 중요한 hook 이였구나 깨달았습니다. 그리고 useMemo로 상태를 비교할 때 깊은 비교를 하지 않는 이이유와 리액트가 참조 비교를 원칙으로 하고 왜 그런지 깨닫게 됐습니다.

  • useShallowState, useAutoCallback
    참조 비교가 아닌 얕은 비교, 깊은 비교를 사용한다면 참조 비교와는 다르게 불필요한 리렌더링을 줄일 수 있구나 알게 됐습니다.

  • useSyncExternalStore
    리액트 18에서 새롭게 생긴 useSyncExternalStore를 사용해서 외부 상태를 구독해서 사용하는 방법을 깨달았다. 단순히 브라우저 API를 사용할 때 쓰면 좋다 라고만 인지하고 있었는데 실제로 써보고 어떤 문제를 해결하려고 나왔는지 찾아보니 유용하게 쓸 수 있는 hook 이라는 걸 깨달았습니다.

자랑하고 싶은 코드

자랑하고 싶은 "코드"보다는 이번 과제를 제대로 습득하기 위해서 어떤 고민과 생각을 하면서 문제를 해결했는지 열심히 문서화하려고 노력했던 점이 스스로 자랑스럽다고 생각합니다! 정리를 잘했다고 보기는 어렵지만 어떻게 공부해야 될지 조금 깨달은 것 같고 향해를 하면서 공부하는 방법을 더욱 더 깨닫길 기원하면서 열심히 써봤습니다!

여기에 제 모든 정수를 담았습니다!!!!
이번 과제 총 정리

칭찬해주고 싶은 동료

  • 5팀의 이지훈
    제가 useMemo로 한참 고민하고 있을 때 비록 다른 팀이지만 지훈님께서 "교수님"이라는 타이틀을 얻을 정도로 잘 설명해준다고 해서 여쭤보러가니 아주 흔쾌하게 시간을 내주시고 과제에 대한 핵심 개념과 힌트를 주셔서 풀 수 있게 됐습니다!!

  • 2팀의 유운우
    사전 스터디 때 말씀을 나눠보니 윤우님께서 아는 게 많고, 실력도 뛰어나다는 인상을 받았습니다. 그래서 비록 다른 팀이지만 도움을 요청했고 흔쾌하게 시간을 내주셨습니다. 덕분에 useShallowState 와 심화 과제의 마지막 문제 ToastProvider 최적화를 할 수 있게 됐습니다!

저도 이 두분에게 받은 좋은 시너지를 저희 팀에게 전달하고 싶어 제가 문서화한걸 공유해보기도 했습니다! 함께 성장하는 향해!!

개선이 필요하다고 생각하는 코드

개선이 필요하거나 코드 관련 고민은 리뷰 받고 싶은 내용 쪽에 적은 것과 동일합니다!

학습 효과 분석

  • 가장 큰 배움이 있었던 부분
    리액트의 근간이 되는 useState에 대해 깊게 이해할 수 있었고, 단순히 상태를 반응형으로 만드는 도구로만 생각했었는데, 지금 생각하면 왜 이런 생각을 못했을까 라고 생각했던 리렌더링 사이에서도 값을 유지하는 원리를 알게된게 큰 수확이었습니다. 이 개념을 통해서 모든 hook들이 파생되는 것 같아 가장 큰 배움이었습니다.

  • 추가 학습이 필요한 영역
    useState도 직접 구현해볼 수 있었으면 좋았을 것 같습니다. 물론 과제하면서 시간 남으면 해봐야지 라고 생각했지만 시간이 없었습니다ㅠㅠ
    나중에 꼭 직접 구현해보겠습니다.

  • 실무 적용 가능성
    코드들에 대한 실무 적용 가능성보다 리액트의 동작 원리들을 이해했고 이런 동작 원리들을 바탕으로 설계할 때나 디버깅할 떄 매우 유용할 것 같습니다!

과제 피드백

  • 정답이 어느정도 정해져 있고 그 안에서만 고민할 수 있어서 집중하기 좋았습니다!
  • useState 를 직접 구현하는 것도 포함 되어있었더라면 좋았겠다 라고 생각했습니다. useRef를 만들기 위해서는 useState 가 필요하니 순서대로 생각하면 있는게 좋지 않았을까 생각이 듭니다. 아마도 코치님들께서도 분묭 생각 하셨을 것 같은데 어떤 이유 때문에 빠졌을 지 궁금합니다.

학습 갈무리

리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.

리액트는 jsx 라는 특별한 문법을 통해 UI 구조를 선언하고, 이를 트랜스파일러가 React.createElement 호출로 ReactElement 객체를 만드는데, 이 때 객체를 가상 DOM이라고 불리는 내부 구조로 사용된다.
최초 렌더링 시에는 이 Virtual DOM을 기반으로 실제 DOM을 생성하고, 이 후 props 나 state 가 변경되면 리렌더링이 일어나고, 변경된 Virtual DOM과 이전 Virtual DOM을 diffing 알고리즘을 통해 달라진 걸 비교해서 달라진 부분만 DOM에 반영한다. 이 과정을 Reconciliation 이라 부른다.

리액트는 최적화하기 위해서는 왜 리렌더링이 일어나는지를 먼저 알아야 한다. 리렌더링이 일어나는 경우는 props, state, 부모 컴포넌트가 리렌더링되면 일어난다. 이 때 변수나 함수를 참조 비교를 하게 되는데 이전과 동일하지 않는다면 변경이 일어난다. 변경이 일어나지 않게 하기 위해서는 변수나 함수를 useMemo, useCallback, memo와 같은 메모이제이션 도구를 활용해 참조를 유지해주고, 의존성 값이 변했을 때만 새로 생성하게 해야된다.

메모이제이션에 대한 나의 생각을 적어주세요.

메모이제이션에 대해서 항상 코드를 짤 때 적용해줘야 되나? 많은 고민들이 있었지만 리액트 공식문서나 여러 개발자들의 의견을 봤을 때 정말 필요하다고 느껴졌을 때 적용하라는 의견을 참고하여 꼭 필요하다고 느껴질 때만 적용하는 걸로 결정했다. 불필요한 리렌더링이 되는 것 처럼 보이지만 문제가 일어나지 않은 상황에서 이를 막기 위해 메모이제이션을 사용한다면 그것 또한 리소스를 잡아 먹기 때문에 정말 문제가 있을 때만 도입하자!

컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.

컨텍스트를 사용하면 깊은 하위 컴포넌트까지 데이터를 전달해주고, 여러 컴포넌트들 간의 상태를 공유할 수 있어서 좋다. 하지만 컨텍스트를 사용하면 코드의 구조가 복잡해져서 읽기가 어려워지고, 상태가 변경되면 구독하고 있는 모든 컴포넌트가 리렌더링 되는 구조 때문에 최적화가 필요하다면 최적화를 진행하기 까다롭다. Zustand, Jotai 같은 최적화도 잘 되어있고 사용하기도 쉬운 전역 상태 라이브러리들이 나와있어 컨텍스트를 사용할 일이 있을까 싶기도 하다. 물론 외부 라이브러리의 의존성이 없고 리액트의 api를 사용한다는게 장점이 될 수도 있지만!

리뷰 받고 싶은 내용

  1. useShallowState 의 타입 부분 질문 드립니다!

처음에는 타입을 원래 구현 되어있던 <T>(initialValue: Parameters<typeof useState<T>>[0]) 을 유지한 채 구현을 시도했습니다.

export const useShallowState = <T>(initialValue: Parameters<typeof useState<T>>[0]) => {
  const [state, setState] = useState(initialValue);
  const setValueShallow = useCallback((newValue: T) => {
    setState((prevValue: T) => {
      return shallowEquals(prevValue, newValue) ? prevValue : newValue;
    });
  }, []);

  return [state, setValueShallow] as const;
};

그런데 setState((prevValue: T) => { 부분에서 아래와 같은 타입 에러가 발생했습니다.

TS2345: Argument of type (prevValue: T) => T is not assignable to parameter of type SetStateAction<undefined>
Type 'undefined' is not assignable to type 'T'

제가 이해한 바로는, Tundefined가 들어올 가능성을 타입스크립트가 열어두면서 undefined로 추론하게 되는 케이스까지 포함된 것 같습니다.

그래서 결국 initialValue가 들어오는 순간 T 타입을 확정 하는 <T>(initialValue: T | (() => T)) 으로 해결하긴 했습니다.

export const useShallowState = <T>(initialValue: T | (() => T)) => {
  const [state, setState] = useState(initialValue);
  const setValueShallow = useCallback((newValue: T) => {
    setState((prevValue) => {
      return shallowEquals(prevValue, newValue) ? prevValue : newValue;
    });
  }, []);

  return [state, setValueShallow] as const;
};

<T>(initialValue: Parameters<typeof useState<T>>[0]) 을 사용하더라도 안전하게 타입 추론을 맞추는 방법이 있는지 여쭤보고 싶습니다!

  1. useAutoCallback 의 타입 부분 질문 드립니다!
    예를 들어 fn(name: string, age: number) ⇒ string 형태일 때 useCallback 안 의 args 타입을 맞추기 위해 (...args: Parameters<T>): ReturnType<T> 이렇게 타입을 지정했습니다.
    이런 식으로 작성하면 타입이 정확한 매칭이 되지 않아 에러가 발생해서, 결국 마지막에 as T타입 단언을 해야지만 에러가 사라졌습니다.
    근데 이렇게 타입 단언 선언을 하는 것이 괜찮은 방법인지 의문이 듭니다. 강제로 맞춘 느낌이 들고 any를 사용한것 같은 느낌이 들어 찝찝합니다. 더 안전하게 타입을 맞추는 방법이 있을지 궁금합니다!
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
  const fnRef = useRef(fn);
  fnRef.current = fn;

  return useCallback((args: Parameters<T>): ReturnType<T> => {
    return fnRef.current(...args);
  }, []) as T;
};
  1. useShallowStateuseAutoCallback 같은 훅들의 목적은 괜찮다고 생각은 들지만, 리액트는 기본적으로 참조 비교로 리렌더링 여부를 판단하고, 이런 동작은 성능을 위한 최적화이기도 하지만, 예측 가능한 동작과 불변성을 유지하기 위한 철학이라고 알고 있습니다.
    그래서 useShallowStateuseAutoCallback 처럼 동작을 바꾸는 훅을 그냥 써도 될까? 라는 고민이 들었고 리액트 철학과 어긋나는 건 아닐까 싶은 생각이 들었습니다.
    혹시 실무에서 자주 사용하시거나 특정 상황에서만 제한적으로 사용하는 편이신가요? 예를 들어 어떤 상황에서 사용할 수 있을까요? 사용하신다면 사이드 이펙트가 생긴 적은 없나요? 깊은 지식이 부족해서 이게 써도 괜찮은 도구인지 판단이 잘 안서서 여쭤봅니다!

  2. 고차 컴포넌트나 고차 함수는 잘 활용하면 코드의 유연성과 재사용성 측면에서 굉장히 유익하다고 생각합니다. 하지만 막상 이런 방식으로 작성된 코드를 읽을 때는 한 번에 이해하기가 어려운 경우가 많습니다.
    개인적으로 좋은 코드는 읽기 쉬운 코드도 중요한 요소라고 생각하는데, 그런 점에서 고차 컴포넌트나 고차 함수가 반드시 좋은 코드라고 말할 수 있을 지 고민됩니다. 그냥 제가 익숙하지 않아서 그렇게 느끼는 걸까요?
    실무에서도 이런 고차 컴포넌트나 고차 함수를 자주 사용하시는지, 만약 신입 개발자가 새로 들어온다면 이런 구조를 바로 이해하기는 어려울 것 같은데 이런 경우에는 어떻게 도와주거나 가이드하시는 지 궁금합니다!

@JiHoon-0330
Copy link

상수님 공부 열심히 하신 게 느껴지는 것 같습니다

@ckdwns9121
Copy link
Member

와 상수님 엄청 깔끔하게 정리잘하셧네요

@k-sang-soo
Copy link
Author

@JiHoon-0330 교수님께 이런 칭찬을 따흑ㅠㅠ

@k-sang-soo
Copy link
Author

@devchangjun 창준님 2주차 PR보고 분발 했습니다🔥

Copy link

@unseoJang unseoJang left a comment

Choose a reason for hiding this comment

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

안녕하세요 상수님~ 1-3 과제도 고생하셨습니다.
전체적으로 되게 직관적이고 눈으로 보이는 코드를 좋아하시는 부분들이 느껴지네요
타입스크립트를 사용하면 생각보다 타입스크립트를 사용하는 유리한점을 적극 고민하면서 리팩토링하면 한층 깔끔하고 정돈된 코드를 볼수 잇을 것 같아요!

그 외 나머지 코드들은 직관적이고 헬퍼함수로 잘 분기되어 깔끔한 코드를 볼수 있었네요
이번주 2-1 과제도 화이팅이고 과제 다 끝나고 복습하시면 지금 보다 훨씬 좋은 가독성있는 코드를 볼수 있을 것 같아요

과제 수고하셨습니다~!

import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer";
import { initialState, toastReducer, type ToastType, useToastActions } from "./toastReducer";
import { debounce } from "../../utils";
import { useAutoCallback } from "@hanghae-plus/lib";

Choose a reason for hiding this comment

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

상수님도 useAutoCallback으로 관리를 해주셨군요
유열님이 구성해주신 React 내장 객체 useCallBack도 한번 확인하면 좋을 것 같아요!

https://github.com/hanghae-plus/front_6th_chapter1-3/pull/15/files

return deepEqualsObjects(a, b);
}

return true;

Choose a reason for hiding this comment

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

??마지막에 모든 조건 다 지나가고 무조건 true 값을 던져주네요??
명시적으로 안맞지 않나 라는 생각이들어요
return a === b;가 맞다는 생각이 드네요

@@ -1,3 +1,30 @@
import { typeUtils } from "../utils/typeUtil.ts";
import { deepEqualsArrays } from "./deepEqualsArrays.ts";
import deepEqualsObjects from "./deepEqualsObjects.ts";

Choose a reason for hiding this comment

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

해당 함수 util로 빼주셧네요 깔끔해요

return false;
}

for (let i = 0; i < objectALength; i++) {

Choose a reason for hiding this comment

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

반복문이 deepEquals를 호출을 안하네요?

  if (!objectB.hasOwnProperty(key)) {
    return false;
  }

  if (!deepEquals(objectA[key], objectB[key])) {
    return false;
  }

이게 빠진것같은데...맞을까요??
아니면 아예 직관적으로 for...in 루프보다 Object.keys() + 루프가 더 안전하고 직관적이지 않을까 생각이드네요

Choose a reason for hiding this comment

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

LGTM 👍 주석이 잘 달려 잇어서 확인이 편하네요

Choose a reason for hiding this comment

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

typeUtil 따로 정리해서 헬퍼함수로 정리해두신 부분 너무 좋네요
// 'Plain Object'만 true
라고 적어두면 이해에 더 도움이 될것 같아요

return Component;
return function MemoizedComponent(props: P) {
const prevPropsRef = useRef<P | null>(null);
const prevComponentRef = useRef<ReactNode | Promise<ReactNode> | null>(null);

Choose a reason for hiding this comment

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

Promise 로 들어가야되는 이유가 혹시 잇을까요???
헷갈리네요

const key = objectAKeys[i];

// 값이 다르거나, B에 해당 key가 없으면 다름
if (objectA[key] !== objectB[key] || !Object.hasOwn(objectB, key)) {

Choose a reason for hiding this comment

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

이부분 생각보다 순서가 중요해서 key가 없는데 접근해서 undefined !== undefined로 통과될 수 있어요

if (!Object.hasOwn(objectB, key) || objectA[key] !== objectB[key]) {
  return false;
}

이렇게 구성하기를 권장드려요

}

// 둘 중 하나라도 null 또는 undefined면 false
if (!arrayA || !arrayB) {

Choose a reason for hiding this comment

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

인자값 받을떄 배열로 받고 null값으로 받을수가 있나요??
이런부분들이 타입스크립트를 사용할때 장점인 것 같아요

}

// 둘 중 하나라도 null 또는 undefined면 false
if (!arrayA || !arrayB) {

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.

4 participants