<div
  dangerouslySetInnerHTML={{
    __html: `
      <img src="http://unsplash.it/100/100?random" onload="console.log('you got hacked');" />
    `,
  }}
/>

가끔 아주 가끔 이상한 일을 해야할때가 있는데 그럴때는 이렇게 하면 된다.


#193
#200

lazyimport했을 경우 ChunkLoadError가 발생하는데 이럴 경우 어떻게 대응할 수 있는지 정리해둔 글들.

#203

모바일 웹뷰에서 특정 영역을 zoom할수 있게 해달라는 요청이 있어서 적용한 내역. 정확히 기억은 안나는데 react-prismazoom를 선택했다.


그 외 비슷한

#269
const root = createRoot(document.querySelector('#confirm-root'))

root.render(<Confirm />)

confirm ui를 만들다보면 window.confirm을 호출하는 방식으로 사용하는게 가장 좋은 방법인데 (안그러면 state로 관리해야하고 결국 이건 무의미한 코드의 반복이다.) 이걸 react로 구현하려면 결국 render를 사용해야함. react-confirm, react-confirm-alert 둘다 소스를 보면 비슷한 방식으로 접근한다.

#270

Avoiding premature abstraction with Unstyled React Components (buildui.com)

React 컴포넌트를 작성할 때, 불필요한 추상화를 피하고 컴포넌트의 유연성을 유지하는 방법. 특히 스타일이 없는 컴포넌트를 통해 어떻게 컴포넌트의 기능에 집중할 수 있는지를 설명.

import React, { ComponentProps, FC, ReactNode } from 'react'

function Spinner() {
  return (
    <span className="absolute inset-0 flex items-center justify-center">
      `<Spinner />`
    </span>
  )
}

type LabelProps = ComponentProps<'span'>

function Label({ children, ...rest }: LabelProps) {
  return <span {...rest}>{children}</span>
}

type LoadingButtonProps = ComponentProps<'button'> & {
  children: FC | ReactNode
}

/**
 * @description 버튼이 비활성화되었을 때 로딩 스피너를 표시할 수 있는 버튼 컴포넌트입니다. 이 컴포넌트는 children 속성으로 함수나 React 노드를 받을 수 있으며, 버튼이 비활성화되면 스피너가 표시되고, 텍스트는 보이지 않게 됩니다.
 */
function LoadingButton({
  disabled,
  className,
  children,
  ...rest
}: LoadingButtonProps) {
  return (
    <button {...rest} className={`${className} relative`} disabled={disabled}>
      {typeof children === 'function' ? (
        children({})
      ) : (
        <>
          {disabled && <Spinner />}
          <Label className={disabled ? 'invisible' : ''}>{children}</Label>
        </>
      )}
    </button>
  )
}

LoadingButton.Spinner = Spinner
LoadingButton.Label = Label

이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다.

#311
💡

이 패턴이 “상태 머신”처럼 들린다면, 그리 놀랄 일도 아닙니다. 결국, 선택의 문제는 상태 머신을 구축할지 말지가 아니라, 그것을 암시적으로 구축할지 명시적으로 구축할지에 달려 있습니다.

stateDiagram-v2
  [*] --> Mounting

  Mounting --> AwaitingEmailInput
  Mounting --> AwaitingCodeInput

  AwaitingEmailInput --> SubmittingEmail

  SubmittingEmail --> AwaitingCodeInput
  SubmittingEmail --> AwaitingEmailInput

  AwaitingCodeInput --> SubmittingCode

  SubmittingCode --> Success
  SubmittingCode --> AwaitingCodeInput

  Success --> [*]

#319

폼 제출 시 FormData 테스트

test('폼 제출 시 formData 확인', () => {
  const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.target as HTMLFormElement)
    const data = Object.fromEntries(formData.entries())
    expect(data).toEqual({ username: 'testuser', password: 'password' })
  })

  render(<LoginForm onSubmit={handleSubmit} />)

  fireEvent.change(screen.getByPlaceholderText('아이디'), {
    target: { value: 'testuser' },
  })
  fireEvent.change(screen.getByPlaceholderText('비밀번호'), {
    target: { value: 'password' },
  })
  fireEvent.click(screen.getByRole('button', { name: '로그인' }))

  expect(handleSubmit).toHaveBeenCalled()
})
#326

