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>

#496

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()
  })
}}>
#495

Serverless 환경에서 SQLite 사용 불가 → Turso 도입

Vercel 같은 플랫폼에서 SQLite 공식 미지원. 읽기 전용 데이터 파일도 최근 환경에서 에러 발생.

TIP

Turso - LibSQL 기반 SQLite-compatible serverless DB. 기존 쿼리 그대로 사용 가능.

JSON 기반 대안:

  • AlaSQL - SQL 문법으로 JSON 쿼리
  • JSONata - 선언적 JSON 질의
#492

llms.txt는 웹사이트나 애플리케이션이 자신이 사용하는 LLM(대규모 언어 모델) 및 관련 설정에 대해 명시적으로 문서화할 수 있는 포맷이다.

아래 링크들은 llms.txt 포맷이 실제로 어떻게 사용되고 있는지 참고한 자료들. 각 사이트는 자신들의 문서를 llms.txt에 구조적으로 명시하고 있다.


#491
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])

#490
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

  1. concurrently

#485

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)
      }}
    />
  )
}
#480
export function parseNativeEmoji(unified: string): string {
  return unified
    .split('-')
    .map((hex) => String.fromCodePoint(parseInt(hex, 16)))
    .join('')
}
  • unified 문자열을 -로 분리하여 개별 유니코드 코드 포인트를 얻는다.
  • 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.
  • 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.

#477
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 테스트 및 디폴트 값 설정에 활용될 수 있다.

#461
25 중 5페이지