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()
})
}) 대화 상자(Dialog Box)는 사용자에게 정보를 전달하고 응답을 요청하는 그래픽 제어 요소이다. Dialog box
Modal - 대화 상자를 연 소프트웨어와의 상호작용을 차단
- System Modal - 닫기 전까지 다른 작업 불가. 과거 단일 작업 시스템에서 사용.
- Application Modal - 프로그램 일시 중단. 워크플로우 방해로 비판받음.
- Document Modal - 부모 창만 차단. macOS 시트 형태.
Modeless - 소프트웨어의 다른 부분과 상호작용 허용. 대화 상자가 열려 있어도 작업 가능. 툴바가 예시.
고려 사항:
- 모달의 문제점 - 사용자 흐름 방해. 습관적으로 확인을 누르게 되어 작업 손실 가능.
- 경고 대신 실행 취소 - 경고는 한계가 있음. 실행 취소(Undo) 기능이 더 효과적.1
- 경고를 사용하는 대신, 실행 취소를 제공하라
Footnotes
Excel View1 라이브러리를 보던 중, Excel과 Node의 상호작용에 대한 궁금증이 생겼다. 아무래도 웹개발을 하다보니 어플리케이션과 통신할 수 있는 부분에 대해서 전혀 생각을 안했었다는 걸 깨닫고 이러한 부분을 보완하기 위해 Excel과의 통신 방식을 찾아봄.
우선, 위 라이브러리 코드를 통해 ActiveXObject('Excel.Application')2로 Excel 객체를 생성하고, 이를 통해 Excel의 다양한 기능에 접근할 수 있다는 것을 확인했다. 그리고 node-activex의 문서에서 링크를 통해 추가적인 정보들을 확인할 수 있었는데 아무래도 자주 보던 영역이 아니라 일단 확인만 하는 단계에서 멈춤.
이 접근을 통해, Excel과 상호작용하는 방법에 대해 실마리를 찾을 수 있었음. 그러나 모든 과정이 순조롭지만은 않은게. ActiveXObject를 사용하는 부분에서 개념적 이해가 부족했고, 그밖에 Excel의 객체모델에 대해서도 배경지식이 많이 부족하다는 걸 알게됨.
이번 경험을 통해, 웹 개발자가 어플리케이션 레벨의 개념들도 이해하면 좋겠다는 생각을 하게 됨. 나중에 기회가 되면 살펴보고 일단 view 기능을 스프레드시트로 구현해 봐야겠다.
Footnotes
-
Excel에서 긴 줄을 탐색하는 데 도움이 되는 도구입니다. 활성 셀을 추적하고 전체 행 데이터를 자동으로 가져와 별도의 창에 표시합니다. 이를 통해 좌우로 스크롤하지 않고도 행의 모든 열을 쉽게 볼 수 있습니다. ↩
-
https://github.com/timepp/excelview/blob/master/excel.js#L70 ↩
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
이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다.
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’인지 확인. 이 테스트를 통해 라우팅이 제대로 동작하는지 확인할 수 있음.
최근에 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을 제거한다는 목표를 달성했기 때문에, 어느 정도는 해결된 것처럼 보이며, 추후 더 나은 테스트 코드를 작성하기 위한 고민을 할 수 있을 것 같습니다.
if (!data) {
throw fetch()
}
- 컴포넌트가 렌더링될 때, 비동기 작업(예: 데이터 패칭)이 시작됩니다. 이 작업은 일반적으로 promise를 반환합니다.
- 비동기 작업이 완료되지 않은 경우, 컴포넌트는 promise를 던집니다. 이는 JavaScript에서 예외를 던지는 것과 유사합니다. Suspense는 promise가 던져질 때 이를 캐치하고 fallback UI를 표시하는 역할을 합니다.
- React는 컴포넌트가 promise를 던졌을 때 이를 감지하고, Suspense 컴포넌트에서 이를 “캐치”합니다. Suspense는 이 promise가 해결될 때까지 대체 UI (fallback)를 렌더링합니다. Concurrent Mode에서는 React가 이 promise를 추적하고, 비동기 작업이 완료될 때까지 렌더링을 중단합니다.
- Promise가 해결되면(즉, 비동기 작업이 완료되면) React는 컴포넌트를 다시 렌더링합니다. Suspense는 현재 데이터 패칭 라이브러리(예: React Query, SWR)와 함께 사용되어 비동기 작업의 상태를 쉽게 관리할 수 있도록 도와줍니다.
- https://github.com/facebook/react/blob/main/packages/react/src/ReactLazy.js#L119C19-L119C26
- https://github.com/TanStack/query/blob/main/packages/react-query/src/suspense.ts#L62
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/throw
- https://jser.pro/ddir/rie?reactVersion=18.3.1&codeKey=ud62nsxll29yy0dzba8
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
Google Sheets API를 활용하기 위해서는 Google Cloud Platform(GCP)에서 필요한 설정을 하고, google-spreadsheet 라이브러리를 통해 스프레드시트를 조작할 수 있습니다. 다음은 이를 위한 단계별 가이드입니다.
- 서비스 계정 설정
- Google Cloud 프로젝트 생성
- Google Sheets API 활성화
- API 자격 증명 생성
- 서비스 계정 생성
- 서비스 계정 키 생성
- 환경 변수 설정
자연어 날짜 처리와 접근성 높은 데이트피커 - chrono와 inclusive-dates
chrono는 다양한 형식의 날짜/시간을 처리하고 주어진 텍스트에서 정보를 추출할 수 있도록 설계된 자연어 날짜 파서.
- “Today”, “Tomorrow”, “Yesterday”, “Last Friday” 등의 상대적 날짜 처리
- “17 August 2013 - 19 August 2013”와 같은 날짜 범위 처리
- “This Friday from 13:00 - 16.00”와 같은 시간 포함 날짜 처리
- “5 days ago”, “2 weeks from now”와 같은 상대적 시간 표현 처리
- “Sat Aug 17 2013 18:40:39 GMT+0900 (JST)“와 같은 표준 날짜 형식 처리
- “2014-11-30T08:15:30-05:30”와 같은 ISO 8601 형식 처리
parse(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): ParsedResult[] {
// 1. 파싱 컨텍스트 생성
// 2. 모든 파서를 실행하고 결과 수집
// 3. 결과를 인덱스 기준으로 정렬
// 4. 모든 리파이너를 적용하여 결과 개선
// 5. 최종 결과 반환
}
inclusive-dates는 자연어 입력을 지원하는 사용자 친화적이고 완전히 접근 가능한 데이트피커. 내부적으로 chrono를 사용하여 자연어 날짜 입력을 처리. 이는 두 라이브러리의 장점을 결합한 좋은 예시.
const parsedDate = await chronoParseDate(text, {
locale: this.locale.slice(0, 2),
minDate: this.minDate,
maxDate: this.minDate,
referenceDate: removeTimezoneOffset(new Date(this.referenceDate)),
...chronoOptions,
})
- chrono의 강력한 자연어 날짜 파싱 능력을 활용.
- inclusive-dates는 이를 사용자 친화적이고 접근성 높은 UI 컴포넌트로 구현.
이러한 조합을 통해, 개발자들은 사용자에게 직관적이고 유연한 날짜 입력 방식을 제공하면서도 접근성과 사용성을 높일 수 있음.