🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

INTERNAL_overrideCreateStore 심화 학습 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 10. · 16 Views

INTERNAL overrideCreateStore 심화 학습 완벽 가이드

Zustand의 내부 API인 INTERNAL_overrideCreateStore를 깊이 있게 분석합니다. 테스트 환경에서의 활용부터 실전 베스트 프랙티스까지, 고급 개발자로 성장하기 위한 필수 지식을 담았습니다.


목차

  1. INTERNAL_overrideCreateStore 상세 분석
  2. 소스 코드 깊이 읽기
  3. 내부 구현 원리
  4. 고급 사용 패턴
  5. 성능 최적화 팁
  6. 실전 베스트 프랙티스

1. INTERNAL overrideCreateStore 상세 분석

김개발 씨는 회사에서 상태 관리 라이브러리로 Zustand를 사용하고 있습니다. 어느 날 테스트 코드를 작성하다가 신기한 함수를 발견했습니다.

"INTERNAL_overrideCreateStore? 이건 뭐지?" 선배 박시니어 씨에게 물어보니, "아, 그거 Zustand의 숨겨진 보물이지"라는 대답이 돌아왔습니다.

INTERNAL_overrideCreateStore는 Zustand의 내부 API로, createStore 함수의 동작을 재정의할 수 있게 해주는 강력한 도구입니다. 마치 자동차의 엔진을 직접 조작할 수 있는 특수 키와 같습니다.

주로 테스트 환경에서 스토어의 동작을 제어하거나, 특수한 디버깅 상황에서 활용됩니다. 이 API를 제대로 이해하면 Zustand의 내부 동작 원리를 깊이 있게 파악할 수 있습니다.

다음 코드를 살펴봅시다.

// Zustand의 INTERNAL_overrideCreateStore 기본 사용법
import { INTERNAL_overrideCreateStore } from 'zustand/vanilla'
import { create } from 'zustand'

// 원본 createStore 함수를 백업합니다
const originalCreateStore = INTERNAL_overrideCreateStore

// 커스텀 createStore 함수를 정의합니다
INTERNAL_overrideCreateStore = (createState) => {
  console.log('스토어가 생성됩니다')
  // 원본 동작을 유지하면서 로깅을 추가합니다
  return originalCreateStore(createState)
}

// 이제 모든 스토어 생성이 로깅됩니다
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}))

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 팀에서 테스트 커버리지를 높이는 프로젝트를 진행하고 있습니다.

Zustand로 작성된 상태 관리 코드를 테스트하던 중, 각 테스트마다 스토어를 초기화하는 것이 쉽지 않다는 것을 깨달았습니다. 선배 박시니어 씨가 코드 리뷰를 하다가 조언을 해주었습니다.

"Zustand에는 숨겨진 API가 있어요. INTERNAL_overrideCreateStore라는 건데, 이걸 활용하면 테스트가 훨씬 수월해집니다." INTERNAL_overrideCreateStore란 정확히 무엇일까요?

쉽게 비유하자면, 이것은 마치 공장의 생산 라인을 잠시 멈추고 설정을 바꿀 수 있는 마스터 키와 같습니다. 일반적으로 공장은 정해진 방식대로 제품을 생산하지만, 특별한 상황에서는 생산 방식 자체를 바꿔야 할 때가 있습니다.

INTERNAL_overrideCreateStore도 마찬가지로 Zustand가 스토어를 생성하는 방식 자체를 바꿀 수 있게 해줍니다. 이 API가 없던 시절에는 어땠을까요?

개발자들은 테스트 환경에서 스토어를 제어하기 위해 복잡한 우회 방법을 사용해야 했습니다. 각 테스트마다 새로운 스토어 인스턴스를 만들거나, 전역 상태를 수동으로 리셋하는 코드를 작성해야 했습니다.

더 큰 문제는 스토어 생성 과정에서 일어나는 일들을 추적하거나 제어하기가 거의 불가능했다는 점입니다. 프로젝트가 커지면서 이런 문제는 더욱 심각해졌습니다.

수십 개의 스토어를 사용하는 대규모 애플리케이션에서는 테스트 코드를 작성하는 것이 악몽처럼 느껴질 때도 있었습니다. 바로 이런 문제를 해결하기 위해 INTERNAL_overrideCreateStore가 등장했습니다.

이 API를 사용하면 스토어가 생성되는 시점에 개입할 수 있습니다. 생성 과정을 로깅할 수도 있고, 특정 조건에서 다른 동작을 하도록 만들 수도 있습니다.

무엇보다 테스트 환경에서 스토어의 동작을 완전히 제어할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 import 부분을 보면 INTERNAL_overrideCreateStore를 vanilla 패키지에서 가져오는 것을 알 수 있습니다. 이것은 Zustand의 핵심 패키지에 있는 함수입니다.

다음으로 원본 함수를 백업하는 부분이 있는데, 이것은 나중에 원래 동작을 복원하기 위해 필수적입니다. 그리고 나서 INTERNAL_overrideCreateStore에 새로운 함수를 할당합니다.

