ARIA 속성 적용 패턴
// region + labelledby
<div role="region" aria-labelledby={titleId}>
<h2 id={titleId}>제목</h2>
</div>
// list + listitem
<div role="list" aria-labelledby="list-title">
<div role="listitem" aria-labelledby={`${label}-label ${label}-value`}>
<span id={`${label}-label`}>{label}</span>
<span id={`${label}-value`}>{value}</span>
</div>
</div>
aria-labelledby- 연결된 요소의 텍스트를 레이블로aria-describedby- 추가 설명이 필요할 때- 중복 제거: aria-label과 화면 텍스트 중복 시 aria-labelledby 사용
전화번호 포맷팅
function formatPhoneNumber(phoneNumber) {
const cleaned = phoneNumber.replace(/\D/g, '')
if (cleaned.length === 11) {
return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 10 && cleaned.startsWith('02')) {
return cleaned.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 10) {
return cleaned.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3')
} else if (cleaned.length === 9 && cleaned.startsWith('02')) {
return cleaned.replace(/(\d{2})(\d{3})(\d{3})/, '$1-$2-$3')
}
return 'Invalid phone number'
}
formatPhoneNumber('01012341234') // 010-1234-1234
formatPhoneNumber('021234567') // 02-123-4567 Map으로 localStorage 래핑
class LocalStorageMap {
constructor(storageKey) {
this.storageKey = storageKey
this.map = this.loadFromStorage()
}
loadFromStorage() {
const data = localStorage.getItem(this.storageKey)
return data ? new Map(JSON.parse(data)) : new Map()
}
saveToStorage() {
localStorage.setItem(this.storageKey, JSON.stringify([...this.map]))
}
set(key, value) {
this.map.set(key, value)
this.saveToStorage()
}
get(key) {
return this.map.get(key)
}
delete(key) {
const result = this.map.delete(key)
this.saveToStorage()
return result
}
clear() {
this.map.clear()
localStorage.removeItem(this.storageKey)
}
}
CAUTION
- localStorage는 문자열만 저장 → JSON 직렬화 필수
- 용량 제한 5~10MB 주의
const color =
(isUnchecked && 'palette.label.alternative') ||
(isChecked && 'palette.primary.normal') ||
'palette.label.normal'
중첩 삼항/논리 연산자는 간결하지만 상태가 늘어나면 유지보수 어려움.
핵심: status 모델을 어떻게 정의하고 참조할 것인가?
class StatusManager {
static Status = {
UNCHECKED: 'UNCHECKED',
CHECKED: 'CHECKED',
DEFAULT: 'DEFAULT',
} as const
static getColor(status: keyof typeof this.Status) {
switch (status) {
case this.Status.UNCHECKED:
return 'palette.label.alternative'
case this.Status.CHECKED:
return 'palette.primary.normal'
default:
return 'palette.label.normal'
}
}
}
const color = StatusManager.getColor(StatusManager.Status.CHECKED)
변수의 prefix는 역할과 의미를 명확히 하기 위해 사용된다.
/**
* 초기값을 나타내며, 상태나 값이 처음 설정될 때 사용된다.
* 적합한 상황: 프로그램이 시작되거나 객체가 처음 생성될 때의 값을 정의한다.
*/
const initialCount = 0 // 카운터의 초기값
const initialState = { loggedIn: false, user: null } // 초기 상태
const initialPosition = { x: 0, y: 0 } // 초기 좌표
/**
* 기본값으로 널리 사용되는 표준 값이다.
* 적합한 상황: 값이 없을 경우 사용할 기본값을 정의한다.
*/
const defaultTheme = 'light' // 기본 테마
const defaultUser = { name: 'Guest', role: 'viewer' } // 기본 사용자
const defaultPageSize = 20 // 페이지당 기본 항목 수
/**
* 기준값 또는 다른 값의 참조점이 되는 값이다.
* 적합한 상황: 값을 계산하거나 파생할 때 기준이 되는 값을 정의한다.
*/
const baseSalary = 3000 // 기준 급여
const baseUrl = 'https://api.example.com' // API의 기준 URL
const baseColor = '#FFFFFF' // 기준 색상
/**
* 변경되기 전 원래 상태 또는 초기 상태를 강조한다.
* 적합한 상황: 데이터를 변경하기 전에 원래 값을 유지해야 할 때 사용한다.
*/
const originalText = 'Hello World' // 변경 전 텍스트
const originalSettings = { theme: 'dark', notifications: true } // 원래 설정값
const originalImage = image.clone() // 원본 이미지 복사
initial*vsdefault*- initial은 초기 상태, 재설정 가능성 적음. default는 기본값, 교체 가능.base*vsoriginal*- base는 기준값(비교/계산용). original은 원래 상태 보존/복구용.
💡이 패턴이 “상태 머신”처럼 들린다면, 그리 놀랄 일도 아닙니다. 결국, 선택의 문제는 상태 머신을 구축할지 말지가 아니라, 그것을 암시적으로 구축할지 명시적으로 구축할지에 달려 있습니다.
stateDiagram-v2
[*] --> Mounting
Mounting --> AwaitingEmailInput
Mounting --> AwaitingCodeInput
AwaitingEmailInput --> SubmittingEmail
SubmittingEmail --> AwaitingCodeInput
SubmittingEmail --> AwaitingEmailInput
AwaitingCodeInput --> SubmittingCode
SubmittingCode --> Success
SubmittingCode --> AwaitingCodeInput
Success --> [*]
💡Blob URL 방식은 Firefox 58+에서 CSP 제한이 있는 페이지에서도 스크립트를 주입할 수 있는 방법입니다. 이 방식을 사용할 때, 비동기성 때문에 발생할 수 있는 문제를 방지하려면 초기화 코드가 준비된 후 스크립트를 전달해야 합니다.
const b = new Blob([script], { type: 'text/javascript' })
const u = URL.createObjectURL(b)
const s = document.createElement('script')
s.src = u
document.body.appendChild(s)
document.body.removeChild(s)
URL.revokeObjectURL(u) vitest에서 사용자 지정 assertion을 추가하여 Zod 스키마와 Response 객체를 비교하기
import { expect } from 'vitest'
import type { ZodTypeAny } from 'zod'
expect.extend({
/**
* @param received 테스트할 Response 객체
* @param schema 검증할 Zod 스키마
*/
async toMatchSchema(received: Response, schema: ZodTypeAny) {
const response = await received.json()
const result = await schema.safeParseAsync(response)
return {
message: () => '',
pass: result.success,
} satisfies ExpectationResult
},
})
vitest.d.ts 파일에서 CustomMatchers 인터페이스를 확장하여 TypeScript와의 통합성을 유지.
import type { ZodTypeAny } from 'zod'
interface CustomMatchers<R = unknown> {
toMatchSchema(schema: ZodTypeAny): Promise<R>
}
todoResponse의 응답 데이터가 todoSchema에 정의된 Zod 스키마와 일치하는지 확인한다.
test('todo', async () => {
expect(todoResponse.ok).toBeTruthy()
expect(todoResponse).toMatchSchema(todoSchema)
})
button 또는 a 요소를 선택적으로 렌더링할 수 있는 ButtonOrLink 컴포넌트를 구현한 부분. props로 전달된 as 값에 따라 해당 태그를 렌더링하며, React.JSX.IntrinsicElements의 타입을 활용하여 각 태그에 맞는 props를 받을 수 있도록 한다.
import * as React from 'react'
type ComponentPropsWithAs<T extends keyof React.JSX.IntrinsicElements> = {
as: T
} & React.ComponentProps<T>
type ButtonOrLinkProps =
| ComponentPropsWithAs<'a'>
| ComponentPropsWithAs<'button'>
export function ButtonOrLink(props: ButtonOrLinkProps) {
switch (props.as) {
case 'a':
return <a {...props} />
case 'button':
return <button {...props} />
default:
return null
}
} vitest와 jest에서 가짜 타이머를 사용하는 방법. 내부적으로는 @sinonjs/fake-timers를 사용. vitest와 jest 모두 시스템 시간을 조작할 수 있도록 하여 테스트가 일관되게 실행되도록 하고 다양한 시간과 날짜를 시뮬레이션하여 다양한 조건에서 코드가 예상대로 작동하는지 확인할 수 있음.
describe('isSameDate', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime('2024-07-20')
})
afterEach(() => {
vi.useRealTimers()
})
it('현재 날짜와 동일한 날짜를 전달하면 true를 반환한다', () => {
expect(isSameDate('2024-07-20')).toBe(true)
})
it('현재 날짜와 다른 날짜를 전달하면 false를 반환한다', () => {
expect(isSameDate('2024-07-19')).toBe(false)
expect(isSameDate('2024-07-21')).toBe(false)
})
})