React Children 재귀 순회
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)
}
})
}
대안 - 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 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 공식 미지원. 읽기 전용 데이터 파일도 최근 환경에서 에러 발생.
TIP
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()
}
}
Footnotes
React 제네릭 컴포넌트 패턴 - <T extends Record<string, any>>로 row/item 타입 추론
import * as React from 'react'
type Props<TRow> = {
rows: TRow[]
renderRow: (row: TRow, index: number) => React.ReactNode
}
const Table = <TRow extends Record<string, any>>({
rows,
renderRow,
}: Props<TRow>) => {
return (
<table>
<tbody>{rows.map((row, index) => renderRow(row, index))}</tbody>
</table>
)
}
function App() {
return (
<Table
rows={[{ name: 'lee' }]}
renderRow={(row, index) => (
<tr key={index}>
<td>{row.name}</td>
</tr>
)}
/>
)
}
import * as React from 'react'
import { UseComboboxProps, useCombobox } from 'downshift'
function Combobox<T extends Record<string, any>>(
props: UseComboboxProps<T> & {
renderItem: (item: T) => React.ReactNode
}
) {
const combobox = useCombobox(props)
return (
<div>
<input
placeholder="구성원을 검색해주세요."
{...combobox.getInputProps()}
/>
<div {...combobox.getMenuProps()}>
{combobox.isOpen &&
props.items.map((item, index) => (
<div key={item.id} {...combobox.getItemProps({ item, index })}>
{props.renderItem(item)}
</div>
))}
</div>
</div>
)
}
function App() {
return (
<Combobox
items={[{ id: 1, name: 'eunsoo' }]}
itemToString={(item) => `${item?.id}`}
renderItem={(item) => {
return <div>{item.name}</div>
}}
onInputValueChange={({ inputValue }) => {
console.log(inputValue)
}}
/>
)
} export function parseNativeEmoji(unified: string): string {
return unified
.split('-')
.map((hex) => String.fromCodePoint(parseInt(hex, 16)))
.join('')
}
unified문자열을-로 분리하여 개별 유니코드 코드 포인트를 얻는다.- 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.
- 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.
const emptyCsvFile = new File([''], 'default.csv', { type: 'text/csv' })
const emptyPngFile = new File([''], 'default.png', { type: 'image/png' })
formData.append('product_coupons', emptyCsvFile)
formData.append('product_imgs', emptyPngFile)
console.assert(emptyCsvFile.size === 0)
빈 파일을 FormData에 첨부하여 전송하는 방법
File객체를 사용하여 빈 파일을 생성할 수 있다.- 파일의
MIME타입을 지정하여 타입에 맞는 빈 파일을 만들 수 있다. - 생성된 빈 파일을
FormData객체에 추가하여 전송할 수 있다.
이는 서버에 빈 형태의 파일을 전송할 때 유용하며, API 테스트 및 디폴트 값 설정에 활용될 수 있다.