이 함수는 createState를 인자로 받는데, 이것은 스토어의 초기 상태와 액션을 정의하는 함수입니다. 우리는 이 지점에서 원하는 동작을 추가하고, 마지막에 원본 함수를 호출하여 실제 스토어를 생성합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 대규모 전자상거래 서비스를 개발한다고 가정해봅시다.

장바구니, 사용자 정보, 주문 내역 등 수많은 스토어가 존재합니다. 통합 테스트를 작성할 때 각 테스트마다 모든 스토어를 깨끗한 상태로 초기화해야 합니다.

INTERNAL_overrideCreateStore를 활용하면 모든 스토어 생성을 추적하고, 테스트가 끝날 때 자동으로 정리할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 프로덕션 코드에서 이 API를 사용하는 것입니다. 함수명에 INTERNAL이 붙어있는 것은 내부 API라는 의미이며, 공식적으로 지원되는 API가 아닙니다.

따라서 Zustand의 버전이 업데이트되면 이 API의 동작이 바뀌거나 제거될 수도 있습니다. 반드시 테스트 환경이나 개발 환경에서만 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.

"아, 그래서 테스트 설정 파일에서만 사용하는 거군요!" INTERNAL_overrideCreateStore를 제대로 이해하면 Zustand의 내부 동작을 깊이 있게 파악할 수 있고, 더 견고한 테스트 코드를 작성할 수 있습니다. 하지만 강력한 도구인 만큼 신중하게 사용해야 합니다.

실전 팁

💡 - 항상 원본 함수를 백업해두고 테스트 종료 시 복원하세요

  • 프로덕션 코드에서는 절대 사용하지 마세요
  • 테스트 셋업 파일에서 한 번만 설정하고 재사용하세요

2. 소스 코드 깊이 읽기

김개발 씨는 박시니어 씨의 조언을 듣고 나서 궁금증이 생겼습니다. "그런데 이 API는 내부적으로 어떻게 구현되어 있을까요?" 박시니어 씨는 웃으며 대답했습니다.

"좋은 질문이에요. Zustand의 소스 코드를 직접 읽어보면 많은 것을 배울 수 있어요."

Zustand의 소스 코드를 살펴보면 INTERNAL_overrideCreateStore가 매우 단순하면서도 영리하게 구현되어 있음을 알 수 있습니다. 핵심은 함수 포인터를 이용한 간접 호출 패턴입니다.

마치 리모컨의 채널 버튼을 누르면 실제 TV가 채널을 바꾸는 것처럼, createStore를 호출하면 내부적으로 오버라이드된 함수가 실행됩니다. 이 패턴을 이해하면 비슷한 확장 가능한 API를 직접 설계할 수 있습니다.

다음 코드를 살펴봅시다.

// Zustand 내부 구현 방식 (단순화된 버전)
type CreateStore = <T>(createState: StateCreator<T>) => StoreApi<T>

// 기본 createStore 구현
const defaultCreateStore: CreateStore = (createState) => {
  let state: any
  const listeners = new Set<() => void>()

  const setState = (partial: any) => {
    const nextState = typeof partial === 'function'
      ? partial(state)
      : partial
    state = Object.assign({}, state, nextState)
    listeners.forEach(listener => listener())
  }

  // 초기 상태를 생성합니다
  state = createState(setState, () => state)

  return { getState: () => state, setState, subscribe: (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }}
}

// 이것이 오버라이드 가능한 포인터입니다
export let INTERNAL_overrideCreateStore: CreateStore = defaultCreateStore

김개발 씨는 퇴근 후 집에 돌아와 노트북을 켰습니다. Zustand의 GitHub 저장소를 열고 소스 코드를 탐색하기 시작했습니다.

vanilla.ts 파일을 열자 흥미로운 코드들이 눈에 들어왔습니다. "오, 생각보다 코드가 간결하네?" 김개발 씨는 감탄했습니다.

수백 줄의 복잡한 코드를 예상했지만, 핵심 로직은 놀라울 정도로 단순했습니다. 함수 포인터 패턴이란 무엇일까요?

이것은 마치 대리인을 세워두는 것과 같습니다. 실제 일을 처리하는 사람이 누구인지는 나중에 바꿀 수 있지만, 외부에서는 항상 같은 창구로 요청을 보냅니다.

프로그래밍에서는 함수 자체를 변수에 저장하고, 필요할 때 그 변수가 가리키는 함수를 바꾸는 기법입니다. 전통적인 방법으로는 이런 유연성을 달성하기 어려웠습니다.

일반적으로 함수를 정의하면 그것은 고정됩니다. createStore라는 함수가 있으면, 그 함수는 항상 같은 동작을 수행합니다.

하지만 테스트나 특수한 상황에서는 다른 동작을 하게 만들고 싶을 때가 있습니다. 전통적인 방법으로는 조건문을 넣거나, 상속을 사용하거나, 복잡한 설정 객체를 전달해야 했습니다.

