[9팀 임규원] Chapter 1-3. React, Beyond the Basics#39
[9팀 임규원] Chapter 1-3. React, Beyond the Basics#39q1Lim wants to merge 12 commits intohanghae-plus:mainfrom
Conversation
susmisc14
left a comment
There was a problem hiding this comment.
규원님, 3주차도 정말 고생 많으셨어요! 초반의 어려움을 멋지게 이겨내고, 매주 점점 더 발전하는 모습을 보니 정말 대단하다는 생각이 듭니다. 그 노력이 빛을 발하는 것 같아 저도 기분이 좋네요. 앞으로도 항상 응원하겠습니다!
There was a problem hiding this comment.
shallowEquals와 deepEquals의 로직이 많이 유사해요! 코드 중복을 줄이 위해, deepEquals에 depth 매개변수를 추가하여 비교 깊이를 제어하는 것은 어떨까요? 이렇게 하면 shallowEquals는 단순히 deepEquals(a, b, 1)을 호출하는 방식으로 간결하게 구현할 수 있습니다.
| if (Array.isArray(objA) && Array.isArray(objB)) { | ||
| if (objA.length !== objB.length) return false; | ||
|
|
||
| for (let i = 0; i < objA.length; i++) { | ||
| if (!deepEquals(objA[i], objB[i])) return false; | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
배열과 객체를 비교하는 방식에 대해 궁금한 점이 있어요! 저는 처음에 배열도 '인덱스를 키로 갖는 객체'와 유사하다고 생각해서, 두 타입을 구분하지 않고 객체 비교 로직으로 한 번에 처리했었는데요. 규원님께서는 Array.isArray() 등으로 배열과 객체를 명확하게 구분해서 별도의 로직으로 처리해주셨더라고요. 이러한 방식이 비교의 정확성을 높이는 가장 확실한 방법이라고 생각하신 걸까요?
There was a problem hiding this comment.
키로 갖는 객체로 유사하지만 배열은 인덱스 기반이기 때문에 배열의 순서도 체크해야할 것 같아서 배열은 따로 분리해서 체크했습니다!
| for (let i = 0; i < objA.length; i++) { | ||
| if (!deepEquals(objA[i], objB[i])) return false; | ||
| } |
There was a problem hiding this comment.
객체의 모든 키가 특정 조건을 만족하는지 검사하는 로직은 every() 배열 메서드를 사용하면 더 간결하게 작성할 수 있습니다. 예를 들어, for 루프 대신 아래와 같이 한 줄로 표현할 수 있어 가독성을 높이는 데 도움이 될 것 같습니다.
return keysA.every(key =>
Object.hasOwn(objB, key) && deepEquals(objA[key], objB[key])
);There was a problem hiding this comment.
인덱스를 사용해야한다고 생각해서 일반적인 for문을 썼는데, every를 까먹고 있었네요! 리마인드 된 것 같아서 감사합니다 ㅎㅎ
| const prevRef = useRef(fn); | ||
| prevRef.current = fn; |
There was a problem hiding this comment.
저는 if 문을 추가해서 이전 callback과 현재 callback을 비교하는 과정을 추가했었는데 지금와서 생각해보면 과한 로직인 것 같아요.
There was a problem hiding this comment.
타입 안정성을 더 높일 수 있는 방법이 있어서 공유드려요! 실제 React의 useRef는 useRef()처럼 초기값 없이 호출하는 경우와 useRef(0)처럼 초기값을 주는 경우의 타입이 다르게 추론돼요.
TypeScript의 **함수 오버로딩(Function Overloading)**을 사용하면 이런 케이스들을 더 정교하게 처리할 수 있습니다. 예를 들어, 아래처럼 여러 시그니처를 선언하는 거죠.
import { useState } from "react";
interface MutableRefObject<T> {
current: T;
}
export function useRef<T = undefined>(): MutableRefObject<T | undefined>;
export function useRef<T>(initialValue: T): MutableRefObject<T>;
export function useRef<T>(initialValue?: T): MutableRefObject<T | undefined> | MutableRefObject<T> {
return useState(() => ({ current: initialValue }))[0];
}이렇게 하면 useRef를 초기값 없이 호출하는 케이스도 지원할 수 있고, 타입스크립트가 ref.current의 타입을 더 정확하게 추론해주는 장점이 있습니다!
There was a problem hiding this comment.
오 이거 두현님 코드에서 유사한 걸 봤습니다! 지수님이 공유주신 것이 더 상세하네요~ 공유 감사합니다!
|
|
||
| const setShallowState = useCallback((newValue: T) => { | ||
| // shallowEquals를 통해 상태 변경을 감지 | ||
| setState((prev) => (shallowEquals(prev, newValue) ? prev : newValue)); |
There was a problem hiding this comment.
저는 setState 바깥에서 새로운 값을 계산했는데, setState 콜백 안에서 prevValue를 사용해야 여러 번의 상태 업데이트가 연달아 일어날 때도 stale state 문제 없이 항상 최신 상태를 기반으로 값을 계산할 수 있겠네요. 제가 놓쳤던 부분인데 다른 분들은 다 꼼꼼히 챙기셔서 약간 슬퍼졌어요...
| // 임의로 타입 조정 initialValue: T | (() => T) | ||
| const [state, setState] = useState<T>(initialValue); | ||
|
|
||
| const setShallowState = useCallback((newValue: T) => { |
There was a problem hiding this comment.
한 가지 개선할 점을 제안하자면, useState의 set 함수처럼 setShallowState도 함수를 인자로 받을 수 있도록 기능을 확장하는 것은 어떨까요?
예를 들어, setShallowState(prev => ({ ...prev, count: prev.count + 1 }))와 같은 패턴을 지원하면 사용자가 더 유연하게 상태를 관리할 수 있게 되어 훅의 완성도가 높아질 것 같습니다.
There was a problem hiding this comment.
과제의 요구사항에 맞게 정확히 State와 Command 컨텍스트를 분리하신 것 같아요! 추가로 제가 평소에 즐겨 사용하는 createSafeContext라는 패턴을 소개해 드리고 싶어요.
createContext를 사용할 때마다 초기값을 반드시 설정해야 하거나 Provider 외부에서 컨텍스트를 사용하는 실수를 방지하기 위해 null 체크를 하고 값을 useMemo로 감싸는 등 반복적인 작업을 하나로 묶어줄 수 있어요.
import { createContext, createElement, useContext, useMemo } from "react";
export function createSafeContext<ContextValue extends object | null>(
rootComponentName: string,
defaultValue?: ContextValue,
) {
const Context = createContext<ContextValue | undefined>(defaultValue);
const Provider = ({ children, ...rest }: ContextValue & { children?: React.ReactNode }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const value = useMemo(() => rest, Object.values(rest)) as ContextValue;
return createElement(Context, { value }, children);
};
const useSafeContext = (consumerName: string) => {
const ctx = useContext(Context);
if (ctx) return ctx;
if (defaultValue) return defaultValue;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
};
Provider.displayName = rootComponentName + "Provider";
return [Provider, useSafeContext] as const;
}이 헬퍼를 적용하면, 현재 컨텍스트를 생성하는 부분을 아래처럼 바꿀 수 있습니다.
export const [ToastStateProvider, useToastState] =
createSafeContext<ContextState>("ToastStateProvider");
export const [ToastCommandProvider, useToastCommand] =
createSafeContext<ContextCommand>("ToastCommandProvider");|
코드가 담백(?)해서 좋아요,,,ㅎ |
tomatopickles404
left a comment
There was a problem hiding this comment.
규원님~~~! 이번 주 해내셨군요 😎
규원님이랑 같은 팀이어서 너무 조아요 허허..
코드에 공부하면서 작성하신 흔적이 느껴지는 것 같습니당
남은 시간도 같이 재밌게 해봐요 ㅎㅎ
| const objA = a as Record<string, unknown>; | ||
| const objB = b as Record<string, unknown>; |
There was a problem hiding this comment.
개인적인 제 취향인데요!
저는 이렇게 타입이 길어지는 경우에는 type을 따로 선언해두는게 더 가독성에 좋게 느껴지더라구요!
이렇게 해봐도 좋을 것 같아요!
type ObjectRecord = Record<string, unknown>;
const objA = a as ObjectRecord;
const objB = b as ObjectRecord;There was a problem hiding this comment.
너무 간결하게 쓰고자했던 것 같아요ㅠ 확실히 타입 지정을 먼저 선언하는게 좋은 것 같습니다! 감사합니다
| const prevPropsRef = useRef<P | null>(null); | ||
| const renderOutputRef = useRef<ReactNode | null>(null); | ||
|
|
||
| if (!equals(prevPropsRef.current, props)) { |
There was a problem hiding this comment.
초기 렌더링을 고려하는 로직이 추가되면 조금 더 안정적인 구조가 될 것 같아요!
if(!prevPropsRef.current || !equals(prevPropsRef.current, props));| const shallowSelector = useShallowSelector(selector); | ||
| return shallowSelector(store.getState()); | ||
|
|
||
| return useSyncExternalStore(store.subscribe, () => shallowSelector(store.getState())); |
There was a problem hiding this comment.
작성해주신 useSyncExternalStore 인터페이스만 봤을 때는 구체적으로 어떤 일을 하는지 직관적이지 않은 것 같아요!
함수에 이름을 붙여주는건 어떨까요?!
const subscribeToStore = store.subscribe;
const getCurrentState = () => shallowSelector(store.getState());
return useSyncExternalStore(
subscribeToStore,
getCurrentState,
);There was a problem hiding this comment.
확실히 다시 보니까 의도가 명확하게 보이지 않는 것 같아요,, 의견 주셔서 감사합니다!
과제 체크포인트
배포 링크
https://q1lim.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
재밌었다!
무의식적으로 사용하는 hook에 대해 깊게 생각하지 않았는데 React 내부의 hook을 구현해보면서 작동 원리를 이해할 수 있는 기회가 되었습니다. 개인적으로 세번째 과제는 이 쇼핑몰 시리즈(?) 중에서 제일 흥미로웠습니다!
설계 친구 AI
이번 과제의 목표는 AI에 대한 의존도를 낮추는 것이었습니다. (하지만 안 쓸 수 없는 😔 ) AI는 함수 내부에 구현해야 하는 요구사항을 정리하고, 설계에 도움을 주는 가이드 역할로만 설정했습니다. 처음부터 구현한 코드를 보여주지 말라는 요청과 함께 과제를 진행하다 보니 중간중간 답답한 부분도 있었고, 특정 함수를 직접 구현해 달라고 한 적도 있었지만 이러한 방식이 결국 스스로 사고하고 구현하는 힘을 기르는 데 더 효과적이었다고 느꼈습니다.
친해지고 싶은 TS
실무에서 React만 썼기 때문에 아직 TS 타입 사용이 능숙하지 않아서 시간을 많이 소요했습니다. 아마 AI에게 추가 질문을 제일 많이 한 부분이 타입 관련 질문이었던 것 같습니다.
기술적 성장
진행하면서 고민했던 부분을 노션으로 나열(?)했습니다.
의식의 흐름을 나열해보자
자랑하고 싶은 코드
자랑하고 싶은 코드가 있었으면 좋았겠지만 이번 과제에서는 없습니다.
개선이 필요하다고 생각하는 코드
useMemo와 useAutoCallback을 활용해 리렌더링을 방지했고, 이 과정이 심화 과제의 목적이라는 것도 이해하고 있습니다. 다만, 생각보다 많은 곳에 useMemo를 사용하다 보니 정말 이 정도까지 필요한지, 혹은 과한 최적화는 아닌지에 대한 고민이 생겼습니다. 학습 목적으로 useMemo를 적극 활용하는 건 이해되지만, 실제 이렇게 많은 useMemo 사용이 일반적일까? 라는 생각도 들었습니다.
학습 효과 분석
과제 피드백
진행한 과제중에 재밌다고 느꼈던 부분 중 하나가 과제에 대한 소소한 힌트들도 한몫했습니다. 👍 그리고 참고할 수 있는 학습자료도 많고, 아카이브가 잘 되어있어서 공부하는데 많은 도움이 되었습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
<리액트의 렌더링 과정>
메모이제이션에 대한 나의 생각을 적어주세요.
메모이제이션의 핵심은 '비용'과 '빈도'라고 생각합니다. 연산량이 많고, 비용이 많이 드는 반복되는 계산이 많은 상황에서 불필요한 리렌더링을 방지하는 부분에서 활용됩니다.
하지만 무조건적인 메모이제이션의 사용은 메모이제이션 관리에 리소스를 더 소요할 수 있습니다.
메모이제이션은 성능 최적화할 수 있는 여러 방법 중 선택할 수 있는 옵션 중 하나라고 생각합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
저는 준일코치님이 세션때 말씀주셨던 것처럼 관심사 분리와 참조 동일성이 먼저 떠오른 것 같습니다.
UI와 비즈니스 로직 등 관심사 분리할 수 있어서 한 파일에 하나의 역할만 집중해서 협업 시 코드를 빠르게 이해하는데 도움이 될 것 같습니다.
컨텍스트를 사용하는 것은 결국 동일한 데이터 혹은 상태를 공유해서 데이터의 일관성을 유지하기 위해 사용할 수 있는 방법 중 하나라고 생각합니다. 실무에서 컨텍스트를 사용했을 때는 항상 Provider를 감싸지 않은 영역에서 데이터를 참조하고 싶어서 어떻게 해야하지를 많이 고민했던 것 같습니다ㅎㅎ
리뷰 받고 싶은 내용