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)
  })
})

#315

Flaky 테스트 방지 - Date, Math.random 같은 비순수 함수를 props로 주입

  • new Date(), Math.random() 같은 비순수 함수는 매번 다른 결과 → 테스트 불안정
  • 해결: 기본 매개변수로 주입하면 프로덕션 동작 유지 + 테스트에서 제어 가능
function Date({ date = new Date() }) {
  const [date, setDate] = React.useState(date)

  return (
    <input
      type="date"
      onChange={(e) => setDate(e.target.value)}
      defaultValue={date}
    />
  )
}

function Random({ randomizer = Math.random }) {
  const [state, setState] = React.useState(randomizer())

  return <div>{state}</div>
}

테스트:

describe('Date Component', () => {
  it('should update state on date change', () => {
    render(<Date date={new Date('2023-01-01')} />)

    const input = screen.getByRole('textbox')

    expect(input.value).toBe('2023-01-01')
  })
})

describe('Random Component', () => {
  it('should render a random number', () => {
    const mockRandomizer = () => 0.5

    render(<Random randomizer={mockRandomizer} />)

    const div = screen.getByText('0.5')

    expect(div).toBeInTheDocument()
  })
})
#314

대화 상자(Dialog Box)는 사용자에게 정보를 전달하고 응답을 요청하는 그래픽 제어 요소이다. Dialog box

Modal - 대화 상자를 연 소프트웨어와의 상호작용을 차단

  • System Modal - 닫기 전까지 다른 작업 불가. 과거 단일 작업 시스템에서 사용.
  • Application Modal - 프로그램 일시 중단. 워크플로우 방해로 비판받음.
  • Document Modal - 부모 창만 차단. macOS 시트 형태.

Modeless - 소프트웨어의 다른 부분과 상호작용 허용. 대화 상자가 열려 있어도 작업 가능. 툴바가 예시.

고려 사항:

  • 모달의 문제점 - 사용자 흐름 방해. 습관적으로 확인을 누르게 되어 작업 손실 가능.
  • 경고 대신 실행 취소 - 경고는 한계가 있음. 실행 취소(Undo) 기능이 더 효과적.1
  • 경고를 사용하는 대신, 실행 취소를 제공하라

Footnotes

  1. https://alistapart.com/article/neveruseawarning/ ↩

#313

Excel View1 라이브러리를 보던 중, Excel과 Node의 상호작용에 대한 궁금증이 생겼다. 아무래도 웹개발을 하다보니 어플리케이션과 통신할 수 있는 부분에 대해서 전혀 생각을 안했었다는 걸 깨닫고 이러한 부분을 보완하기 위해 Excel과의 통신 방식을 찾아봄.

우선, 위 라이브러리 코드를 통해 ActiveXObject('Excel.Application')2로 Excel 객체를 생성하고, 이를 통해 Excel의 다양한 기능에 접근할 수 있다는 것을 확인했다. 그리고 node-activex의 문서에서 링크를 통해 추가적인 정보들을 확인할 수 있었는데 아무래도 자주 보던 영역이 아니라 일단 확인만 하는 단계에서 멈춤.

이 접근을 통해, Excel과 상호작용하는 방법에 대해 실마리를 찾을 수 있었음. 그러나 모든 과정이 순조롭지만은 않은게. ActiveXObject를 사용하는 부분에서 개념적 이해가 부족했고, 그밖에 Excel의 객체모델에 대해서도 배경지식이 많이 부족하다는 걸 알게됨.

이번 경험을 통해, 웹 개발자가 어플리케이션 레벨의 개념들도 이해하면 좋겠다는 생각을 하게 됨. 나중에 기회가 되면 살펴보고 일단 view 기능을 스프레드시트로 구현해 봐야겠다.


Footnotes

  1. 활성 셀의 행 전체를 별도 창에 띄워서 좌우 스크롤 없이 보게 해주는 도구 ↩

  2. https://github.com/timepp/excelview/blob/master/excel.js#L70 ↩

#312

Avoiding premature abstraction with Unstyled React Components (buildui.com)

React 컴포넌트를 작성할 때, 불필요한 추상화를 피하고 컴포넌트의 유연성을 유지하는 방법. 특히 스타일이 없는 컴포넌트를 통해 어떻게 컴포넌트의 기능에 집중할 수 있는지를 설명.

import React, { ComponentProps, FC, ReactNode } from 'react'

function Spinner() {
  return (
    <span className="absolute inset-0 flex items-center justify-center">
      `<Spinner />`
    </span>
  )
}

type LabelProps = ComponentProps<'span'>

function Label({ children, ...rest }: LabelProps) {
  return <span {...rest}>{children}</span>
}

type LoadingButtonProps = ComponentProps<'button'> & {
  children: FC | ReactNode
}

/**
 * @description 버튼이 비활성화되었을 때 로딩 스피너를 표시할 수 있는 버튼 컴포넌트입니다. 이 컴포넌트는 children 속성으로 함수나 React 노드를 받을 수 있으며, 버튼이 비활성화되면 스피너가 표시되고, 텍스트는 보이지 않게 됩니다.
 */