Zustand의 개발자들은 더 우아한 해결책을 찾았습니다. let 키워드로 함수를 선언하는 것입니다.

보통 export한 함수는 const로 선언하지만, 이것을 let으로 선언하면 나중에 다른 함수로 교체할 수 있습니다. 이것이 바로 INTERNAL_overrideCreateStore의 핵심 아이디어입니다.

위의 코드를 자세히 분석해보겠습니다. 먼저 CreateStore 타입을 정의합니다.

이것은 상태 생성 함수를 받아서 스토어 API를 반환하는 함수 타입입니다. 다음으로 defaultCreateStore를 구현하는데, 이것이 실제 스토어를 생성하는 로직입니다.

내부를 보면 state 변수와 listeners 집합이 있습니다. setState 함수는 부분 상태를 받아서 현재 상태와 병합하고, 모든 리스너에게 변경을 알립니다.

그리고 createState를 호출하여 초기 상태를 만듭니다. 마지막 부분이 핵심입니다.

let 키워드로 INTERNAL_overrideCreateStore를 선언하고 defaultCreateStore로 초기화합니다. 이제 외부에서 이 변수에 다른 함수를 할당하면, 모든 create 호출이 새로운 함수를 사용하게 됩니다.

실제로 Zustand의 create 함수는 내부에서 INTERNAL_overrideCreateStore를 호출합니다. 김개발 씨는 코드를 읽으면서 무릎을 쳤습니다.

"와, 이렇게 간단한 방법으로 이런 강력한 기능을 만들 수 있구나!" 이 패턴의 장점은 여러 가지입니다. 첫째, 구현이 매우 단순합니다.

복잡한 추상화나 디자인 패턴이 필요 없습니다. 둘째, 성능 오버헤드가 거의 없습니다.

함수 호출이 한 단계만 추가될 뿐입니다. 셋째, 타입 안정성이 보장됩니다.

TypeScript가 함수 시그니처를 검증해줍니다. 하지만 이 패턴에도 주의할 점이 있습니다.

전역 상태를 변경하는 것이기 때문에 멀티 스레드 환경이나 동시성 이슈에 취약할 수 있습니다. 하지만 JavaScript는 싱글 스레드이고, 주로 테스트 환경에서 사용하므로 실제로는 문제가 되지 않습니다.

김개발 씨는 이 패턴을 메모장에 적어두었습니다. 나중에 자신이 라이브러리를 만들 때 참고할 좋은 예제가 될 것 같았습니다.

실전 팁

💡 - let과 const의 차이를 이해하면 이 패턴이 명확해집니다

  • 함수 포인터 패턴은 플러그인 시스템을 만들 때도 유용합니다
  • TypeScript의 타입 시스템이 함수 시그니처를 검증해주므로 안전합니다

3. 내부 구현 원리

다음 날 출근한 김개발 씨는 박시니어 씨에게 어제 공부한 내용을 설명했습니다. "소스 코드를 읽어보니 함수 포인터 패턴을 사용하더라고요!" 박시니어 씨는 만족스러운 표정으로 고개를 끄덕였습니다.

"좋아요. 그럼 이제 Zustand의 전체 흐름 속에서 이 API가 어떻게 동작하는지 알아볼까요?"

INTERNAL_overrideCreateStore의 진정한 힘은 Zustand의 전체 아키텍처 안에서 발휘됩니다. create 함수가 호출되면 내부적으로 createStore를 사용하는데, 이때 오버라이드된 함수가 있다면 그것이 대신 실행됩니다.

마치 전기 회로에서 스위치를 바꿔 끼우면 전류의 경로가 바뀌는 것처럼, 함수 참조를 바꾸면 실행 흐름이 바뀝니다. 이 메커니즘을 이해하면 Zustand뿐 아니라 다른 라이브러리의 내부 구조도 쉽게 파악할 수 있습니다.

다음 코드를 살펴봅시다.

// Zustand의 create 함수와 INTERNAL_overrideCreateStore의 관계
import { INTERNAL_overrideCreateStore } from 'zustand/vanilla'

// create 함수의 내부 구현 (단순화)
function create<T>(createState: StateCreator<T>) {
  // 여기서 오버라이드된 함수가 호출됩니다
  const api = INTERNAL_overrideCreateStore(createState)

  // React Hook으로 감싸서 반환합니다
  const useStore = (selector?: any) => {
    const [, forceUpdate] = useState({})

    useEffect(() => {
      return api.subscribe(() => forceUpdate({}))
    }, [])

    return selector ? selector(api.getState()) : api.getState()
  }

  return useStore
}

// 실행 흐름 추적
const stores: any[] = []

INTERNAL_overrideCreateStore = (createState) => {
  console.log('새로운 스토어 생성 감지')
  const store = originalCreateStore(createState)
  stores.push(store) // 모든 스토어를 추적합니다
  return store
}

박시니어 씨는 화이트보드를 꺼내서 Zustand의 아키텍처를 그리기 시작했습니다. "자, 전체 그림을 보면 이해가 더 쉬울 거예요." 화이트보드 맨 위에는 create 함수가 있었습니다.

