TypeScript Discriminated Union 문서화 딜레마

// 개발: 타입 안전, IDE 자동완성, 명확한 의도
type Button =
  | { type: 'submit'; color: string }
  | { type: 'reset'; text: string }
<!-- 문서화: 조건부 속성 설명 어려움, 테이블 복잡, 예시 다수 필요 -->

| 속성  | 타입                | 조건                    |
| ----- | ------------------- | ----------------------- |
| type  | 'submit' \| 'reset' | 필수                    |
| color | string              | type='submit'일 때 필수 |
| text  | string              | type='reset'일 때 필수  |

  • 자세한 타입 정의는 TypeScript 정의 파일 참고
  • typedoc, api-extractor로 자동 생성
#524

react-spring

  • onRest 애니메이션이 물리적으로 정지했을 때 → UI 상태 업데이트용
  • onResolve Promise가 resolve될 때 → 비동기 플로우 제어용
const [springs, api] = useSpring(() => ({
  x: 0,
  onRest: () => setStatus('stopped'), // UI 상태
  onResolve: () => console.log('done'), // Promise 기반 로직
}))

// Promise 방식도 가능
await api.start({ x: 100 })
#523

npm install 실패 시 플래그 선택

# 1. Node 버전 불일치 (engines 필드)
npm install --ignore-engines

# 2. peer dependencies 충돌 (가장 흔함)
npm install --legacy-peer-deps

# 3. 마지막 수단 (권장 안함)
npm install --force

--legacy-peer-deps가 필요한 상황

{
  "react": "^18.2.0",
  // 라이브러리가 아직 React 18 공식 지원 안 하지만 실제로는 동작함.
  "react-beautiful-dnd": "^13.1.1" // peer: react@^16.8 || ^17
}
# 프로젝트 전체 적용
echo "legacy-peer-deps=true" > .npmrc
#522

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()는 실제 크기 반환.

#521

Node.js에서 윈도우즈 외부 경로 파일 읽기

const path = require('path')
const fs = require('fs').promises

// 슬래시 사용 (Node.js가 자동 변환)
// ('C:/Users/username/Documents/file.txt')

// 또는 백슬래시 이스케이프
// ('C:\\Users\\username\\Documents\\file.txt')

// 권장: path.join 사용 (크로스 플랫폼)
path.join(process.env.USERPROFILE, 'Documents', 'file.txt')

// path.resolve로 절대 경로 생성
path.resolve('../../Documents/file.txt')

사용자 입력으로 경로 받을 때는 path.normalize()로 디렉토리 트래버설 공격 방지.

#520

Date.getDay() - 요일 반환 (0=일요일, 6=토요일)

new Date().getDay() // 0~6
new Date('2024-12-25').getDay() // 3 (수요일)
new Date(2024, 11, 25).getDay() // 월은 0부터 시작

const days = ['일', '월', '화', '수', '목', '금', '토']
days[new Date().getDay()] // 오늘 요일
#519

검색어 필터링 UI → Debounce (300-500ms)

타이핑 완료 후 검색이 더 자연스러움. “자바스크립트” 입력 시 매 글자마다 검색하면 비효율적.

const handleSearch = debounce((term) => performSearch(term), 300)
input.addEventListener('input', (e) => handleSearch(e.target.value))

Throttle은 실시간 검색 결과를 보여주면서 요청 제한할 때 사용 (1초마다 최대 1회 등).

#518

TypeScript 제네릭 기본값 - <T extends Type = DefaultValue>

type SelectProps<T extends 'single' | 'multiple' = 'multiple'> = {
  mode: T
  value: T extends 'single' ? string : string[]
}

const a: SelectProps = { mode: 'multiple', value: ['a', 'b'] } // 기본값 사용
const b: SelectProps<'single'> = { mode: 'single', value: 'a' } // 명시적 지정

// 여러 매개변수에 각각 기본값
type ApiResponse<
  TData = any,
  TStatus extends 'loading' | 'success' | 'error' = 'loading'
> = {
  data: TStatus extends 'success' ? TData : null
  status: TStatus
}

가장 자주 사용되는 케이스를 기본값으로 설정하면 제네릭을 항상 명시하는 번거로움 줄어듦.

#516

Vitest 모킹: vi.mocked() vs vi.hoisted()

vi.mocked()는 타입만 제공, 실제 모킹 구현체는 별도로 필요.

// ❌ mockImplementation이 undefined
const mockUseSize = vi.mocked(useSize)

// ✅ vi.hoisted() 사용 (추천)
const mockUseSize = vi.hoisted(() => vi.fn())

vi.mock('ahooks', () => ({ useSize: mockUseSize }))

beforeEach(() => {
  mockUseSize.mockImplementation(() => ({ width: 100, height: 20 }))
})

DOM 속성 모킹 (scrollWidth/clientWidth):

beforeEach(() => {
  Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
    configurable: true,
    get() {
      return 150
    },
  })
})

afterEach(() => {
  // 원래 속성 복원
})

DOM 측정이 복잡하면 Playwright/Cypress로 통합 테스트 고려.

#515

ResizeObserver Mock (Vitest, ES Module 환경)

vi.hoisted()로 모킹 함수 미리 선언 후 vi.mock()에서 사용:

import { vi } from 'vitest'

const { mockResizeObserver, MockResizeObserver } = vi.hoisted(() => {
  let observers: { callback: ResizeObserverCallback; observer: any }[] = []

  const MockResizeObserver = vi.fn().mockImplementation((callback) => {
    const observer = {
      observe: vi.fn(),
      unobserve: vi.fn(),
      disconnect: vi.fn(),
    }
    observers.push({ callback, observer })
    return observer
  })

  return {
    MockResizeObserver,
    mockResizeObserver: {
      triggerResize: (entries: ResizeObserverEntry[], index = 0) => {
        observers[index]?.callback(entries, observers[index].observer)
      },
      reset: () => {
        observers = []
      },
    },
  }
})

vi.mock('resize-observer-polyfill', () => ({ default: MockResizeObserver }))

// 테스트에서 사용
beforeEach(() => mockResizeObserver.reset())

it('should handle resize', () => {
  const mockEntry = {
    target: document.createElement('div'),
    contentRect: { width: 100 },
  }
  mockResizeObserver.triggerResize([mockEntry])
})

핵심: ES Module에서는 vi.hoisted() 필수. 여러 observer는 배열로 추적.

#514
31 중 7페이지