AuthContext 테스트 패턴

// localStorage 함수 테스트
vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => {
  if (key === 'auth_token') return 'test_token'
  return null
})

// AuthProvider 상태 테스트
const TestComponent = () => {
  const { token, login, logout } = useAuth()

  return (
    <div>
      <p data-testid="token">{token || 'null'}</p>
      <button onClick={() => login('test_token')}>Login</button>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

// useAuth 훅 테스트
it('AuthProvider 외부에서 호출하면 에러', () => {
  expect(() => renderHook(() => useAuth())).toThrowError()
})

it('AuthProvider 내부에서 정상 동작', () => {
  const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>
  const { result } = renderHook(() => useAuth(), { wrapper })
  expect(result.current).toHaveProperty('token', null)
})
#328

조건부 렌더링을 한다고 했을때 예전에는 주로 B로 처리했던 것 같은데 디버깅 때문에 고생해서 그런지 생각이 바뀌었다.

function renderA() {
  return <Item isPacked={true} name="Space suit" />
}

function renderB() {
  return <>{isPacked ? <Item name="Space suit" /> : null}</>
}

조건부 렌더링을 보다 간결하게 표현하기 위해 If, Then, 그리고 Else 컴포넌트 개념을 차용하는 방법. 이 컴포넌트는 If에서 condition prop을 통해 조건을 받아들이고, 자식으로 ThenElse를 받아 각각의 내용을 렌더링하도록 한다.

type Props = {
  /** 렌더링할 조건 */
  condition: boolean
  /** 자식 컴포넌트 (Then, Else 포함) */
  children: React.ReactNode
}

/**
 * If 컴포넌트는 조건부 렌더링을 위한 컴포넌트
 */
function If({ condition, children }: Props) {
  let thenChild = null
  let elseChild = null

  React.Children.forEach(children, (child) => {
    if (!React.isValidElement(child)) return
    if (child.type === Then) thenChild = child
    if (child.type === Else) elseChild = child
  })

  return condition ? thenChild : elseChild
}

/**
 * Then, Else 컴포넌트는 조건이 참 또는 거짓일 때 렌더링되는 콘텐츠를 포함
 */
const Then = ({ children }: React.PropsWithChildren) => <>{children}</>
const Else = ({ children }: React.PropsWithChildren) => <>{children}</>
#335

useStateObject는 React의 useState를 확장한 가벼운 래퍼로, 객체 상태 관리를

#354
function toggleReducer(state, action) {
  switch (action.type) {
    default:
      return state
  }
}

function useToggle({ reducer = toggleReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, {})
  return { state, dispatch }
}

export function Component() {
  useToggle({
    reducer(currentState, action) {
      console.log(currentState, action)
    },
  })
}

useReducer를 이용한 커스텀훅 사용에 대한 간단한 예시. 생각해보니 reducer를 전달해서 재사용하는 방법은 잘 생각못했는데 응용할 수 있을 것 같다.



Footnotes

  1. 리듀서를 사용하여 예측 가능하고 테스트 가능한 방식으로 상태 업데이트 및 작업을 캡슐화하는 방법과 State Reducer 패턴을 사용하여 이를 사용하는 구성 요소에서 상태 업데이트를 추상화하여 해당 구성 요소가 특정 기능에 더 집중하도록 만드는 방법을 설명.

#36
import * as React from 'react'

type ContainerProps = {
  children: typeof Body
}

type BodyProps = {
  id: string
}

function Container({ children }: ContainerProps) {
  const id = '1234'

  return <>{children({ id })}</>
}

export function Body({ id }: BodyProps) {
  return <>{id}</>
}

export function Page() {
  return <Container>{Body}</Container>
}

컨테이너/프레젠테이션 패턴을 활용한 React 컴포넌트 구조.

  • 컨테이너 컴포넌트(Container)는 상태를 관리하고 데이터를 하위 컴포넌트에 전달함.
  • 프레젠테이션 컴포넌트(Body)는 데이터를 받아서 UI를 렌더링함.
  • 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리함.
#386

Footnotes

  1. React에서 컴포지션의 개념과 “Prop Drilling”을 피하기 위해 React에서 사용하는 방법을 설명.

  2. Headless UI 컴포넌트의 개념을 소개. 헤드리스 UI 컴포넌트가 무엇인지, 기존 UI 컴포넌트와 어떻게 다른지 설명하고, 다양한 프로그래밍 언어와 프레임워크에서 헤드리스 UI 컴포넌트를 구현하는 방법에 대한 예제.

#46

How To Maintain A Large Next.js Application — Smashing Magazine

  • TypeScript 사용
  • Lerna , Nx , Rush , Turborepo , yarn workspaces를 사용하여 Mono-Repo 구조 사용
  • Hygen과 같은 코드 생성기를 사용하여 상용구 코드 생성
  • Redux 툴킷을 통해 하위 상용구와 함께 Redux와 같이 잘 설정된 패턴 사용
  • 비동기 데이터를 가져오기 위해 React 쿼리 또는 SWR 사용
  • Husky와 함께 Commitizen 및 Semantic Release 사용
  • UI 구성 요소 시각화를 위해 스토리북 사용
  • 처음부터 유지 관리 가능한 테스트 작성
  • Dependabot을 사용하여 자동으로 패키지 업데이트
  • Production Checklist | Next.js
#48

React 제네릭 컴포넌트 패턴 - <T extends Record<string, any>>로 row/item 타입 추론

#480

React 19 Concurrent 훅 - useTransition, useOptimistic vs React Query

#495

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

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

재귀 순회

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

React에서 iframe 내부에 컴포넌트 렌더링

// react-frame-component 사용 (권장)
import Frame from 'react-frame-component'
;<Frame head={<style>{`body { margin: 0; }`}</style>}>
  <MyComponent />
</Frame>

// 직접 구현: createPortal + contentDocument
function IframeRenderer({ children }) {
  const iframeRef = useRef<HTMLIFrameElement>(null)
  const [mountNode, setMountNode] = useState<HTMLElement | null>(null)
  useEffect(() => {
    const iframe = iframeRef.current
    const handleLoad = () => setMountNode(iframe?.contentDocument?.body ?? null)
    iframe?.addEventListener('load', handleLoad)
    if (iframe?.contentDocument?.readyState === 'complete') handleLoad()
    return () => iframe?.removeEventListener('load', handleLoad)
  }, [])
  return (
    <>
      <iframe ref={iframeRef} />
      {mountNode && createPortal(children, mountNode)}
    </>
  )
}

iframe 내부는 부모 CSS 미적용 (스타일 별도 주입 필요), 이벤트 버블링 안 됨. 단순 스타일 격리 목적이면 Shadow DOM 고려.

#506

대용량 리스트에서 selected 아이템 조회 최적화

O(n×m) → O(n+m)로 개선: Map으로 인덱싱

// 기존: 매번 find (느림)
selected.map((key) => items.find((item) => item.key === key))

// 개선: Map 인덱싱 (빠름)
const itemsMap = new Map(items.map((item) => [item.key, item]))
selected.map((key) => itemsMap.get(key)).filter(Boolean)

React에서 백그라운드 인덱싱:

function useItemsIndex(items: Item[]) {
  const [map, setMap] = useState(new Map())
  const [ready, setReady] = useState(false)

  useEffect(() => {
    // 청크 단위로 처리하여 UI 블로킹 방지
    const newMap = new Map(items.map((item) => [item.key, item]))
    setMap(newMap)
    setReady(true)
  }, [items])

  return { map, ready }
}

10만개 이상이면 Web Worker 고려.

#508

RSC에서 날짜 처리: 쿠키 기반 타임존

문제: Date 객체 직렬화, 서버/클라이언트 타임존 불일치, 하이드레이션 FOUT

해결: 서버에서 타임존 적용해서 렌더링

// middleware.js - 타임존 감지
export function middleware(request) {
  const timezone =
    request.cookies.get('user-timezone')?.value ||
    request.geo?.timezone || // Vercel
    'UTC'

  const response = NextResponse.next()
  response.headers.set('x-user-timezone', timezone)
  return response
}

// 서버 컴포넌트
const getUserTimezone = cache(() => {
  return headers().get('x-user-timezone') || 'UTC'
})

// 클라이언트 - 타임존 자동 감지 후 쿠키 저장
useEffect(() => {
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
  document.cookie = `user-timezone=${tz}; path=/; max-age=31536000`
}, [])

첫 방문은 인프라 추정값 사용, 이후 정확한 타임존 적용. FOUT 없음.


대안: useSyncExternalStore (client component 전용)

'use client'

const timezoneStore = {
  getSnapshot: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
  getServerSnapshot: () => 'UTC',
  subscribe: () => () => {},
}

function useTimezone() {
  return useSyncExternalStore(
    timezoneStore.subscribe,
    timezoneStore.getSnapshot,
    timezoneStore.getServerSnapshot
  )
}

서버: UTC → 클라이언트: 실제 타임존. 하이드레이션 에러 없음, 대신 FOUT 발생.

#509

RTL에서 overflow scroll 상태 테스트

// scrollWidth > clientWidth면 가로 스크롤 필요
const isOverflowScrollable = (el) => ({
  horizontal: el.scrollWidth > el.clientWidth,
  vertical: el.scrollHeight > el.clientHeight,
})

test('overflow 확인', () => {
  render(<ScrollableComponent parentWidth={300} childWidth={500} />)

  const parent = screen.getByTestId('parent-container')

  expect(isOverflowScrollable(parent).horizontal).toBe(true)
})

test('동적 크기 변경', () => {
  const { rerender } = render(<Comp parentWidth={400} childWidth={300} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    false
  )

  rerender(<Comp parentWidth={400} childWidth={600} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    true
  )
})

DOM 완전 렌더링 후 측정해야 정확. getBoundingClientRect()는 실제 크기 반환.

#521

react-is로 children에서 특정 컴포넌트 필터링

react-is는 React 공식 패키지로, React 요소 타입 확인 유틸리티. typeofinstanceof로는 React.memo(), forwardRef() 등을 구분할 수 없어서 필요함. 라이브러리 개발, children 타입 검사에 필수.

import * as ReactIs from 'react-is'

// 특정 컴포넌트 타입인지 확인
function isComponentType<T>(element: ReactNode, Type: ComponentType<T>) {
  return ReactIs.isElement(element) && element.type === Type
}

// children에서 특정 컴포넌트만 필터링
function filterChildren<T>(children: ReactNode, Type: ComponentType<T>) {
  return Children.toArray(children).filter(
    (child): child is ReactElement<T> =>
      ReactIs.isElement(child) && child.type === Type
  )
}

// 사용: Layout에서 Header, Content, Footer 분리
const Layout = ({ children }) => {
  const headers = filterChildren(children, Header)
  const contents = filterChildren(children, Content)

  return (
    <div>
      <div className="header">{headers}</div>
      <div className="content">{contents}</div>
    </div>
  )
}
#528

컴포넌트가 렌더링하는 요소를 외부에서 제어하는 세 가지 패턴.

언제무엇이 필요한지로 고른다.

#538

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

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

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

절차

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 🎉' }
}

확인 — 감싸는 레이어(hook이든 actor든)에서 여섯을 다 채웠는가. 원본 데모는 전부 비어 있다:

  • 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 모두 좁힘
#540

바깥 클릭 감지를 useEffect 없이 ref callback 하나로 만든다.

언제: React 19 이상일 때. 18 이하는 아래 함정을 보고 useEffect 쪽을 쓴다.

절차

function useClickOutside(handler) {
  const handlerRef = useRef(handler)
  handlerRef.current = handler // stale closure 방지

  return useCallback((node) => {
    const handleClick = (e) => {
      if (!node.contains(e.target)) handlerRef.current()
    }

    document.addEventListener('mousedown', handleClick)
    return () => document.removeEventListener('mousedown', handleClick)
  }, [])
}

// 사용
function Modal({ onClose }) {
  const ref = useClickOutside(onClose)
  return <div ref={ref}>...</div>
}

확인: 모달을 여러 번 열고 닫은 뒤 바깥을 클릭한다. 핸들러가 한 번만 불리면 cleanup이 돌고 있는 것이다 — 안 돌면 옛 리스너가 쌓여 여러 번 불린다.

함정

  • ref callback은 DOM 노드 참조만 전달한다. 안에서 등록한 리스너 같은 사이드이펙트는 자동 해제되지 않는다.
  • React 18 이하에서는 이 패턴이 안 된다. unmount 시 callback(null)은 오지만 동일 함수 참조가 없어 removeEventListener를 못 부른다 → 리스너를 별도 ref에 저장해야 한다. cleanup return을 공식 지원하는 건 React 19부터다.

참고

dnd-kit drop 직후 튕김 — setQueryData 는 화면을 한 틱 늦게 바꾼다

dnd-kit Sortable + react-query에서 drop 직후 아이템이 원래 자리로 돌아갔다가 새 자리로 점프한다. 12프레임짜리 튕김이고, DragOverlay를 쓰면 더 두드러진다. onDragEnd에…

Fiber 가 렌더링을 쪼갤 수 있는 이유 — 콜 스택 대신 세 포인터

재귀(콜 스택) 대신 child / sibling / return 세 포인터로 트리를 순회한다 → 스택 없이 DFS pre-order, 언제든 중단·재개 가능. Fiber가 렌더링을 쪼갤 수 있는 이유.

재귀로 짜면 진행 상태가 JS 콜 스택에 쌓이는데, requestIdleCallback으로 도중에 yield하면 그 스택을 버리게 된다. 재개하려면 스택을 다시 쌓아야 해서 “어디까지 했는지”를 외부에서 들고 있어야 한다.

그 상태를 workInProgress 포인터 하나로 환원한다. 노드마다 걸린 세 링크를 직접 따라가므로 호출 스택이 필요 없다.

1. child 있으면?      → 내려간다
2. root에 도달했으면? → 종료
3. sibling 없으면?    → return(부모)으로 올라가며 반복
4. sibling 있으면?    → 옆으로 간다

내려가는 길 = beginWork, 올라오는 길 = completeWork. 한 노드를 두 번(하강·상승) 지나는 흐름이 그대로 두 단계로 갈린다.


#560

React 에 싱글톤 패턴 접목하기

싱글톤이 안티패턴이라는 말은 싱글톤이 아니라 React에 붙이던 방식을 겨눈 것이었다.

언제프레임워크를 모르는 상태 덩어리(토스트 매니저 같은)를 React에 stale 없이 연결할 때. 코어(표준 JS 클래스)와 어댑터(useSyncExternalStore 한 겹)를 갈라두면 Vue·Svelte는 어댑터만 갈아끼운다.

#563

useDeferredValue 의 보장 범위 — 값은 보장, 반영 시점은 미보장

검색어를 치면 입력창은 바로 바뀌어야 하는데, 아래 무거운 목록까지 매번 다시 그리면 입력이 밀린다. useDeferredValue(value)는 같은 값을 한 박자 늦게 따라오는 짝으로 하나 더 만들어준다.

언제같은 값을 쓰는 무거운 목록이 입력마다 다시 그려져 입력이 밀릴 때.

#570

로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS

로딩 스피너의 깜빡임은 양 끝에서 따로 생기는데 CSS는 한쪽만 잡는다.

언제스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊길 때. 앞쪽인지 뒤쪽인지부터 판별한다.

#573

useMemo 와 useState 의 갈림길 — 틀린 동작인가 느린 동작인가

테마에서 뽑은 강조색처럼 한 번 정해지면 유지돼야 하는 값은 useMemo에 두면 안 된다.

언제갈림길은 하나다 — "이 값이 재계산되면 틀린 동작인가, 아니면 그냥 느린 동작인가."

Suspense는 비동기를 동기처럼 보이게 만든다. 데이터가 아직 없으면 promise를 던지고, React가 그걸 받아 기다렸다가 다시 그린다. 그래서 상태를 조율하는 대신 입력을 미루고 → 질의하고 → 그리는 순서로 끝난다.

언제: 비동기 데이터를 그리는데 로딩·에러가 늘면서 화면이 튈 때. 또는 새로 만들면서 어느 쪽으로 갈지 고를 때.

절차

  1. 검색어·결과·로딩 중인지·에러인지를 각각 들고 있는지 본다. 넷이 따로 켜지고 꺼지니 실제로는 있을 수 없는 조합까지 코드가 표현할 수 있다 — “로딩 중이면서 동시에 에러” 같은 것. 그 조합이 한 프레임이라도 그려지면 화면이 튄다.
  2. 분기를 if/else에서 트리로 옮긴다 — 로딩은 <Suspense>, 에러는 <ErrorBoundary>, 성공은 본문.
  3. 본문에 loading·error를 인자로 넘기지 않는다. 안 넘겨도 가장 가까운 경계가 대신 잡기 때문이다 — render는 결과 하나만 받는다.

확인: 있을 수 없는 조합을 적을 수 있는가. 적을 수 없으면 된 것이다 — 상태가 로딩이거나, 에러거나, 성공이거나 셋 중 하나가 된다.

함정: 스피너가 너무 빨리 뜨고 사라지는 건 이 축으로 안 고쳐진다 — 다른 축이다 → 573


곁가지 — 인자를 위로 들어올려 바깥이 처리하게 하는 이 모양은 대수적 효과(algebraic effects)의 handler와 같다.1

stale-while-revalidate(두 시계·isStale) 부분은 570으로.

Footnotes

  1. try/catch와 비슷하다 — 중간 함수들은 몰라도 되고 가장 가까운 handler가 받는다. 다른 점은 처리하고 끝나지 않는다는 것이다. handler가 값을 돌려주면 효과를 일으킨 자리로 되돌아가 실행이 이어진다. (Algebraic Effects for the Rest of Us)

#575

useMemo 캐시 적중 진단 — 매 렌더 새로 만들어지는 deps

useMemo가 실제로 재사용되고 있는지 계산 횟수로 확인한다.

언제무거운 계산을 useMemo로 감쌌는데 렌더 시간이 줄지 않을 때.

#578

GenUI 도입 — 모델이 정하는 게 인자인가 배치인가 코드인가

Static/Declarative/Open-ended 판별부터 카탈로그 설계·신뢰 경계·회귀 전략까지.

언제누군가 "우리도 GenUI 해보자"고 말했을 때. 회의에서 그 단어가 나오면 셋 중 뭔지부터 확정한다.

#587

프로그래밍 원칙을 조작 가능하게 — 증상·절차·경계로 다시 쓰기

선언형 슬로건("파생하라", "추상을 미뤄라")을 언제 적용하고 언제 적용하지 말지의 절차로 바꾼 일곱 레시피.

언제원칙 목록이 "무엇을 믿어라"는 주는데 "언제 어떻게 하라"를 안 줄 때. 원칙마다 증상 → 절차 → 경계(언제 적용하지 말 것) 를 붙인다. 경계가 핵심이다 — 슬로건은 자기가 틀리는 지점을 말하지 않는다.

#588

React 컴포넌트 안정성 규칙 열셋 — 앱 컴포넌트와 라이브러리 컴포넌트

tearing·FOUC·hydration 부터 Activity·ViewTransition 까지, 각 규칙이 언제 필요한지 적용 범위와 함께.

언제컴포넌트에 이 규칙을 적용할지 정할 때. 먼저 이게 앱 컴포넌트인지 라이브러리 컴포넌트인지 가른다. 앱 컴포넌트는 자기가 어디서 쓰이는지 알기 때문에 절반은 해당이 없다 — 전부 예방적으로 적용하면 그게 새로운 over-engineering 이다.

#594

네이버 지도 마커 + fitBounds

좌표를 데이터에서 계산하고, 0·1·N 카메라를 가르고, 줌 오버슈트·여백·마커 생명주기를 처리하는 절차.

언제naver.maps v3 에 마커를 여러 개 찍고 카메라를 자동으로 맞출 때. 증상이 있으면 바로 간다.

#595