개발자들이 실제로 사용하는 공개 API입니다. 그 아래에는 createStore 함수가 있고, 그 옆에 INTERNAL_overrideCreateStore가 화살표로 연결되어 있었습니다.

"보세요. create를 호출하면 내부에서 INTERNAL_overrideCreateStore를 호출합니다.

만약 이것이 오버라이드되어 있다면 우리의 커스텀 함수가 실행되는 거죠." 실행 흐름을 따라가 보겠습니다. 먼저 개발자가 create 함수를 호출합니다.

이것은 가장 바깥쪽 레이어입니다. create 함수 내부를 보면 INTERNAL_overrideCreateStore를 호출하는 부분이 있습니다.

이 시점이 바로 우리가 개입할 수 있는 지점입니다. 만약 오버라이드 함수를 설정하지 않았다면 기본 createStore가 실행됩니다.

하지만 오버라이드 함수를 설정했다면 그 함수가 대신 실행됩니다. 우리의 함수는 원하는 작업을 수행한 후 원본 함수를 호출하여 실제 스토어를 생성합니다.

이렇게 생성된 스토어 API는 다시 create 함수로 돌아옵니다. create 함수는 이것을 React Hook으로 감싸서 반환하고, 개발자는 컴포넌트에서 이 Hook을 사용합니다.

왜 이런 구조가 필요했을까요? Zustand의 설계 철학은 단순함과 확장성의 균형입니다. 대부분의 사용자는 간단한 API만 필요하지만, 고급 사용자는 더 깊은 제어가 필요할 때가 있습니다.

이 구조는 두 가지 요구를 모두 만족시킵니다. 일반 사용자는 create 함수만 알면 됩니다.

INTERNAL_overrideCreateStore의 존재조차 모르고 사용할 수 있습니다. 하지만 고급 사용자는 필요할 때 내부 동작을 제어할 수 있습니다.

김개발 씨는 화이트보드의 그림을 사진으로 찍었습니다. "이제 전체 구조가 머릿속에 그려지네요." 실제 프로젝트에서는 어떻게 활용할까요?

예를 들어 Redux DevTools와 같은 디버깅 도구를 만든다고 가정해봅시다. 모든 스토어의 생성과 상태 변경을 추적해야 합니다.

INTERNAL_overrideCreateStore를 오버라이드하여 각 스토어가 생성될 때마다 추적 시스템에 등록할 수 있습니다. 또 다른 예시로는 Time Travel Debugging이 있습니다.

애플리케이션의 모든 상태 변경을 기록하고, 특정 시점으로 되돌아갈 수 있게 만들 수 있습니다. 이런 기능을 구현하려면 모든 스토어의 setState 호출을 가로채야 하는데, INTERNAL_overrideCreateStore가 완벽한 진입점이 됩니다.

하지만 이 강력한 기능에는 책임이 따릅니다. 오버라이드 함수에서 오류가 발생하면 모든 스토어 생성이 실패합니다.

애플리케이션 전체가 작동하지 않을 수 있습니다. 따라서 반드시 try-catch로 에러를 처리하고, 최악의 경우 원본 함수를 호출하여 기본 동작이라도 수행되도록 해야 합니다.

박시니어 씨는 마지막으로 조언을 추가했습니다. "이 API는 양날의 검입니다.

강력하지만 조심해서 사용해야 해요. 프로덕션에서는 절대 사용하지 말고, 테스트나 개발 도구에서만 활용하세요."

실전 팁

💡 - 항상 try-catch로 에러를 처리하세요

  • 오버라이드 함수는 최대한 단순하게 유지하세요
  • 프로덕션 빌드에서는 이 코드가 제거되도록 조건부 컴파일을 사용하세요

4. 고급 사용 패턴

김개발 씨는 이제 기본기를 탄탄히 다졌습니다. 하지만 실전에서는 어떻게 활용할까요?

마침 팀에서 테스트 프레임워크를 개선하는 프로젝트가 시작되었습니다. 박시니어 씨가 말했습니다.

"좋은 타이밍이네요. 지금까지 배운 걸 실전에 적용해봅시다."

고급 사용 패턴은 INTERNAL_overrideCreateStore의 진정한 잠재력을 끌어내는 기법들입니다. 스토어 목 생성, 타임 트래블 디버깅, 상태 변경 추적, 자동 초기화 등 다양한 패턴이 있습니다.

마치 요리의 기본기를 익힌 후 응용 레시피를 배우는 것과 같습니다. 이런 패턴들을 익히면 테스트 효율성이 크게 향상되고, 디버깅도 훨씬 수월해집니다.

다음 코드를 살펴봅시다.

// 패턴 1: 스토어 자동 추적 및 정리
class StoreManager {
  private stores: StoreApi<any>[] = []
  private originalCreateStore: any

  constructor() {
    this.originalCreateStore = INTERNAL_overrideCreateStore
  }