function LoadingButton({
  disabled,
  className,
  children,
  ...rest
}: LoadingButtonProps) {
  return (
    <button {...rest} className={`${className} relative`} disabled={disabled}>
      {typeof children === 'function' ? (
        children({})
      ) : (
        <>
          {disabled && <Spinner />}
          <Label className={disabled ? 'invisible' : ''}>{children}</Label>
        </>
      )}
    </button>
  )
}

LoadingButton.Spinner = Spinner
LoadingButton.Label = Label

이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다.

#311

React Router의 Link 컴포넌트 테스트하기

React 애플리케이션에서 라우팅은 중요한 부분. 특히, 사용자가 링크를 클릭할 때 적절한 페이지로 이동하는지 테스트하는 것은 중요. 다음은 React Router의 <Link /> 컴포넌트를 테스트하는 코드.

describe('Link', () => {
  it('이동', () => {
    const Home = () => <Link to="/about">About</Link>
    const About = () => <h1>About</h1>

    const { getByRole } = render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </MemoryRouter>
    )

    fireEvent.click(getByRole('link'))

    expect(screen.getByRole('heading')).toHaveTextContent('About')
  })
})

가끔 <Link />도 테스트해야 할 때가 있다. 위 코드는 사용자가 “About” 링크를 클릭하면 “About” 페이지로 이동하는지 테스트. fireEvent.click을 사용해 링크 클릭 이벤트를 발생시키고, screen.getByRole을 통해 헤딩 요소의 텍스트가 ’About’인지 확인. 이 테스트를 통해 라우팅이 제대로 동작하는지 확인할 수 있음.


#310

최근에 Enzyme을 제거하고 React Testing Library로 교체하는 작업을 진행했습니다. JSX 영역은 별 문제 없이 진행되었으나, state나 props를 다루는 구현 부분에서 약간의 애매함이 있었습니다. 개인적으로는 기존 접근 방식이 나쁘지 않았다고 생각했기 때문에, 새로운 접근 방식으로 전환하는 데 주저하게 되었습니다.

결론적으로, state와 props의 JSON 결과물을 렌더링하고 getByTestId를 사용하여 이를 참조하는 방식으로 테스트를 진행하기로 했습니다. 다음은 그 예시입니다:

render() {
  return <>
    {process.env.NODE_ENV === 'test' && (
      <pre data-testid="ThumbnailDebug">
        {JSON.stringify({
          props: this.props,
          state: this.state,
        })}
      </pre>
    )}
  </>
}

이 방법을 통해 Enzyme을 제거한다는 목표를 달성했기 때문에, 어느 정도는 해결된 것처럼 보이며, 추후 더 나은 테스트 코드를 작성하기 위한 고민을 할 수 있을 것 같습니다.


#309

Next.js Route Loader & Mini CSS Extract Plugin - prefetch와 CSS chunk 에러 처리

prefetchViaDom - route-loader.ts

function prefetchViaDom(
  href: string,
  as: string,
  link?: HTMLLinkElement
): Promise<any> {
  return new Promise<void>((resolve, reject) => {
    const selector = `
      link[rel="prefetch"][href^="${href}"],
      link[rel="preload"][href^="${href}"],
      script[src^="${href}"]`
    if (document.querySelector(selector)) {
      return resolve()
    }

    link = document.createElement('link')

    // The order of property assignment here is intentional:
    if (as) link!.as = as
    link!.rel = `prefetch`
    link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
    link!.onload = resolve as any
    link!.onerror = () =>
      reject(markAssetError(new Error(`Failed to prefetch: ${href}`)))

    // `href` should always be last:
    link!.href = href

    document.head.appendChild(link)
  })
}
  • 이미 prefetch/preload된 리소스면 스킵
  • <link rel="prefetch"> 생성 후 head에 추가
  • href는 항상 마지막에 설정 (브라우저 요청 타이밍 제어)

CSS Chunk Load Error - Mini CSS Extract Plugin

 Template.indent([
  "var errorType = event && event.type;",
  "var realHref = event && event.target && event.target.href || fullhref;",
  'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + errorType + ": " + realHref + ")");',
  'err.name = "ChunkLoadError";',
  // TODO remove `code` in the future major release to align with webpack
  'err.code = "CSS_CHUNK_LOAD_FAILED";',
  "err.type = errorType;",
  "err.request = realHref;",
  "if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag)",
  "reject(err);",
]),
  • ChunkLoadError 커스텀 에러 생성
  • 실패한 <link> 태그 DOM에서 제거 후 reject
#306

Google Sheets API를 쓰려면 GCP 설정을 먼저 하고 google-spreadsheet로 조작한다.

순서를 기억하려고 적어둔 것이다. 각 단계의 방법이 아니라 밟는 차례가 내용이다.

  1. 서비스 계정 설정
  2. Google Cloud 프로젝트 생성
  3. Google Sheets API 활성화
  4. API 자격 증명 생성
  5. 서비스 계정 생성
  6. 서비스 계정 키 생성
  7. 환경 변수 설정

#305
30 중 15페이지