icon안동민 개발노트

테스팅과 성능 최적화


 React와 TypeScript를 함께 사용할 때, 테스팅과 성능 최적화는 애플리케이션의 품질을 보장하는 중요한 요소입니다.

 이 절에서는 타입 안전성을 유지하면서 효과적인 테스트와 최적화 전략을 다룹니다.

단위 테스트 작성

 Jest와 React Testing Library를 사용한 컴포넌트 테스트

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { Counter } from './Counter';
 
test('counter increments when button is clicked', () => {
  render(<Counter initialCount={0} />);
  const incrementButton = screen.getByText('Increment');
  const countDisplay = screen.getByText('Count: 0');
 
  fireEvent.click(incrementButton);
 
  expect(countDisplay).toHaveTextContent('Count: 1');
});

 훅 테스트

import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
 
test('useCounter hook', () => {
  const { result } = renderHook(() => useCounter(0));
 
  act(() => {
    result.current.increment();
  });
 
  expect(result.current.count).toBe(1);
});

목(Mock) 객체와 스텁(Stub) 활용

 타입스크립트로 목 객체 생성

interface ApiClient {
  fetchData: () => Promise<string>;
}
 
const mockApiClient: jest.Mocked<ApiClient> = {
  fetchData: jest.fn().mockResolvedValue('mocked data')
};
 
test('component uses API client', async () => {
  render(<DataComponent apiClient={mockApiClient} />);
  await screen.findByText('mocked data');
  expect(mockApiClient.fetchData).toHaveBeenCalled();
});

통합 테스트와 E2E 테스트

 Cypress를 사용한 E2E 테스트

describe('Login Flow', () => {
  it('should login successfully', () => {
    cy.visit('/login');
    cy.get('input[name="username"]').type('testuser');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome, testuser');
  });
});

성능 분석과 최적화

 React DevTools Profiler를 사용한 성능 분석

  1. React DevTools의 Profiler 탭 사용
  2. 렌더링 시간이 긴 컴포넌트 식별
  3. 불필요한 리렌더링 확인

useMemo, useCallback의 타입 세이프티

import React, { useMemo, useCallback } from 'react';
 
interface Props {
  data: number[];
  onItemClick: (item: number) => void;
}
 
const ExpensiveList: React.FC<Props> = ({ data, onItemClick }) => {
  const sortedData = useMemo(() => [...data].sort((a, b) => a - b), [data]);
 
  const handleItemClick = useCallback((item: number) => {
    console.log('Item clicked:', item);
    onItemClick(item);
  }, [onItemClick]);
 
  return (
    <ul>
      {sortedData.map((item) => (
        <li key={item} onClick={() => handleItemClick(item)}>{item}</li>
      ))}
    </ul>
  );
};

React.memo와 타입스크립트

import React from 'react';
 
interface Props {
  name: string;
  age: number;
}
 
const Person: React.FC<Props> = React.memo(({ name, age }) => (
  <div>{name} ({age} years old)</div>
));
 
export default Person;

대규모 리스트 렌더링 최적화

 react-window를 사용한 가상화

import React from 'react';
import { FixedSizeList as List } from 'react-window';
 
interface RowProps {
  index: number;
  style: React.CSSProperties;
}
 
const Row: React.FC<RowProps> = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);
 
const VirtualizedList: React.FC = () => (
  <List
    height={400}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);
 
export default VirtualizedList;

TypeScript의 고급 기능 활용

 const assertions를 사용한 최적화

const ACTIONS = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
} as const;
 
type Action = typeof ACTIONS[keyof typeof ACTIONS];
 
function reducer(state: number, action: Action): number {
  switch (action) {
    case ACTIONS.INCREMENT:
      return state + 1;
    case ACTIONS.DECREMENT:
      return state - 1;
    default:
      return state;
  }
}

 satisfies 연산자 사용

interface Config {
  endpoint: string;
  apiKey: string;
}
 
const config = {
  endpoint: 'https://api.example.com',
  apiKey: 'my-secret-key',
  debug: true
} satisfies Config & { debug?: boolean };
 
// config.endpoint와 config.apiKey는 타입 체크됨
// config.debug는 추가적인 속성으로 허용됨

Best Practices와 도구 사용 가이드

 1. 테스팅 Best Practices

  • 컴포넌트의 동작에 집중한 테스트 작성
  • 사용자 관점에서의 테스트 케이스 구성
  • 목 객체 사용 시 타입 안전성 유지
  • 테스트 커버리지 모니터링 (예 : Jest의 --coverage 옵션 사용)

 2. 성능 최적화 Best Practices

  • 컴포넌트 분할을 통한 렌더링 최적화
  • 불변성 유지를 통한 비교 연산 최적화
  • 대규모 데이터 처리 시 가상화 기법 활용
  • 코드 스플리팅과 지연 로딩 활용

 3. 도구 사용 가이드

  • Jest: 단위 테스트와 통합 테스트에 사용
  • React Testing Library: 컴포넌트 테스트에 활용
  • Cypress: E2E 테스트 구현에 사용
  • React DevTools: 성능 프로파일링에 활용
  • Lighthouse: 전반적인 웹 성능 측정에 사용

 4. TypeScript 활용

  • 엄격한 타입 체크 설정 (strict: true in tsconfig.json)
  • 유니온 타입과 타입 가드를 활용한 정확한 타입 추론
  • const assertionssatisfies 연산자를 통한 타입 안정성 강화

 5. 지속적 통합 및 배포 (CI/CD)

  • 테스트 자동화 구축 (예 : GitHub Actions, Jenkins)
  • 성능 회귀 테스트 자동화
  • 타입 체크를 CI/CD 파이프라인에 포함

 React와 TypeScript를 함께 사용하는 프로젝트에서 테스팅과 성능 최적화는 서로 밀접하게 연관되어 있습니다.

 타입 안전성은 런타임 오류를 줄이고, 이는 곧 성능 향상으로 이어집니다.

 잘 작성된 테스트는 성능 최적화 과정에서 기능 회귀를 방지하는 안전망 역할을 합니다.

 테스팅에 있어 TypeScript의 장점은 타입 정보를 활용하여 더 정확한 목 객체와 스텁을 생성할 수 있다는 것입니다.

 이는 특히 복잡한 인터페이스를 가진 컴포넌트나 함수를 테스트할 때 유용합니다.

 또한 타입 시스템을 통해 테스트 케이스 작성 시 가능한 모든 경우의 수를 고려할 수 있게 됩니다.

 성능 최적화 측면에서 TypeScript는 컴파일 시점에 많은 최적화를 가능하게 합니다. 예를 들어, const assertions를 사용하면 객체의 불변성을 보장하고, 이는 React의 비교 알고리즘 성능을 향상시킵니다.

 또한 정확한 타입 정보는 불필요한 런타임 타입 체크를 줄여 전반적인 애플리케이션 성능을 개선합니다.