  // 모든 스토어 생성을 추적합니다
  enableTracking() {
    INTERNAL_overrideCreateStore = (createState) => {
      const store = this.originalCreateStore(createState)
      this.stores.push(store)
      console.log(`스토어 생성됨. 총 ${this.stores.length}개`)
      return store
    }
  }

  // 모든 스토어를 초기 상태로 리셋합니다
  resetAllStores() {
    this.stores.forEach(store => {
      const currentState = store.getState()
      // 각 필드를 초기값으로 재설정
      Object.keys(currentState).forEach(key => {
        if (typeof currentState[key] !== 'function') {
          currentState[key] = undefined
        }
      })
    })
  }

  // 원본 함수를 복원합니다
  restore() {
    INTERNAL_overrideCreateStore = this.originalCreateStore
    this.stores = []
  }
}

팀 회의에서 테스트 리더인 이테스트 씨가 문제를 제기했습니다. "지금 우리 테스트 코드는 각 테스트마다 수동으로 스토어를 초기화하고 있어요.

이게 너무 번거롭고 실수하기도 쉽습니다." 김개발 씨는 이때다 싶어서 손을 들었습니다. "제가 해결책을 제안하고 싶습니다!" 그리고 지난 며칠간 공부한 내용을 바탕으로 StoreManager 클래스를 설명하기 시작했습니다.

StoreManager 패턴이란 무엇일까요? 이것은 마치 도서관 사서가 모든 책의 대출과 반납을 관리하는 것과 같습니다.

스토어가 생성될 때마다 자동으로 등록하고, 필요할 때 일괄적으로 관리할 수 있게 해줍니다. 수동으로 각각의 스토어를 찾아다닐 필요가 없습니다.

전통적인 테스트 방법의 문제점은 무엇이었을까요? 각 테스트 파일에서 사용하는 스토어를 import하고, beforeEach에서 수동으로 초기화 함수를 호출해야 했습니다.

새로운 스토어가 추가될 때마다 테스트 코드도 수정해야 했습니다. 더 큰 문제는 스토어 초기화를 깜빡하면 테스트 간에 상태가 공유되어 간헐적인 실패가 발생한다는 점이었습니다.

StoreManager는 이 모든 문제를 한 번에 해결합니다. 생성자에서 원본 함수를 백업하고, enableTracking 메서드를 호출하면 자동으로 모든 스토어를 추적하기 시작합니다.

이제 애플리케이션 어디서든 create를 호출하면 그 스토어가 자동으로 등록됩니다. 위의 코드를 단계별로 살펴보겠습니다.

먼저 stores 배열이 있습니다. 이것은 생성된 모든 스토어를 담는 컨테이너입니다.

enableTracking 메서드는 INTERNAL_overrideCreateStore를 오버라이드하여 각 스토어를 배열에 추가합니다. 콘솔 로그를 통해 몇 개의 스토어가 생성되었는지 실시간으로 확인할 수 있습니다.

resetAllStores 메서드가 핵심입니다. 이것은 모든 스토어를 순회하면서 상태를 초기화합니다.

함수가 아닌 모든 필드를 undefined로 설정하여 깨끗한 상태로 만듭니다. 마지막으로 restore 메서드는 원본 함수를 복원하고 추적 목록을 비웁니다.

실제 테스트 코드에서는 어떻게 사용할까요? Jest나 Vitest 같은 테스트 프레임워크에서는 setupTests.js 파일에 전역 설정을 작성합니다.

여기서 StoreManager 인스턴스를 생성하고, beforeAll에서 enableTracking을 호출하고, afterEach에서 resetAllStores를 호출하면 됩니다. 이제 모든 테스트 파일에서 자동으로 스토어가 초기화됩니다.

김개발 씨의 발표를 듣던 이테스트 씨가 감탄했습니다. "와, 이거 정말 유용하겠는데요?

바로 적용해봅시다!" 하지만 박시니어 씨가 추가 조언을 했습니다. "좋은 아이디어인데, 한 가지 더 개선할 점이 있어요.

resetAllStores에서 undefined로 설정하는 대신, 각 스토어의 초기 상태를 저장해두었다가 그것으로 복원하는 게 더 안전합니다." 김개발 씨는 고개를 끄덕이며 코드를 개선하기 시작했습니다. stores 배열에 스토어뿐 아니라 초기 상태도 함께 저장하도록 수정했습니다.

이렇게 하면 각 스토어를 정확한 초기 상태로 되돌릴 수 있습니다. 또 다른 고급 패턴으로는 타임 트래블 디버깅이 있습니다.

모든 setState 호출을 가로채서 히스토리에 저장하고, 특정 시점으로 되돌아갈 수 있게 만드는 것입니다. 복잡한 상태 버그를 디버깅할 때 매우 유용합니다.

김개발 씨는 이제 진정한 고급 개발자의 문턱에 서있었습니다.

실전 팁

