static create 패턴 - 생성자 대신 정적 메서드로 객체 생성

언제 사용?

  • 생성 로직이 복잡하거나 유효성 검사 필요
  • 생성 실패 시 null/Result 반환 (생성자는 항상 인스턴스 반환)
  • 팩토리 패턴, 싱글톤
class User {
  private constructor(private readonly name: string) {}

  // 유효성 검사 + 실패 시 null 반환
  static create(name: string): User | null {
    if (!name || name.length < 3) return null
    return new User(name)
  }
}

// 팩토리 패턴
class Shape {
  static create(type: 'circle' | 'rect', size: number): Shape {
    return type === 'circle' ? new Circle(size) : new Rectangle(size)
  }
}

// 싱글톤
class Config {
  private static instance: Config
  private constructor() {}
  static create() {
    return Config.instance ??= new Config()
  }
}

TIP

단순 초기화만 필요하면 일반 생성자가 더 직관적. 복잡한 생성 로직에만 사용.

#381

React Children API - 가능하지만 비추천

WARNING

암묵적 의존성, 타입 안전성 부족, 매 렌더 트리 순회, 예측 불가능

재귀 순회

function traverseReactNode(children: ReactNode, callback, typeToMatch?) {
  Children.forEach(children, (child) => {
    if (!isValidElement(child)) return
    if (child.type === Fragment) {
      traverseReactNode(child.props.children, callback, typeToMatch)
      return
    }
    if (child.type === typeToMatch) callback(child)
    if (child.props?.children) {
      traverseReactNode(child.props.children, callback, typeToMatch)
    }
  })
}

동적 래핑

const renderChildren = (children) => {
  const elements = React.Children.toArray(children)
  const hasLink = elements.some(
    (el) => React.isValidElement(el) && el.props.url
  )
  return hasLink ? children : <ul>{children}</ul>
}
// toArray는 string, number도 포함 → isValidElement 체크 필수

대안: Compound Component

// ❌ 마법처럼 동작 (예측 불가)
<Tabs>{/* 어디에 넣든 Tab 찾아줌 */}</Tabs>

// ✅ Compound Component
<Tabs.Root>
  <Tabs.List>
    <Tabs.Trigger value="a">A</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="a">Content</Tabs.Content>
</Tabs.Root>

React 팀도 2021년부터 Children API 사용 권장하지 않음: “Using Children is uncommon and can lead to fragile code”

역사적 배경: 2013년엔 Context API도 없었음. “선언형”이라면서 Children API로 명령형 트리 순회 제공하는 이중성.


#496

간단한 Redux 스타일 Store 구현

type Reducer<S, A> = (state: S, action: A) => S
type Listener<S> = (state: S) => void

interface Store<S, A> {
  getState: () => S
  subscribe: (listener: Listener<S>) => () => void
  dispatch: (action: A) => A
}

function createStore<S, A>(
  reducer: Reducer<S, A>,
  preloadedState: S
): Store<S, A> {
  let currentState = preloadedState
  let listeners: Listener<S>[] = []

  return {
    getState: () => currentState,
    subscribe: (listener) => {
      listener(currentState)
      listeners.push(listener)
      return () => {
        listeners = listeners.filter((l) => l !== listener)
      }
    },
    dispatch: (action) => {
      currentState = reducer(currentState, action)
      listeners.forEach((l) => l(currentState))
      return action
    },
  }
}
#529

Maybe Monad로 null 체크 체이닝

type Maybe<T> = T | null

const Maybe = {
  of: <T>(value: T): Maybe<T> => (value != null ? value : null),
  map: <T, U>(m: Maybe<T>, fn: (v: T) => U): Maybe<U> =>
    m != null ? Maybe.of(fn(m)) : null,
}

// 사용 예: DOM 요소 찾아서 스크롤
Maybe.of(scrollElement.current)
  .map((root) => root.querySelector(`#${id}`))
  .map((target) => target.getClientRects()[0])
  .map((rect) => {
    root.scrollLeft = rect.left
  })

Generator로 early return 패턴도 가능

function* handleScroll(id) {
  const root = scrollElement.current
  if (!root) return
  const target = root.querySelector(`#${id}`)
  if (!target) return
  yield target.getClientRects()[0]?.left ?? 0
}
#533

여러 단계의 비동기 UI 업데이트를 setLoading → setProgress → setLoading(false)로 흩뿌리지 않고 async generator의 yield 시퀀스로 시간 순서대로 표현. 원본 트릭은 @ericclemmons.

type FlowEvent<P extends string, D> = { phase: P; data: D }

async function* loadingFlow(
  signal: AbortSignal,
): AsyncGenerator<FlowEvent<'starting' | 'slow' | 'done', string>> {
  yield { phase: 'starting', data: 'Starting…' }
  await wait(1000, signal)
  yield { phase: 'slow', data: 'Taking longer than usual' }
  await wait(2000, signal)
  yield { phase: 'done', data: 'Got it 🎉' }
}

원본의 빈 자리 → 소비 레이어 책임

원본 데모는 4가지가 비어 있음. hook이든 actor든 wrapping 레이어에서 채워야 함:

  • cancellation: generator에 AbortSignal 주입 → await 대상(wait, fetch)이 signal-aware해야 진짜 취소됨
  • 재진입: 새 실행 시 직전 iterator abort
  • error path: generator 내부 throw → 상태 노출
  • unmount/teardown: cleanup으로 in-flight iterator abort
  • stale closure: 최신 클로저 참조 (hook은 ref, actor는 input)
  • typed: phase/data 모두 좁힘

어디에 맞는가

generator는 시간이 코드의 한 방향(↓)으로만 흐르는 모델. 그 모양에 맞는 시나리오:

  • 멀티 단계 loading 메시지 (위 loadingFlow)
  • progressive search — tier별로 점진적 결과 yield, tier 경계가 자연스러운 cancellation point, debounce 불필요
  • optimistic mutation — { phase: 'optimistic' | 'reconciled' | 'rolledBack', data } 전이를 4개 콜백 대신 한 함수로
#540