<div
dangerouslySetInnerHTML={{
__html: `
<img src="http://unsplash.it/100/100?random" onload="console.log('you got hacked');" />
`,
}}
/>
가끔 아주 가끔 이상한 일을 해야할때가 있는데 그럴때는 이렇게 하면 된다.
// cannot be used as a JSX component
@types/react, @types/react-dom에 대한 참조가 잘못되면서 발생하는 이슈
- React + Webpack: ChunkLoadError: Loading chunk X failed. | by Raphaël Léger | Medium
- How to fix ChunkLoadError in your ReactJS application - Codemzy’s Blog
lazy로 import했을 경우 ChunkLoadError가 발생하는데 이럴 경우 어떻게 대응할 수 있는지 정리해둔 글들.
모바일 웹뷰에서 특정 영역을 zoom할수 있게 해달라는 요청이 있어서 적용한 내역. 정확히 기억은 안나는데 react-prismazoom를 선택했다.
그 외 비슷한
const root = createRoot(document.querySelector('#confirm-root'))
root.render(<Confirm />)
confirm ui를 만들다보면 window.confirm을 호출하는 방식으로 사용하는게 가장 좋은 방법인데 (안그러면 state로 관리해야하고 결국 이건 무의미한 코드의 반복이다.)
이걸 react로 구현하려면 결국 render를 사용해야함. react-confirm, react-confirm-alert 둘다 소스를 보면 비슷한 방식으로 접근한다.
if (!data) {
throw fetch()
}
Suspense는 던져진 promise를 캐치해서 resolve될 때까지 fallback을 렌더하고, resolve되면 다시 렌더한다. 던지는 건 예외와 같은 메커니즘이다.
- https://github.com/facebook/react/blob/eb8feb71096eec5c885b2a4c7d8d030d3622f265/packages/react/src/ReactLazy.js#L222
- https://github.com/TanStack/query/blob/b8e35598ef24855a2792414faea5721d833fa61e/packages/react-query/src/suspense.ts#L61
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/throw
- https://jser.pro/ddir/rie?reactVersion=18.3.1&codeKey=ud62nsxll29yy0dzba8
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
이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다.
💡이 패턴이 “상태 머신”처럼 들린다면, 그리 놀랄 일도 아닙니다. 결국, 선택의 문제는 상태 머신을 구축할지 말지가 아니라, 그것을 암시적으로 구축할지 명시적으로 구축할지에 달려 있습니다.
stateDiagram-v2
[*] --> Mounting
Mounting --> AwaitingEmailInput
Mounting --> AwaitingCodeInput
AwaitingEmailInput --> SubmittingEmail
SubmittingEmail --> AwaitingCodeInput
SubmittingEmail --> AwaitingEmailInput
AwaitingCodeInput --> SubmittingCode
SubmittingCode --> Success
SubmittingCode --> AwaitingCodeInput
Success --> [*]
폼 제출 시 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()
})
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)
})
조건부 렌더링을 한다고 했을때 예전에는 주로 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을 통해 조건을 받아들이고, 자식으로 Then과 Else를 받아 각각의 내용을 렌더링하도록 한다.
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}</>
useSyncExternalStore 활용 가능한 부분들
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
-
리듀서를 사용하여 예측 가능하고 테스트 가능한 방식으로 상태 업데이트 및 작업을 캡슐화하는 방법과 State Reducer 패턴을 사용하여 이를 사용하는 구성 요소에서 상태 업데이트를 추상화하여 해당 구성 요소가 특정 기능에 더 집중하도록 만드는 방법을 설명. ↩
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를 렌더링함. - 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리함.
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
React 제네릭 컴포넌트 패턴 - <T extends Record<string, any>>로 row/item 타입 추론
React 19 Concurrent 훅 - useTransition, useOptimistic vs React Query
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로 명령형 트리 순회 제공하는 이중성.
- react-children-utilities - deepMap, deepFind, deepFilter
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 고려.
대용량 리스트에서 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 고려.
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 발생.
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()는 실제 크기 반환.
react-is로 children에서 특정 컴포넌트 필터링
react-is는 React 공식 패키지로, React 요소 타입 확인 유틸리티. typeof나 instanceof로는 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>
)
}
여러 단계의 비동기 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모두 좁힘
바깥 클릭 감지를 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. 한 노드를 두 번(하강·상승) 지나는 흐름이 그대로 두 단계로 갈린다.
React 에 싱글톤 패턴 접목하기
싱글톤이 안티패턴이라는 말은 싱글톤이 아니라 React에 붙이던 방식을 겨눈 것이었다.
언제 — 프레임워크를 모르는 상태 덩어리(토스트 매니저 같은)를 React에 stale 없이 연결할 때. 코어(표준 JS 클래스)와 어댑터(useSyncExternalStore 한 겹)를 갈라두면 Vue·Svelte는 어댑터만 갈아끼운다.
useDeferredValue 의 보장 범위 — 값은 보장, 반영 시점은 미보장
검색어를 치면 입력창은 바로 바뀌어야 하는데, 아래 무거운 목록까지 매번 다시 그리면 입력이 밀린다. useDeferredValue(value)는 같은 값을 한 박자 늦게 따라오는 짝으로 하나 더 만들어준다.
언제 — 같은 값을 쓰는 무거운 목록이 입력마다 다시 그려져 입력이 밀릴 때.
로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS
로딩 스피너의 깜빡임은 양 끝에서 따로 생기는데 CSS는 한쪽만 잡는다.
언제 — 스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊길 때. 앞쪽인지 뒤쪽인지부터 판별한다.
useMemo 와 useState 의 갈림길 — 틀린 동작인가 느린 동작인가
테마에서 뽑은 강조색처럼 한 번 정해지면 유지돼야 하는 값은 useMemo에 두면 안 된다.
언제 — 갈림길은 하나다 — "이 값이 재계산되면 틀린 동작인가, 아니면 그냥 느린 동작인가."
Suspense는 비동기를 동기처럼 보이게 만든다. 데이터가 아직 없으면 promise를 던지고, React가 그걸 받아 기다렸다가 다시 그린다. 그래서 상태를 조율하는 대신 입력을 미루고 → 질의하고 → 그리는 순서로 끝난다.
언제: 비동기 데이터를 그리는데 로딩·에러가 늘면서 화면이 튈 때. 또는 새로 만들면서 어느 쪽으로 갈지 고를 때.
절차
- 검색어·결과·로딩 중인지·에러인지를 각각 들고 있는지 본다. 넷이 따로 켜지고 꺼지니 실제로는 있을 수 없는 조합까지 코드가 표현할 수 있다 — “로딩 중이면서 동시에 에러” 같은 것. 그 조합이 한 프레임이라도 그려지면 화면이 튄다.
- 분기를
if/else에서 트리로 옮긴다 — 로딩은<Suspense>, 에러는<ErrorBoundary>, 성공은 본문. - 본문에 loading·error를 인자로 넘기지 않는다. 안 넘겨도 가장 가까운 경계가 대신 잡기 때문이다 —
render는 결과 하나만 받는다.
확인: 있을 수 없는 조합을 적을 수 있는가. 적을 수 없으면 된 것이다 — 상태가 로딩이거나, 에러거나, 성공이거나 셋 중 하나가 된다.
함정: 스피너가 너무 빨리 뜨고 사라지는 건 이 축으로 안 고쳐진다 — 다른 축이다 → 573
곁가지 — 인자를 위로 들어올려 바깥이 처리하게 하는 이 모양은 대수적 효과(algebraic effects)의 handler와 같다.1
stale-while-revalidate(두 시계·isStale) 부분은 570으로.
Footnotes
-
try/catch와 비슷하다 — 중간 함수들은 몰라도 되고 가장 가까운 handler가 받는다. 다른 점은 처리하고 끝나지 않는다는 것이다. handler가 값을 돌려주면 효과를 일으킨 자리로 되돌아가 실행이 이어진다. (Algebraic Effects for the Rest of Us) ↩
useMemo 캐시 적중 진단 — 매 렌더 새로 만들어지는 deps
useMemo가 실제로 재사용되고 있는지 계산 횟수로 확인한다.
언제 — 무거운 계산을 useMemo로 감쌌는데 렌더 시간이 줄지 않을 때.
GenUI 도입 — 모델이 정하는 게 인자인가 배치인가 코드인가
Static/Declarative/Open-ended 판별부터 카탈로그 설계·신뢰 경계·회귀 전략까지.
언제 — 누군가 "우리도 GenUI 해보자"고 말했을 때. 회의에서 그 단어가 나오면 셋 중 뭔지부터 확정한다.
프로그래밍 원칙을 조작 가능하게 — 증상·절차·경계로 다시 쓰기
선언형 슬로건("파생하라", "추상을 미뤄라")을 언제 적용하고 언제 적용하지 말지의 절차로 바꾼 일곱 레시피.
언제 — 원칙 목록이 "무엇을 믿어라"는 주는데 "언제 어떻게 하라"를 안 줄 때. 원칙마다 증상 → 절차 → 경계(언제 적용하지 말 것) 를 붙인다. 경계가 핵심이다 — 슬로건은 자기가 틀리는 지점을 말하지 않는다.
React 컴포넌트 안정성 규칙 열셋 — 앱 컴포넌트와 라이브러리 컴포넌트
tearing·FOUC·hydration 부터 Activity·ViewTransition 까지, 각 규칙이 언제 필요한지 적용 범위와 함께.
언제 — 컴포넌트에 이 규칙을 적용할지 정할 때. 먼저 이게 앱 컴포넌트인지 라이브러리 컴포넌트인지 가른다. 앱 컴포넌트는 자기가 어디서 쓰이는지 알기 때문에 절반은 해당이 없다 — 전부 예방적으로 적용하면 그게 새로운 over-engineering 이다.
네이버 지도 마커 + fitBounds
좌표를 데이터에서 계산하고, 0·1·N 카메라를 가르고, 줌 오버슈트·여백·마커 생명주기를 처리하는 절차.
언제 — naver.maps v3 에 마커를 여러 개 찍고 카메라를 자동으로 맞출 때. 증상이 있으면 바로 간다.