💡 - 초기 상태를 반드시 저장해두세요

  • 타임 트래블 기능은 메모리를 많이 사용하므로 개발 환경에서만 활성화하세요
  • StoreManager는 싱글톤 패턴으로 구현하면 더 관리하기 쉽습니다

5. 성능 최적화 팁

몇 주 후, StoreManager를 프로젝트에 적용한 팀은 테스트 속도가 느려지는 것을 발견했습니다. 김개발 씨가 프로파일링을 해보니 스토어 초기화 로직에서 병목이 발생하고 있었습니다.

박시니어 씨가 조언했습니다. "고급 기능을 사용할 때는 성능도 함께 고려해야 해요."

INTERNAL_overrideCreateStore를 사용할 때는 성능 영향을 최소화하는 것이 중요합니다. 오버라이드 함수는 모든 스토어 생성 시 실행되므로, 무거운 작업이 있으면 애플리케이션 시작 시간이 길어집니다.

마치 고속도로 톨게이트에서 검사를 추가하면 차량 통과 속도가 느려지는 것과 같습니다. 최적화 기법을 적용하면 기능은 유지하면서 오버헤드를 최소화할 수 있습니다.

다음 코드를 살펴봅시다.

// 최적화된 StoreManager 구현
class OptimizedStoreManager {
  private storeMap = new Map<string, {
    api: StoreApi<any>,
    initialState: any
  }>()
  private originalCreateStore: any

  enableTracking() {
    this.originalCreateStore = INTERNAL_overrideCreateStore

    INTERNAL_overrideCreateStore = (createState) => {
      // 스토어 생성 (원본 함수 호출)
      const store = this.originalCreateStore(createState)

      // 초기 상태를 한 번만 깊은 복사합니다
      const initialState = JSON.parse(
        JSON.stringify(store.getState())
      )

      // 고유 ID 생성 (Symbol 사용으로 충돌 방지)
      const storeId = Symbol('store')

      // Map을 사용하여 O(1) 조회 성능 보장
      this.storeMap.set(storeId.toString(), {
        api: store,
        initialState
      })

      return store
    }
  }

  // 배치 초기화로 성능 개선
  resetAllStores() {
    // 배열 변환 없이 직접 순회
    for (const { api, initialState } of this.storeMap.values()) {
      // 함수 호출 최소화
      api.setState(initialState, true) // replace 플래그 사용
    }
  }

  getStoreCount() {
    return this.storeMap.size
  }
}

김개발 씨는 Chrome DevTools를 열고 성능 프로파일링을 시작했습니다. 테스트 스위트를 실행하면서 CPU 사용량을 모니터링했습니다.

결과는 충격적이었습니다. 스토어 초기화에만 전체 시간의 30%가 소요되고 있었습니다.

"이건 심각한 문제네요." 김개발 씨는 박시니어 씨에게 결과를 보여주었습니다. 박시니어 씨는 코드를 살펴보더니 몇 가지 문제점을 지적했습니다.

"첫째, 배열 대신 Map을 사용하세요. 둘째, 상태 복사를 최적화하세요.

셋째, 불필요한 함수 호출을 줄이세요." 자료구조 선택의 중요성을 알아보겠습니다. 처음 구현에서는 배열을 사용했습니다.

배열은 직관적이지만 조회 성능이 O(n)입니다. 스토어가 100개라면 특정 스토어를 찾는 데 평균 50번의 비교가 필요합니다.

하지만 **Map을 사용하면 O(1)**입니다. 몇 개의 스토어가 있든 즉시 찾을 수 있습니다.

위의 최적화된 코드를 살펴보겠습니다. storeMap은 Map 자료구조입니다.

키는 Symbol을 사용하여 충돌을 원천적으로 방지합니다. 값은 스토어 API와 초기 상태를 담은 객체입니다.

이렇게 하면 나중에 초기화할 때 빠르게 접근할 수 있습니다. 초기 상태 저장 최적화도 중요합니다.

처음 구현에서는 매번 Object.keys를 사용하여 상태를 순회했습니다. 이것은 비효율적입니다.

대신 스토어 생성 시점에 한 번만 깊은 복사를 수행합니다. JSON.parse와 JSON.stringify를 사용하면 간단하지만, 함수나 Symbol은 복사되지 않는다는 점을 주의해야 합니다.

resetAllStores 메서드도 최적화되었습니다. for...of 루프를 사용하여 Map을 직접 순회합니다.

배열로 변환하는 중간 단계가 없으므로 메모리 할당이 줄어듭니다. setState에 두 번째 인자로 true를 전달하면 replace 모드로 동작하여 병합 대신 완전히 교체합니다.

이것이 더 빠릅니다. 실제로 얼마나 빨라졌을까요?

김개발 씨는 최적화 전후를 벤치마킹했습니다. 100개의 스토어를 초기화하는 데 걸리는 시간이 150ms에서 15ms로 줄었습니다.

무려 10배의 성능 향상이었습니다. 테스트 전체 실행 시간도 20% 감소했습니다.

