TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

AI 서비스 개발, 운영, 성능개선을 하나의 루프로 연결합니다

aidevops.kr에서 LLMOps, RAG, AI Agent, 관측성, 평가, 비용-성능 최적화를 실전 운영 관점으로 정리합니다.

React + TypeScript 설계 가이드

· 수정 4월 16일
React + TypeScript 설계 가이드 다이어그램
이 글에서 다루는 핵심 흐름, 아키텍처 구조, 주요 판단 포인트를 한눈에 이해할 수 있도록 정리한 그림입니다.
React와 TypeScript 조합의 진짜 장점은 에러를 미리 잡는 것보다 컴포넌트 계약을 분명히 만드는 데 있습니다. 어떤 props를 받는지, 어떤 이벤트가 발생하는지, 어떤 상태가 가능한지 타입으로 드러나면 팀 전체의 변경 비용이 줄어듭니다. 반대로 타입만 복잡하고 의도가 보이지 않으면 오히려 생산성이 떨어질 수 있습니다.

시작은 간단하게 가져간다

요즘은 Vite 템플릿으로 React + TypeScript 프로젝트를 빠르게 만들 수 있습니다.

npm create vite@latest my-app -- --template react-ts

초기 단계에서 중요한 것은 설정 자체보다 팀이 어떤 타입 규칙을 공유할지입니다. strict를 켜고, any 사용을 최소화하고, 도메인 모델과 UI 모델을 구분하는 것만으로도 품질이 크게 달라집니다.

Props 타입은 컴포넌트 계약이다

Props 타입은 재사용성을 높이는 문서 역할을 합니다. 꼭 필요한 값과 선택 가능한 값을 분명히 나누고, 의미가 드러나는 이름을 쓰는 것이 좋습니다.

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  disabled?: boolean
  onClick?: () => void
  children: React.ReactNode
}

function Button({ variant = 'primary', disabled = false, onClick, children }: ButtonProps) {
  return (
    <button className={variant} disabled={disabled} onClick={onClick}>
      {children}
    </button>
  )
}

상태 모델링은 문자열보다 유니온이 강하다

로딩, 성공, 실패 같은 상태를 여러 boolean으로 관리하면 조합 오류가 생기기 쉽습니다. TypeScript에서는 분기 가능한 상태를 유니온으로 표현하는 편이 더 안전합니다.

type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string }

이런 구조는 렌더링 분기와도 잘 맞고, “불가능한 상태”를 줄여줍니다.

이벤트 타입은 필요한 곳에서만 명시한다

입문 단계에서는 모든 이벤트 타입을 직접 적으려 하지만, 실무에서는 IDE 추론을 적극 활용하는 편이 좋습니다. 다만 외부로 추출되거나 재사용되는 핸들러는 명시적인 타입이 읽기 좋습니다.

제네릭은 진짜 재사용 경계에서만

TypeScript를 쓰면 제네릭 컴포넌트를 만들고 싶어지지만, 너무 이르게 일반화하면 오히려 사용성이 떨어집니다. 두세 번 이상 반복되는 패턴에서만 제네릭을 도입하는 편이 낫습니다.

자주 하는 실수

가장 흔한 실수는 백엔드 DTO 타입을 UI 컴포넌트에 그대로 퍼뜨리는 것입니다. API 응답 구조가 바뀌면 화면이 전부 흔들리기 때문입니다. 또 다른 실수는 React.FC에 과도하게 의존하거나, any로 급하게 우회해 타입 시스템 신뢰를 떨어뜨리는 경우입니다.

마무리

React + TypeScript의 핵심은 복잡한 타입 기교가 아니라 명확한 계약입니다. Props, 상태, 이벤트, 데이터 모델을 적절히 분리하면 타입은 코드를 어렵게 만드는 장치가 아니라 변경에 강한 인터페이스가 됩니다.

운영 환경에서 어려워지는 지점

  • API DTO, 폼 모델, 화면 props가 하나의 인터페이스로 섞이기 시작하면 타입 경계가 빠르게 무너진다.
  • any를 임시 탈출구가 아니라 상시 지름길로 쓰기 시작하면 컴파일 안정성은 금방 약해진다.
  • 공용 유틸리티 타입이 비즈니스 의미보다 추상화 기교를 숨기는 방향으로 커지면 유지보수 비용이 커진다.

중요한 아키텍처 결정

  • 초기에 strict, noUncheckedIndexedAccess, 경로 alias 규칙을 켜서 나쁜 패턴이 기본값이 되지 않게 한다.
  • 도메인 타입, 서버 응답 타입, UI 상태 타입을 분리하고 하나의 만능 모델로 억지 통합하지 않는다.
  • 모든 화면이 가져다 쓰는 거대한 types.ts보다 명확한 모듈 경계를 우선한다.

실무 예시

안정적인 방식은 전송 계층 타입을 경계에서 UI 친화적 모델로 변환하는 것이다.

type UserResponse = {
  id: string
  full_name: string
  last_login_at: string | null
}

type UserCardModel = {
  id: string
  displayName: string
  isDormant: boolean
}

function toUserCardModel(user: UserResponse): UserCardModel {
  return {
    id: user.id,
    displayName: user.full_name,
    isDormant: user.last_login_at === null,
  }
}

피해야 할 안티패턴

  • 백엔드 응답 구조를 그대로 재사용 컴포넌트에 노출하는 것.
  • 반복이 충분히 확인되기 전에 제네릭 헬퍼부터 만드는 것.
  • 모델링 문제를 해결하지 않고 단언문으로 경고를 덮어버리는 것.

운영 체크리스트

  • tsconfig 변경은 포맷팅이 아니라 아키텍처 결정으로 리뷰한다.
  • ESLint 규칙과 TypeScript strict 수준을 함께 맞춘다.
  • 훅, context 값, 컴포넌트 props 같은 공개 경계에는 명시적 타입을 요구한다.
  • 타입 그래프가 커질수록 증분 빌드 시간을 함께 본다.

최종 판단

React와 TypeScript 조합이 강해지는 지점은 타입이 실제 경계를 설명할 때다. 구현 세부사항만 장황하게 따라가면 복잡도만 늘고 안전성은 남지 않는다.

Continue Reading

다음으로 읽기 좋은 글

다음 탐색

이 주제를 시스템 관점으로 더 이어서 보기