[2팀 유윤우] Chapter 1-3. 프레임워크 없이 SPA 만들기#10
[2팀 유윤우] Chapter 1-3. 프레임워크 없이 SPA 만들기#10yunwoo-yu wants to merge 19 commits intohanghae-plus:mainfrom
Conversation
|
우와 멋있다....... ✨ ✨ |
| return shallowSelector(router); | ||
|
|
||
| return useSyncExternalStore( | ||
| (onStoreChange) => router.subscribe(onStoreChange), |
There was a problem hiding this comment.
useSyncExternalStore에서 subscribe 함수가 onStoreChange 콜백을 인자로 받는다는 걸 덕분에 알게됐네요 감사합니다!
| if (shallowEquals(value, newValue)) { | ||
| return; | ||
| } | ||
|
|
||
| setValue(newValue); |
There was a problem hiding this comment.
조건이 하나밖에 없어서 if (!shallowEquals(value, newValue)){ setValue(newValue) }와 같이 조건을 반전시켜
바로 setValue(newValue)를 실행하는 게 가독성이 좀 더 좋을 수도 있겠다는 개인적인 생각이 들었습니다!
There was a problem hiding this comment.
- 오 제가 디테일한 부분을 놓쳤네요 동의합니다 좋은 의견 감사합니다😄
|
LGTM ...잘봤습니다~~!! |
|
따봉따봉 따따따봉 Context는 관심사의 범위를 제한해주는 도구다라는 점이 아주 공감되네요! |
susmisc14
left a comment
There was a problem hiding this comment.
안녕하세요, 윤우님!
채영님 추천으로 리뷰를 하게 됐는데, PR을 정말 재밌게 읽었습니다. 특히 동료의 트러블슈팅을 지원한 사례가 가장 인상 깊었어요. 자신의 과제뿐만 아니라 팀원들과 함께 항해에 임하시는 모습이 정말 멋지다고 생각했습니다.
내일 중간 네트워킹에서 뵙고 직접 인사 나눌 수 있기를 기대하겠습니다!
There was a problem hiding this comment.
윤우님 PR에서 원시 타입에 대한 깊은 고민을 엿볼 수 있어 좋았습니다!
그 글을 읽으니, 예전에 Radix UI가 원시 타입인 Symbol을 활용해 컨텍스트(Context) 충돌 문제를 해결했던 것이 떠오르더라고요. Provider가 중첩되어도 각자의 값만 참조하게 만드는 이 패턴을 **'Scoped Context'**라고 하는데, 마침 이번 과제 주제와도 관련이 깊어 공유해 드립니다.
혹시 관심 있으시다면 Scoped Context 아티클을 읽어보시면, 컨텍스트를 더 안전하고 확장성 있게 사용하는 좋은 아이디어를 얻으실 수도 있을 것 같아요!
추가로 저는 depth라는 인자를 추가해서 비교의 깊이를 조절할 수 있게 만들었어요. 이 depth 값을 재귀적으로 1씩 줄여나가다가 0이 되면 === 비교로 마무리하는 방식입니다. 이렇게 하면 deepEquals와 shallowEquals를 하나의 함수로 통합할 수 있는 장점이 있습니다. 예를 들어, shallowEquals(a, b)는 내부적으로 deepEquals(a, b, 1)을 호출하는 거죠. 이런 방식을 고려한다면 중복되는 구조를 깔끔하게 해결할 수 있을 것 같아요!
| if (Object.is(a, b)) { | ||
| return true; | ||
| } | ||
|
|
||
| // 2. 원시 값 처리 | ||
| if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { | ||
| return Object.is(a, b); | ||
| } | ||
|
|
||
| // 3. 배열 깊은비교 재귀 | ||
| if (Array.isArray(a) && Array.isArray(b)) { | ||
| if (a.length !== b.length) { | ||
| return false; | ||
| } | ||
|
|
||
| for (let i = 0; i < a.length; i++) { | ||
| if (!deepEquals(a[i], b[i])) return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
Object.is로 NaN 같은 엣지 케이스를 처리하고, Array.isArray로 배열과 객체를 완벽하게 분리해서 비교해주신 덕분에 완성도 있는 비교 함수가 완성된 것 같아요!
저는 처음에 배열도 '인덱스를 키로 갖는 객체'와 유사하다고 생각해서, 두 타입을 구분하지 않고 객체 비교 로직으로 한 번에 처리했었는데요. 다른 분들 코드를 보고 있다보니 ['a']와 { '0': 'a', length: 1 }이 같게 처리될 수 있지만, 실제로는 타입도 다르고 .map 같은 배열 메서드도 사용할 수 없다는 점에서 예상치 못한 부작용이 생길 수 있겠다는 걸 깨달을 수 있었습니다.
| } | ||
|
|
||
| // props가 다르면 새로운 렌더링 | ||
| const result = Component(props); |
There was a problem hiding this comment.
컴포넌트를 Component(props)처럼 직접 함수로 호출하는 대신 createElement()를 사용하는 것이, 컴포넌트 내부의 훅이나 컨텍스트가 올바르게 동작하도록 보장하는 React의 공식적인 방법이라고 알고 있습니다!
There was a problem hiding this comment.
createElement를 쓰는건 생각 못했는데 타입까지 좀 더 명확하게 추론이 되더라구요!!👍
| const valueRef = useRef<T | null>(null); | ||
| const depsRef = useRef<DependencyList>([]); |
There was a problem hiding this comment.
value와 deps처럼 항상 함께 변경되는 관련성 높은 데이터는 하나의 객체로 묶어 관리하는 것이 코드의 응집도를 높이고, 나중에 다른 상태가 추가되더라도 더 관리하기 편해지는 장점이 있을 수 있습니다.
물론 이건 개발자의 취향이나 상황에 따른 선택일 수 있을 것 같아요!
There was a problem hiding this comment.
저는 useMemo라는 응집도 안에서 value와 dpes는 역할이 다르다고 생각했었는데 많은 useMemo를 사용한다고 생각했을 땐 하나로 관리하는게 더 적은 useRef를 사용할 수 있어 그것도 장점이 되겠다는 생각이드네요👍
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의 타입을 더 정확하게 추론해주는 장점이 있습니다!
| const setValueWithShallowEquals = useCallback((newValue: T) => { | ||
| if (shallowEquals(value, newValue)) { | ||
| return; | ||
| } | ||
|
|
||
| setValue(newValue); | ||
| }, []); |
There was a problem hiding this comment.
useState의 set 함수처럼 setShallowState도 함수를 인자로 받을 수 있도록 기능을 확장해보면 좋을 것 같아요.
예를 들어, setShallowState(prev => ({ ...prev, count: prev.count + 1 }))와 같은 패턴을 지원하면 사용자가 더 유연하게 상태를 관리할 수 있게 되어 훅의 완성도가 높아질 것 같습니다.
There was a problem hiding this comment.
아 그렇네요 함수형 업데이트도 추가해보겠습니다 감사합니다😁
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");
yunwoo-yu
left a comment
There was a problem hiding this comment.
@susmisc14 안녕하세요 지수님 정성스런 코드리뷰 감사합니다🥹
페어팀끼리 모이느라 정신없어서 인사는 못드렸지만 다음에 또 기회가 될때 꼭 인사해요 리뷰와 꿀팁들 감사합니다!!!👍
| } | ||
|
|
||
| // props가 다르면 새로운 렌더링 | ||
| const result = Component(props); |
There was a problem hiding this comment.
createElement를 쓰는건 생각 못했는데 타입까지 좀 더 명확하게 추론이 되더라구요!!👍
| const valueRef = useRef<T | null>(null); | ||
| const depsRef = useRef<DependencyList>([]); |
There was a problem hiding this comment.
저는 useMemo라는 응집도 안에서 value와 dpes는 역할이 다르다고 생각했었는데 많은 useMemo를 사용한다고 생각했을 땐 하나로 관리하는게 더 적은 useRef를 사용할 수 있어 그것도 장점이 되겠다는 생각이드네요👍
| const setValueWithShallowEquals = useCallback((newValue: T) => { | ||
| if (shallowEquals(value, newValue)) { | ||
| return; | ||
| } | ||
|
|
||
| setValue(newValue); | ||
| }, []); |
There was a problem hiding this comment.
아 그렇네요 함수형 업데이트도 추가해보겠습니다 감사합니다😁
과제 체크포인트
배포 링크
https://hanghae-plus.github.io/front_6th_chapter1-3/
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
과제를 진행하며 기존에 알고있던 지식도 한번 더 다지고 새로 알게 된 메서드와 해당 훅을 제공하는 라이브러리에서는 어떻게, 왜 이렇게 구현했는지 한번 비교해보고 학습했습니다. 과제테스트를 통과한 후 다른 분들에게 도움을 드렸던 문제들을 정리해봤습니다. 예전에 눈으로만 보며 넘어가서 어렴풋이만 알고있던 지식들을 기록해보며 머릿속에 넣어보려 노력했습니다!
기술적 성장
자바스크립트의 원시&참조 타입
JavaScript의 원시타입과 참조타입을 다룹니다. 타입을 정확히 이해하면 변수 할당, 함수 파라미터 전달, 상태 관리에서 예상치 못한 버그를 방지할 수 있습니다. 특히 React나 상태 관리 라이브러리를 사용할 때 얕은 비교 동작을 이해하는 데 핵심적인 지식이 됩니다.
원시타입
원시타입은 JavaScript에서 값 자체가 변수에 직접 저장되는 가장 기본적인 데이터 타입입니다. 객체나 배열과 달리 메모리 주소가 아닌 실제 값이 저장되어, 변수 간 할당 시 값이 복사됩니다.
7가지 원시타입
JavaScript는 다음 7가지 원시타입을 제공합니다:
template)특수 원시타입
Symbol 타입 이해하기
Symbol은 변경 불가능(immutable)하고 유일함을 보장하는 원시타입입니다. 주로 객체 프로퍼티의 충돌 없는 키를 만들 때 사용합니다.
Symbol 함수의 정적 프로퍼티 중 Well-Known Symbol이라 불리는 특별한 심볼들이 있습니다. 이 Well-Known Symbol은 자바스크립트 엔진에 상수로 존재해 이 값을 참조해 일정한 처리를 진행합니다.
일정한 처리라 함은 어떠한 객체가 Symbol.iterator를 프로퍼티 key로 하는 메소드를 가지고 있다면 자바스크립트 엔진은 이 객체가 이터레이션 프로토콜을 따른다고 생각하고 이터레이터하게 동작하게 합니다.
저희가 사용하는 빌트인 객체 Array, String 등 순회 가능한 객체는 모두 내부에 Symbol.iterator를 가지고 있기에 이터레이션 프로토콜을 따라 반복문, 스프레드 연산자의 사용이 가능합니다.
bigint
bigint는 임의의 정밀도로 정수를 나타낼 수 있는 원시 타입입니다. Number 타입의 안전한 정수 범위(2^53 - 1)를 넘어서는 큰 정수를 다룰 때 사용됩니다.
원시타입과 참조타입의 차이
참조 타입은 위에서 설명한 타입 외에 모든 객체, 배열 등이 참조 타입으로 분류됩니다. JavaScript의 데이터 타입을 이해하려면 원시타입과 참조타입의 근본적인 차이를 알아야 합니다. 이 차이는 변수 할당, 함수 인자 전달, 비교 연산에서 완전히 다른 동작을 보입니다.
값 저장 방식의 차이
원시타입: 변수에 실제 값이 직접 저장됩니다.
참조타입: 변수에 **메모리 주소(참조)**가 저장됩니다.
비교 연산의 차이
원시타입: 값 자체를 비교합니다.
참조타입: 메모리 주소를 비교합니다.
함수 인자 전달의 차이
원시타입: Call by Value (값에 의한 전달)
참조타입: Call by Reference (참조에 의한 전달)
💡 이러한 차이점은 React 사용 시 알아야 하는 불변성 개념과 직결됩니다. 특히 참조타입을 다룰 때 의도치 않게 원본 객체나 배열을 변경하여 예상과 다른 동작을 일으킬 수 있으므로, 두 타입의 차이점을 명확히 이해하고 코드를 작성하는 것이 중요합니다.
얕은 비교, 깊은 비교
자바스크립트에서의 비교
자바스크립트에서 값을 비교하는 연산자는 두 가지입니다.
대부분의 경우 일치연산자로 충분하지만, 다음과 같은 특수한 경우에는 예상과 다른 결과가 나올 수 있습니다.
정확한 비교가 필요하다면 Object.is() 메서드를 사용 해야합니다.
Object.is()는 일치연산자와 거의 동일하지만, NaN과 ±0을 올바르게 구분합니다.
얕은 비교 (shallowEqual)
얕은 비교(Shallow Comparison) 는 다음과 같이 동작한다고 알려져 있습니다.
하지만 React와 Zustand에서의 얕은 비교는 다르게 동작합니다.
Zustand의 얕은 비교 코드:
Zustand의 얕은 비교는 객체와 배열의 최상위 레벨 값까지 비교합니다. compareEntries와 compareIterables 함수가 객체를 순회하며 값을 비교합니다.
compareEntries 함수는 어댑터 패턴을 통해 Map, Set, 일반객체를 순회하며 값을 비교합니다. compareIterables 함수는 entries 메서드가 없는 Array, String 등의 데이터를 순회하며 비교합니다.
React의 shallowEqual도 유사한 방식으로 동작합니다:
실제로 얕은 비교 코드를 구현할 때는 일반적으로 알려진 얕은 비교(메모리 주소만 참조)와 달리, 객체의 최상위 레벨 속성까지 비교하는 방식을 사용해야 합니다.
깊은 비교
깊은 비교는 객체나 배열의 중첩된 모든 속성을 재귀적으로 비교하는 방식입니다. React나 Zustand 같은 라이브러리에서는 성능상의 이유로 일반적으로 사용하지 않으며, 객체 크기에 따라 비용이 기하급수적으로 증가합니다.
얕은 비교와 달리 배열이나 객체의 중첩된 구조를 만날 때마다 즉시 재귀 호출을 통해 더 깊은 레벨(DFS)로 들어가 끝까지 파고들어 비교를 완료합니다.
JSON.stringify() 방식
간혹 JSON.stringify()를 통한 비교를 사용하는 경우가 있지만, 여러 문제점이 존재합니다:
이외에도 NaN, Infinity 같은 특수 값 처리 불가, 데이터 손실 가능성, 함수나 undefined 값 무시 같은 문제가 있습니다. 다음과 같은 제한적인 상황에서는 고려해볼 수 있습니다:
React에서의 실용적 활용
React와 연계해서 사용하기에는 memo의 두 번째 인자에 직접 비교 함수를 제공하는 방식이 더 적합합니다:
추가 팁
💡 React의 불변성을 지키기 위해 깊은 복사가 필요하다면 lodash-es의 cloneDeep 함수를 이용하면 간편하게 깊은 복사를 통해 새로운 객체를 얻을 수 있습니다.
트러블슈팅 지원 사례
Q1. useRef에 왜 함수를 넣어줘야 하나요?
이 useRef 구현에서 useState에 함수를 전달하는 이유는 불필요한 객체 생성을 방지하기 위해서입니다.
위처럼 작성하면 컴포넌트가 리렌더링될 때마다 { current: initialValue } 객체가 새로 생성됩니다. useState는 초기값을 한 번만 사용하지만, 객체 생성 자체는 매번 발생합니다.
이를 해결하는 방법이 지연 초기화(lazy initialization)입니다.
함수를 전달하면 useState가 최초 렌더링 시에만 함수를 실행하여 객체를 생성합니다. 이후 리렌더링에서는 함수가 실행되지 않아 불필요한 객체 생성을 방지합니다.
React의 실제 구현에서 이를 확인할 수 있습니다:
이는 React에서 인라인 스타일을 권장하지 않는 이유와 동일합니다. 렌더링 과정에서 객체 리터럴은 항상 새로운 참조를 생성하기 때문입니다.
이번과제 ToastContext 구축 시 value 부분에도 적용되는 개념입니다.👀
참고문서
Q2. useMemo에 return에 왜 에러가 뜨는지 모르겠어요
문제 코드
TypeScript 에러입니다. useMemo 훅의 반환 타입은 T로 정의했지만, valueRef.current는 undefined일 가능성이 있어 타입 불일치가 발생합니다.
해결 방법: 초기 상태 처리 추가
valueRef.current === undefined 조건을 추가하여 초기 렌더링 시 undefined 상태를 처리합니다. 이제 반환 시점에는 항상 T 타입이 보장됩니다.
학습 효과 분석
이번 과제를 통해 일반적인 얕은 비교와 React의 얕은 비교 방식이 다르다는 점을 파악했습니다.
기초적인 타입별 특징과 메모리 할당 방식을 다시 학습했고, 참조 타입 사용 시 React의 불변성 원칙을 지키는 방법을 정리했습니다.
Zustand와 React의 얕은 비교 구현 코드를 분석한 결과, Zustand는 Symbol.iterator 활용, Map과 Set 객체 처리, Object.is 메서드 사용 등 더 정교한 비교 로직을 구현하고 있음을 확인했습니다.
과제를 빠르게 진행하여 다른분들의 문제를 파악해보며 설명하는 과정에서 왜? 를 납득시키기 위한 좀 더 논리적인 원리를 파고드는 경험도 했습니다.
현재 과제는 Preact의 방식과 유사한 패턴이지만 좀 더 대규모 환경인 React의 코드를 헤집어보며 hook 실행 시점과 지연초기화가 어떻게 가능한지 학습했습니다. (몇천줄 속에서 코드 찾는 요령이 늘은 것 같아요 👍)
과제 피드백
useAutoCallback 구현 시 인자를 전달하지 않아도 테스트가 통과하는 부분이 있었습니다. (E2E 제외)
UnitTest가 추가되어도 괜찮겠다! 생각이 들었습니다.
현 React의 구조는 너무 복잡한데 Preact로 접근해서 풀어보는 방식이 새롭고 좋았습니다!! 쉽게 배우고 -> 깊게 들어가는 느낌
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
저는 React 내부적으로 렌더링이 일어나는 순서에 대해 딥다이브하며 한번 작성해봤습니다.
Fiber 아키텍처
Fiber는 React 16부터 도입된 재조정(Reconciliation) 엔진입니다. 각 컴포넌트나 요소를 객체로 표현합니다. 이하 설명은 Fiber 아키텍처의 렌더링 과정입니다.
Fiber 노드의 구조:
두 단계의 렌더링 과정
React의 렌더링은 두 단계로 나누어 집니다.
Render Phase (렌더 단계)
Commit Phase (커밋 단계)
이중 트리 구조
React는 두 개의 트리를 동시에 관리합니다.
이 구조는 더블 버퍼링 방식으로 작동합니다. 백그라운드에서 새로운 트리를 구성하고, 완료되면 포인터를 교체하여 새 트리를 활성화합니다. 이를 통해 작업 중단과 재시작, 우선순위 기반 업데이트가 가능합니다.
현재의 동작이 마운트냐, 업데이트냐에 따라 달라지는 WorkLoop 부분도 확인했지만 스케쥴러부터 관련 함수들까지의 양이 너무 방대해 아직 완벽하게 흐름을 이해하지 못했습니다.😅
관련 아티클
React 톺아보기 -2
메모이제이션에 대한 나의 생각을 적어주세요.
개인적인 생각으로는 메모이제이션 훅은 예기치 못한 동작들을 원하는 방향으로 돌아갈 수 있게 도와주는 훅에 더 가깝다고 느꼈습니다.
물론 대규모 프로젝트나 빅 데이터를 다루지 못했기에 이런 결론에 도달했을 수 있지만, 조금 더 근본적인 생각으로 돌아가보자면 이 메모이제이션 훅들은 렌더링을 개선해주고 큰 계산의 결과값을 캐싱하는 용도로 나온 훅으로 알고있습니다.
그런데 이 훅들을 사용하지 않았을 때 버벅임이나 성능적인 문제가 아니라 기술적인 오류가 발생한다면 가장 먼저 내가 컴포넌트의 구조를 잘못 잡았나? 부터 생각해보고 이 훅들을 사용하지 않아도 "정상적"으로 돌아가는 방향으로 개선하는걸 우선해볼 것 같습니다.
분명 이 메모이제이션 훅들은 필요한 훅이지만 비용이 적지 않은 만큼 내가 설계한 방향이 맞았나? 더 나은 방법은 없을까? 라고 리팩토링의 고민을 하게 해줄 수 있는 훅이라고 생각해봤습니다 🙇♂️
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
왜 컨텍스트와 상태관리가 필요한가?
Props Drilling의 한계
컴포넌트 트리가 깊어질수록 props 전달 체인이 길어집니다. 특히 중간 컴포넌트들이 자신은 사용하지 않는 데이터를 단순히 하위 컴포넌트로 전달하기 위해서만 존재하게 되는 상황이 발생합니다. 이는 변경점 발생 시 모든 중간 단계 컴포넌트를 수정해야 하는 문제로 이어집니다.
형제 컴포넌트 간 데이터 공유의 복잡성
React의 단방향 데이터 흐름에서 형제 컴포넌트끼리 직접 소통할 방법이 없습니다. 데이터를 공유하려면 가장 가까운 공통 부모 컴포넌트까지 상태를 끌어올려야 하는데, 이는 상위 컴포넌트의 책임을 무겁게 만들고 컴포넌트 간 결합도를 높입니다.
전역 상태 관리의 필요성
애플리케이션 전반에서 사용되는 데이터(인증 상태, 설정 값 등)는 컴포넌트 계층과 상관없이 접근할 수 있어야 합니다. props로 이런 데이터를 전달하면 모든 컴포넌트가 이 데이터에 의존하게 되어 결합도가 높아집니다.
결국 컨텍스트와 상태관리는 "데이터가 필요한 곳에서 바로 접근할 수 있게 해주는 도구" 라는 생각이 들었습니다.
컨텍스트와 상태관리를 사용하지 않으면 어떤 문제가 발생할까?
인터페이스 변경 시 수정 범위 확산
props로 전달되는 데이터의 타입이나 구조가 변경되면, 해당 데이터를 전달하는 모든 중간 컴포넌트의 props 인터페이스를 수정해야 합니다. 이는 변경의 영향 범위를 예측하기 어렵게 만들고, 실수로 누락되는 부분이 생길 가능성을 높입니다.
컴포넌트의 독립성 저하
특정 props에 의존하는 컴포넌트는 해당 props를 제공하는 상위 컴포넌트 없이는 동작할 수 없습니다. 이는 컴포넌트의 재사용성을 제한합니다.
상태 일관성 관리의 어려움
동일한 데이터를 여러 컴포넌트에서 각각 관리하면 데이터 동기화 문제가 발생합니다. 한 곳에서 데이터가 업데이트되어도 다른 곳의 데이터는 이전 상태를 유지하는 불일치가 생길 수 있습니다.
중복 연산과 메모리 비효율성
같은 데이터 가공 로직이 여러 컴포넌트에 분산되어 있으면 동일한 계산이 중복으로 수행됩니다. 또한 비슷한 상태를 여러 곳에서 관리하면 메모리 사용량이 불필요하게 증가할 수 있습니다.
컨텍스트의 다양한 활용법
컴파운드 컴포넌트 패턴으로 제한된 범위의 Context 활용
전역 Context의 리렌더링 문제를 피하면서도 관련 컴포넌트들끼리는 상태를 공유할 수 있는 절충안입니다. 특정 기능 단위로 Context 범위를 제한하여 해당 영역 내에서만 상태를 공유합니다.
이 패턴은 Modal, Accordion, Tab 등 UI 컴포넌트에서 내부 상태를 공유해야 할 때 특히 유용합니다. 컴포넌트의 내부 구현은 Context를 사용하지만 외부에서는 일반적인 props 인터페이스로 사용할 수 있어 캡슐화가 잘 됩니다.
Custom Hooks를 통한 상태 로직 재사용
상태 관리 로직을 훅으로 분리하면 여러 컴포넌트에서 동일한 상태 패턴을 재사용할 수 있습니다. 전역 상태 없이도 비슷한 상태 로직이 필요한 컴포넌트들에서 일관된 동작을 구현할 수 있습니다.
localStorage나 sessionStorage와의 동기화, API 호출 상태 관리, 폼 validation 등의 로직을 훅으로 만들어 재사용하는 방식입니다.
서버 상태 관리 라이브러리 활용
React Query, SWR 같은 라이브러리를 사용하면 서버에서 가져오는 데이터는 별도로 관리할 수 있습니다. 캐싱, 동기화, 백그라운드 업데이트를 자동으로 처리해주므로 클라이언트 상태와 서버 상태를 분리하여 복잡성을 줄일 수 있습니다.
리뷰 받고 싶은 내용
ToastProvider
번외: 준일 코치님 멘토링 세션을 진행하며 제가 왜 빅테크를 가고 싶어하는지, 업무를 해당 회사들에 어울리는 인재처럼 진행하고 있었는지 다시 한번 돌아보게 되었습니다 감사합니다🙇♂️