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

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

핵심 요점

  1. 비순수한 컴포넌트

    • new Date()와 같은 비순수한 함수는 호출할 때마다 다른 결과를 선택하므로, 테스트 결과의 일관성이 없음
  2. 해결책 - 의존성 주입

    • 비순수한 함수의 결과를 prop으로 전달하여 컴포넌트를 예측 가능하게 만들고, 테스트 시 특정 날짜를 제어
  3. 기본 매개변수

    • 기본 매개변수 사용으로 오늘 날짜의 기본 동작을 유지하면서, 테스트에서 유연성을 제공

개선된 예시

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

대화 상자를 연 소프트웨어와의 상호작용을 차단합니다.

  • 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. Excel에서 긴 줄을 탐색하는 데 도움이 되는 도구입니다. 활성 셀을 추적하고 전체 행 데이터를 자동으로 가져와 별도의 창에 표시합니다. 이를 통해 좌우로 스크롤하지 않고도 행의 모든 열을 쉽게 볼 수 있습니다.

  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 애플리케이션에서 라우팅은 중요한 부분. 특히, 사용자가 링크를 클릭할 때 적절한 페이지로 이동하는지 테스트하는 것은 중요. 다음은 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

전통적인 비즈니스 모델이나 판매 전략을 통해 성장한 기업들이 기술적인 면에서 열등감을 느끼며, 표면적인 변화에 집착하고 기술 도입에 실패하는 등 여러 문제를 겪는다. 이러한 기업들은 기술 전환 과정에서 자주 자원 낭비와 재정 악화를 경험한다. 불필요하거나 과도한 기술 도입 시도는 상당한 자원 낭비를 초래하며, 이는 기업의 재정 상태를 악화시키고 중요한 프로젝트나 핵심 사업에 필요한 자원이 부족해지는 결과를 초래한다.

내부적으로도 갈등과 사기 저하가 발생한다. 기술 전환 과정에서 역할과 책임이 명확히 정의되지 않으면, 직원들 사이에 갈등이 발생하고, 새로운 기술 도입에 대한 교육 부족이나 명확한 비전 부재는 직원들의 사기를 저하시키고 이직률을 높이는 요인이 된다. 이러한 혼란은 결국 고객 이탈을 가속화한다. 기술 도입이 미흡하거나 중단되는 경우, 고객들은 기업의 신뢰성을 의심하게 되고, 특히 디지털 서비스가 부실하면 고객 경험이 악화되어 경쟁사로 이탈하게 된다. 이는 매출 감소로 직결되고 기업의 시장 점유율을 급격히 떨어뜨린다.

경영진의 신뢰 손상도 큰 문제가 된다. 기술 전환의 실패는 경영진의 판단력에 대한 의문을 불러일으키며, 이는 주주들과 투자자들의 신뢰를 흔들리게 하고, 주가 하락과 함께 기업의 시장 가치를 떨어뜨린다. 장기적인 재무 안정성에도 부정적인 영향을 미친다. 또한, 잘못된 기술 도입으로 인해 기업이 기술에 지나치게 의존하게 되면 본래의 영업 중심 모델이 약화되고, 기술적 문제가 발생할 때마다 더 큰 리스크에 노출된다.

이 과정에서 혁신 저해가 일어난다. 무분별한 기술 도입이 오히려 조직의 혁신 문화를 저해하고, 기술 도입이 실패로 끝날 경우 조직 내에서는 새로운 시도를 꺼리게 되어 전체적인 혁신 역량이 저하된다. 결과적으로 기업은 변화하는 시장 환경에 적응하지 못하게 된다. 이러한 실패와 고객 신뢰 상실은 장기적으로 회복 불가능한 손실을 초래한다. 브랜드 이미지가 심각하게 손상되고, 시장에서의 입지가 약화되어 재도약이 어려워진다.

이와 같은 부정적인 결과는 영업 중심의 기업들이 기술적 역량을 무리하게 키우려 할 때 나타나는 전형적인 문제들이다. 이러한 함정에 빠지지 않기 위해서는, 기술 도입에 있어 신중하고 체계적인 접근이 필요하며, 기존의 강점을 유지하면서 기술을 보완하는 전략을 세워야 한다. 기술적 열등감을 극복하려는 시도가 오히려 기업의 몰락을 초래할 수 있음을 인지하고, 보다 현실적이고 균형 잡힌 접근을 해야 한다.

#308
if (!data) {
  throw fetch()
}
  1. 컴포넌트가 렌더링될 때, 비동기 작업(예: 데이터 패칭)이 시작됩니다. 이 작업은 일반적으로 promise를 반환합니다.
  2. 비동기 작업이 완료되지 않은 경우, 컴포넌트는 promise를 던집니다. 이는 JavaScript에서 예외를 던지는 것과 유사합니다. Suspense는 promise가 던져질 때 이를 캐치하고 fallback UI를 표시하는 역할을 합니다.
  3. React는 컴포넌트가 promise를 던졌을 때 이를 감지하고, Suspense 컴포넌트에서 이를 “캐치”합니다. Suspense는 이 promise가 해결될 때까지 대체 UI (fallback)를 렌더링합니다. Concurrent Mode에서는 React가 이 promise를 추적하고, 비동기 작업이 완료될 때까지 렌더링을 중단합니다.
  4. Promise가 해결되면(즉, 비동기 작업이 완료되면) React는 컴포넌트를 다시 렌더링합니다. Suspense는 현재 데이터 패칭 라이브러리(예: React Query, SWR)와 함께 사용되어 비동기 작업의 상태를 쉽게 관리할 수 있도록 도와줍니다.

#307
17 중 3페이지