이테스트 씨가 감탄하며 말했습니다. "와, 이 정도면 프로덕션에서도 쓸 수 있겠는데요?" 하지만 박시니어 씨가 제동을 걸었습니다.

"아니요. 아무리 최적화해도 이건 내부 API입니다.

언제든 바뀔 수 있어요. 테스트와 개발 도구에서만 사용하세요." 추가 최적화 기법도 있습니다.

지연 초기화를 사용하면 실제로 사용되는 스토어만 추적할 수 있습니다. WeakMap을 사용하면 스토어가 더 이상 참조되지 않을 때 자동으로 메모리에서 제거됩니다.

프로덕션 빌드에서 코드 제거를 위해 if (process.env.NODE_ENV !== 'production') 조건을 사용할 수도 있습니다. 김개발 씨는 최적화된 코드를 커밋하면서 뿌듯함을 느꼈습니다.

단순히 작동하는 코드를 넘어서 효율적인 코드를 작성하는 것, 그것이 진정한 개발자의 자세라는 것을 배웠습니다.

실전 팁

💡 - 항상 프로파일링부터 시작하세요 (측정하지 않으면 최적화할 수 없습니다)

  • Map과 Set을 적극 활용하세요 (조회 성능이 중요할 때)
  • 깊은 복사는 한 번만 수행하고 재사용하세요

6. 실전 베스트 프랙티스

프로젝트가 성공적으로 마무리되었습니다. 팀 회고 시간에 김개발 씨는 지난 몇 주간의 경험을 정리하여 발표했습니다.

"INTERNAL_overrideCreateStore를 안전하고 효과적으로 사용하는 방법을 공유하고 싶습니다." 모두가 귀를 기울였습니다.

실전 베스트 프랙티스는 실제 프로젝트에서 검증된 사용 패턴과 주의사항을 정리한 것입니다. 환경 분리, 에러 처리, 타입 안정성, 테스트 격리, 문서화 등 여러 측면을 고려해야 합니다.

마치 운전면허를 딴 후 실제 도로에서 안전하게 운전하는 법을 배우는 것과 같습니다. 이런 원칙들을 따르면 강력한 기능을 안전하게 활용할 수 있습니다.

다음 코드를 살펴봅시다.

// 베스트 프랙티스를 적용한 완성형 구현
// test-utils/store-manager.ts
export class TestStoreManager {
  private static instance: TestStoreManager
  private storeMap = new Map<symbol, StoreData>()
  private originalCreateStore: any
  private isEnabled = false

  private constructor() {
    // 싱글톤 패턴
  }

  static getInstance(): TestStoreManager {
    if (!TestStoreManager.instance) {
      TestStoreManager.instance = new TestStoreManager()
    }
    return TestStoreManager.instance
  }

  setup() {
    // 테스트 환경에서만 활성화
    if (process.env.NODE_ENV !== 'test') {
      console.warn('StoreManager는 테스트 환경에서만 사용하세요')
      return
    }

    this.originalCreateStore = INTERNAL_overrideCreateStore

    INTERNAL_overrideCreateStore = (createState) => {
      try {
        const store = this.originalCreateStore(createState)
        const storeId = Symbol('store')

        this.storeMap.set(storeId, {
          api: store,
          initialState: this.cloneState(store.getState())
        })

        return store
      } catch (error) {
        console.error('스토어 생성 중 오류 발생:', error)
        // 오류가 발생해도 원본 함수는 호출
        return this.originalCreateStore(createState)
      }
    }

    this.isEnabled = true
  }

  private cloneState(state: any) {
    // structuredClone 사용 (더 안정적)
    return structuredClone(state)
  }

  resetAll() {
    if (!this.isEnabled) return

    for (const { api, initialState } of this.storeMap.values()) {
      api.setState(initialState, true)
    }
  }

  teardown() {
    if (this.originalCreateStore) {
      INTERNAL_overrideCreateStore = this.originalCreateStore
    }
    this.storeMap.clear()
    this.isEnabled = false
  }
}

김개발 씨의 발표 자료 첫 페이지에는 큰 글씨로 이렇게 적혀 있었습니다. "강력한 기능에는 큰 책임이 따른다." "지난 프로젝트를 진행하면서 많은 시행착오를 겪었습니다.

처음에는 단순히 작동하는 코드를 작성하는 데 집중했지만, 점차 안정성과 유지보수성이 더 중요하다는 것을 깨달았습니다." 첫 번째 원칙은 환경 분리입니다. INTERNAL_overrideCreateStore는 절대 프로덕션에서 사용하면 안 됩니다.

위의 코드를 보면 setup 메서드 첫 줄에 환경 검사가 있습니다. process.env.NODE_ENV가 'test'가 아니면 경고만 출력하고 종료합니다.

이렇게 하면 실수로 프로덕션에 배포되어도 아무런 영향이 없습니다. 두 번째 원칙은 싱글톤 패턴입니다.

StoreManager는 애플리케이션 전체에서 하나의 인스턴스만 존재해야 합니다. 여러 인스턴스가 있으면 서로 충돌할 수 있습니다.

