TypeScript 중첩 객체 타입 부분 수정
interface O {
actions: { a: string; b: number }
}
// 중첩 속성 Optional로 변경
type MakeNestedOptional<T, K extends keyof T, OK extends keyof T[K]> = Omit<
T,
K
> & {
[P in K]: Omit<T[K], OK> & Partial<Pick<T[K], OK>>
}
type Result = MakeNestedOptional<O, 'actions', 'b'> // { actions: { a: string; b?: number } }
// 중첩 속성 타입 오버라이드
type OverrideNested<T, K extends keyof T, Override> = Omit<T, K> & {
[P in K]: Omit<T[K], keyof Override> & Override
}
type Result2 = OverrideNested<O, 'actions', { b: boolean }> // { actions: { a: string; b: boolean } }
한두 군데만 쓸 거면 그냥 손으로 타입 작성이 더 명확함. 유틸리티 타입은 반복 사용할 때만 가치.
// 라이브러리 객체 변이 주의
// ❌ info.actions.onDownload = undefined
// ✅ const modifiedInfo = { ...info, actions: { ...info.actions, onDownload: undefined } }
증기 계란 조리기: 계란이 많을수록 물을 적게 넣는 이유
계란 1개 (완숙): 117ml
계란 6개 (완숙): 96ml
증기 조리 방식은 물의 양 = 조리 시간. 계란이 많으면:
- 서로 열을 공유/유지 (“집단 난방” 효과)
- 증기가 좁은 공간에 집중되어 효율 ↑
- 열 손실 표면적이 상대적으로 ↓
물에 삶는 것과 달리, 증기 조리는 배치 밀도에 따라 열 효율이 크게 달라짐.
styled-components에서 &-header 같은 BEM 스타일 자식 선택자를 HTML에서 참조하는 방법? 없다.
// ❌ 해시된 클래스명과 매칭 안됨
const Container = styled.div`
&-header { color: red; }
`
// ✅ 방법 1: 일반 클래스 선택자
const Container = styled.div`
.header { color: red; }
`
<Container><div className="header">Header</div></Container>
// ✅ 방법 2: Styled 컴포넌트 변수로 선언 (추천)
const Header = styled.div`color: red;`
const Container = styled.div`
${Header} { margin-bottom: 20px; }
`
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 19 Concurrent 훅 - useTransition, useOptimistic vs React Query
useTransition
const [isPending, startTransition] = useTransition()
function handleFilter(value: string) {
startTransition(async () => {
const data = await fetchData(value)
setResults(data)
})
}
startTransition으로 감싼 state 업데이트는 **비긴급(non-urgent)**으로 처리- 급한 업데이트(타이핑, 클릭 피드백)를 먼저 처리하고, transition 작업은 뒤로 미룸
isPending으로 로딩 상태 확인, 기존 UI 유지하면서 백그라운드에서 새 UI 준비
useOptimistic
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(current, newItem) => [...current, newItem]
)
async function handleAdd(item: Item) {
addOptimistic(item) // 즉시 UI 반영
await saveToServer(item) // 실패하면 자동 rollback
}
- 서버 응답 전에 UI 먼저 업데이트, 실패 시 자동 복구
- 좋아요 버튼, 장바구니 추가 같은 인터랙션에 적합
현실: Query가 이미 너무 편함
const { data, isPending } = useQuery({
queryKey: ['items', filter],
queryFn: () => fetchItems(filter),
})
useMutation({
mutationFn: addItem,
onMutate: async (newItem) => {
const previous = queryClient.getQueryData(['items'])
queryClient.setQueryData(['items'], (old) => [...old, newItem])
return { previous }
},
onError: (err, _, context) => {
queryClient.setQueryData(['items'], context.previous)
},
})
캐싱, 리페치, devtools, stale-while-revalidate까지 한 방에 해결. 팀에서 이미 쓰고 있으면 “굳이?” 됨.
useTransition이 의미 있는 지점 - 무거운 클라이언트 연산, Next.js Server Actions 조합
<form action={(formData) => {
startTransition(async () => {
await createItem(formData)
router.refresh()
})
}}>
Serverless 환경에서 SQLite 사용 불가 → Turso 도입
Vercel 같은 플랫폼에서 SQLite 공식 미지원. 읽기 전용 데이터 파일도 최근 환경에서 에러 발생.
Turso - LibSQL 기반 SQLite-compatible serverless DB. 기존 쿼리 그대로 사용 가능.
JSON 기반 대안:
llms.txt는 웹사이트나 애플리케이션이 자신이 사용하는 LLM(대규모 언어 모델) 및 관련 설정에 대해 명시적으로 문서화할 수 있는 포맷이다.
아래 링크들은 llms.txt 포맷이 실제로 어떻게 사용되고 있는지 참고한 자료들. 각 사이트는 자신들의 문서를 llms.txt에 구조적으로 명시하고 있다.
const isVisible = document.visibilityState === 'visible'
const isHidden = document.visibilityState === 'hidden'
document.addEventListener('visibilitychange', onChange)
/**
* `visibilitychange` 이벤트는 정상적인 탭 전환 시에는 잘 작동하지만, 시스템 슬립, 화면 잠금, 또는 브라우저가 백그라운드에서 복귀할 때는 누락될 수 있다.
* 그래서 수동으로 처리되는 부분이 필요
*/
document.addEventListener('mousemove', setVisible)
document.addEventListener('keydown', setVisible)
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
const onChange = () => {
const newState = document.visibilityState !== 'hidden'
if (newState !== isVisible) {
setIsVisible(newState)
}
}
// addEventListener
return () => {
// removeEventListener
}
}, [isVisible])
type Task = (callback: (result: string) => void) => void
type TasksCallback = (results: string[]) => void
class TaskRunner {
protected tasks: Task[] = []
protected results: Array<string> = []
addTask(task: Task) {
this.tasks.push(task)
}
}
class ParallelTaskRunner extends TaskRunner {
run(callback: TasksCallback) {
const totalTasks = this.tasks.length
this.tasks.forEach((task) => {
task((result) => {
this.results.push(result)
if (this.results.size === totalTasks) {
callback([...this.results])
}
})
})
}
}
class SerialTaskRunner extends TaskRunner {
index = 0
run(callback: TasksCallback) {
const executeTask = () => {
if (this.index >= this.tasks.length) {
callback([...this.results])
return
}
this.tasks[this.index]((result) => {
this.results.push(result)
this.index++
executeTask()
})
}
executeTask()
}
}