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

대용량 리스트에서 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

Tumblr 테마 Vue → Web Components 마이그레이션

NPF 데이터는 <script type="application/json" data-npf>에 저장. 컴포넌트에서 closest로 포스트 컨테이너 찾아서 참조 (React Context 패턴과 유사).

<div id="{PostID}" data-type="{PostType}">
  {block:Text}
  <script type="application/json" data-npf>
    {NPF}
  </script>
  <tumblr-npf-media></tumblr-npf-media>
  <tumblr-npf-text></tumblr-npf-text>
  {/block:Text}
</div>
// 데이터 참조
const post = this.closest('[id][data-type]')
const npf = JSON.parse(post.querySelector('script[data-npf]').textContent)

// 렌더링 - createElement 사용 (Tumblr 템플릿 ${} 충돌 회피)
const img = document.createElement('img')
img.src = imageURL
this.appendChild(img)

제약: {NPF}는 Text 포스트에서만 사용 가능. Photo는 기존 Tumblr 변수 사용.

#507

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

Jest setupFiles vs setupFilesAfterEnv - 실행 시점이 다르다.

  • setupFiles: 테스트 프레임워크 설치 . Jest 전역 객체 없음. 환경 변수, 폴리필용.
  • setupFilesAfterEnv: 테스트 프레임워크 설치 . jest.setTimeout(), 커스텀 matcher, 전역 beforeEach용.

process.env는 setupFiles에서. 모듈이 import 시점에 환경 변수를 읽기 때문에 setupFilesAfterEnv에서 설정하면 이미 늦다.

// jest.config.js
{ setupFiles: ['./jest.env.js'], setupFilesAfterEnv: ['./jest.setup.js'] }

// jest.env.js - 환경 변수
process.env.API_URL = 'http://test-api.example.com'

// jest.setup.js - Jest API 활용
import '@testing-library/jest-dom'
beforeEach(() => { jest.clearAllMocks() })
#505

yalc - 로컬 Node 모듈을 다른 프로젝트에서 바로 테스트. npm link보다 문제 적음 (파일 복사 방식).

npm i -g yalc

# 패키지에서
yalc publish          # ~/.yalc에 저장
yalc push             # 연결된 모든 프로젝트에 반영
yalc publish --push   # 둘 다

# 앱에서
yalc add my-module
yalc remove my-module && npm install  # 정리

watch 모드: "dev": "tsup src/index.ts --watch --onSuccess 'yalc push'"

.gitignore: .yalc, yalc.lock

#503

tsconfig 핵심: target, lib, module

TypeScript (.ts)

lib: 타입 체크 시 뭘 알고 있나? (Promise, Map 등)

target: 문법을 얼마나 낮출 건가? (ES2020 → ?. 그대로, ES2019 → 삼항연산자로)

module: import/export를 뭘로? (CommonJS → require, ESNext → import)

JavaScript (.js)

target vs lib 분리 이유: 문법(syntax)과 API(runtime)는 다름

  • ?. → target이 변환 (문법)
  • Promise → 폴리필이 해결 (API)
{
  "target": "ES2019", // 하위호환 (optional chaining 이전)
  "module": "ESNext", // 트리쉐이킹 가능
  "lib": ["ES2020", "DOM"], // 타입은 넉넉하게
  "moduleResolution": "Node"
}

TS 버전 올리면서 target 그대로 두면 Webpack4 같은 구형 번들러에서 파싱 실패할 수 있음.

#501

MDIR 스타일 인터페이스 개발 - SQLite DB를 탐색하는 도구

  1. Neovim Telescope 순수 Neovim 솔루션. :StoryBrowse, :StorySearch
  2. VS Code 확장 Activity Bar + Tree View + Quick Pick
  3. Bun Single-file Executable --compile로 DB + 웹서버 + 프론트엔드 단일 파일 (~100MB)
#500

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 } }
#499

증기 계란 조리기: 계란이 많을수록 물을 적게 넣는 이유

계란 1개 (완숙): 117ml
계란 6개 (완숙): 96ml

증기 조리 방식은 물의 양 = 조리 시간. 계란이 많으면:

  • 서로 열을 공유/유지 (“집단 난방” 효과)
  • 증기가 좁은 공간에 집중되어 효율 ↑
  • 열 손실 표면적이 상대적으로 ↓

물에 삶는 것과 달리, 증기 조리는 배치 밀도에 따라 열 효율이 크게 달라짐.

#498
30 중 7페이지