private constructor와 getInstance 메서드를 사용하여 싱글톤을 구현했습니다. 세 번째 원칙은 방어적 프로그래밍입니다.

오버라이드 함수는 try-catch로 감싸야 합니다. 어떤 이유로든 오류가 발생하면 최소한 원본 함수는 호출하여 애플리케이션이 멈추지 않도록 합니다.

또한 isEnabled 플래그를 사용하여 setup이 호출되지 않았을 때는 다른 메서드들이 안전하게 아무 동작도 하지 않습니다. 네 번째 원칙은 안전한 상태 복사입니다.

처음에는 JSON.parse/stringify를 사용했지만, 이것은 Date, Map, Set 같은 객체를 제대로 복사하지 못합니다. structuredClone을 사용하면 거의 모든 타입을 안전하게 복사할 수 있습니다.

이것은 최신 JavaScript의 표준 API입니다. 실제 테스트 파일에서는 어떻게 사용할까요?

김개발 씨는 두 번째 슬라이드로 넘어가며 setupTests.ts 파일의 예제를 보여주었습니다. beforeAll에서 StoreManager.getInstance().setup()을 호출하고, afterEach에서 resetAll()을 호출하고, afterAll에서 teardown()을 호출합니다.

이 세 줄이면 충분합니다. "특히 teardown이 중요합니다." 김개발 씨가 강조했습니다.

"테스트가 끝난 후 반드시 원본 함수를 복원해야 합니다. 그렇지 않으면 다른 테스트 파일이 영향을 받을 수 있습니다." 다섯 번째 원칙은 명확한 네이밍입니다.

클래스명을 TestStoreManager로 지었습니다. 이름만 봐도 테스트용이라는 것을 알 수 있습니다.

메서드명도 setup, resetAll, teardown처럼 명확합니다. 코드를 읽는 사람이 각 메서드의 역할을 즉시 이해할 수 있어야 합니다.

여섯 번째 원칙은 문서화입니다. README.md에 사용법과 주의사항을 명확히 작성해야 합니다.

특히 "이것은 내부 API를 사용하므로 Zustand 버전 업데이트 시 작동하지 않을 수 있음"이라는 경고를 반드시 포함해야 합니다. 미래의 개발자(또는 6개월 후의 자신)가 혼란스러워하지 않도록 배려하는 것입니다.

팀원 한 명이 질문했습니다. "이 모든 걸 꼭 지켜야 하나요?

좀 과한 것 같은데요." 박시니어 씨가 대신 대답했습니다. "처음에는 과해 보일 수 있어요.

하지만 프로젝트가 커지고 팀원이 늘어나면 이런 원칙들이 빛을 발합니다. 특히 내부 API를 사용할 때는 더욱 조심해야 해요." 김개발 씨는 마지막 슬라이드를 보여주었습니다.

"결론적으로 INTERNAL_overrideCreateStore는 양날의 검입니다. 올바르게 사용하면 테스트와 디버깅을 혁신적으로 개선할 수 있습니다.

하지만 잘못 사용하면 애플리케이션 전체를 망칠 수도 있습니다. 항상 신중하게, 원칙을 지키면서 사용하세요." 회의실에 박수가 울려 퍼졌습니다.

김개발 씨는 지난 몇 주간의 여정을 떠올리며 미소 지었습니다. 처음에는 단순히 낯선 API였던 것이 이제는 완전히 이해하고 활용할 수 있는 도구가 되었습니다.

프로그래밍은 이런 것이라고 김개발 씨는 생각했습니다. 끊임없이 배우고, 실험하고, 실패하고, 개선하는 과정.

그리고 그 과정에서 진정한 성장이 일어나는 것입니다.

실전 팁

💡 - 환경 변수로 테스트 환경을 확실히 분리하세요

  • 싱글톤 패턴으로 전역 상태 충돌을 방지하세요
  • try-catch와 fallback으로 방어적 프로그래밍을 실천하세요
  • structuredClone으로 안전한 깊은 복사를 수행하세요
  • README에 주의사항과 사용법을 명확히 문서화하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#React#Zustand#StateManagement#Testing#InternalAPI#React,State Management,CLI

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

마이크로서비스 배포 완벽 가이드

Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.

Spring Boot 상품 서비스 구축 완벽 가이드

실무 RESTful API 설계부터 테스트, 배포까지 Spring Boot로 상품 서비스를 만드는 전 과정을 다룹니다. JPA 엔티티 설계, OpenAPI 문서화, Docker Compose 배포 전략을 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링으로 풀어냅니다.

단위 테스트와 통합 테스트 완벽 가이드

테스트 코드 작성이 처음이라면 이 가이드로 시작하세요. JUnit 5 기초부터 Mockito, MockMvc, SpringBootTest, Testcontainers까지 실무에서 바로 쓸 수 있는 테스트 기법을 단계별로 배웁니다.

Application Load Balancer 완벽 가이드

AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.

고객 상담 AI 시스템 완벽 구축 가이드

AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.