심화

[활용 01] TypeScript 실무 활용 로드맵

2026.06.11·약 24분

TypeScript 기초 문법을 한 번 훑고 나면 오히려 다음 단계에서 더 막히는 경우가 많습니다. string, number, type, interface는 알겠는데, 막상 React 프로젝트를 열면 어디부터 타입을 붙여야 할지 애매해집니다. 컴포넌트 props부터 잡아야 하는지, API 응답 타입부터 만들어야 하는지, 아니면 상태 관리 코드부터 정리해야 하는지 순서가 잘 보이지 않습니다.

활용 편은 TypeScript 문법을 다시 나열하는 흐름이 아닙니다. 실제 화면을 만들 때 데이터가 어디서 들어오고, 어떤 컴포넌트를 지나고, 어떤 상태로 보관되고, 어떤 요청 값으로 서버에 전달되는지 따라가며 타입을 붙이는 흐름에 가깝습니다. 그래서 이 로드맵에서는 React 화면 하나를 기준으로 props, event, state, API, query, store, form, Next.js 구조까지 차례대로 연결합니다.

이 글은 활용 01번 글입니다. 뒤에 이어지는 44개의 글을 단순히 목록으로 외우기보다, 지금 공부하는 내용이 어느 구간에 속하는지 확인하는 기준으로 쓰면 됩니다. 예시는 게시글 목록, 검색 폼, 작성 폼, 상세 페이지가 있는 일반적인 게시판 화면을 기준으로 잡습니다. 처음부터 모든 타입을 완벽하게 설계하려고 하기보다, 화면에서 가장 가까운 타입부터 잡고 서버 데이터와 라우팅 경계로 넓혀 가는 방향이 덜 복잡합니다.

React 화면부터 타입을 붙이면 흐름이 보인다

React 컴포넌트에서 props 이벤트 state 타입이 연결되는 구조

React 프로젝트에서 TypeScript를 처음 적용할 때 가장 먼저 확인하기 좋은 지점은 컴포넌트입니다. 컴포넌트는 화면을 나누는 단위이면서 동시에 데이터를 주고받는 단위입니다. 어떤 컴포넌트가 어떤 props를 받아야 하는지 정리하면, 그 컴포넌트를 잘못 사용하는 코드가 빠르게 드러납니다.

예를 들어 게시글 목록 화면을 만든다고 생각해보면, 게시글 카드 컴포넌트는 제목, 작성자, 날짜, 댓글 수 같은 값을 받을 수 있습니다. 이때 props 타입을 정리하지 않으면 각 컴포넌트가 어떤 값을 기대하는지 코드만 보고 추측해야 합니다. 반대로 props 타입이 있으면 컴포넌트의 사용 규칙이 문서처럼 남습니다.

type PostCardProps = {
  title: string;
  authorName: string;
  createdAt: string;
  commentCount: number;
};

function PostCard({ title, authorName, createdAt, commentCount }: PostCardProps) {
  return (
    <article>
      <h3>{title}</h3>
      <p>{authorName} · {createdAt}</p>
      <span>댓글 {commentCount}개</span>
    </article>
  );
}

이 코드에서 봐야 할 부분은 타입 문법 자체가 아닙니다. PostCard가 화면에서 어떤 데이터를 필요로 하는지 명확히 드러난다는 점입니다. 제목은 문자열이고, 댓글 수는 숫자여야 합니다. 다른 곳에서 이 컴포넌트를 사용할 때 commentCount에 문자열을 넘기면 TypeScript가 바로 알려줍니다.

실제 작업에서는 props 타입이 컴포넌트 수정 범위를 확인하는 기준이 되기도 합니다. 게시글 카드에 썸네일이 추가되면 thumbnailUrl을 필수로 받을지, 이미지가 없는 게시글도 허용해서 optional props로 둘지 결정해야 합니다. 이 판단이 흐려지면 컴포넌트 안에서 조건문이 늘어나고, 사용하는 쪽에서는 어떤 값을 넘겨야 하는지 다시 확인해야 합니다.

활용 02번부터 05번까지는 이 흐름을 기준으로 React에서 TypeScript를 쓰는 기본 구조, props 타입, children 타입, optional props와 default value를 다룹니다. 초반 구간에서 봐야 할 핵심은 “타입을 많이 쓰는 법”이 아니라 “컴포넌트가 외부와 맺는 계약을 어떻게 적을 것인가”입니다.

이벤트와 상태 타입은 화면 변화의 범위를 정한다

컴포넌트 props 타입을 잡은 뒤에는 이벤트와 상태 타입이 이어집니다. 화면은 사용자가 클릭하고, 입력하고, 제출하면서 계속 바뀝니다. 이때 TypeScript는 값이 바뀌는 경로를 확인하는 역할을 합니다. 버튼 클릭 이벤트, input 변경 이벤트, form 제출 이벤트가 각각 어떤 이벤트 객체를 받는지 알고 있으면 불필요한 추측이 줄어듭니다.

특히 input, select, textarea는 초반에 자주 헷갈립니다. 모두 값을 입력받는 요소처럼 보이지만 이벤트 대상의 타입이 조금씩 다릅니다. React에서 onChange를 다룰 때 이벤트 타입을 정확히 적으면 event.target.value를 안전하게 사용할 수 있고, 나중에 폼 로직이 커져도 흐름을 따라가기 쉽습니다.

import { ChangeEvent, FormEvent, useState } from 'react';

function PostSearchForm() {
  const [keyword, setKeyword] = useState('');

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setKeyword(event.target.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log(keyword);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={keyword} onChange={handleChange} />
      <button type="submit">검색</button>
    </form>
  );
}

이 예시에서는 검색어가 문자열이라는 사실이 useState('')에서 자연스럽게 추론됩니다. 하지만 항상 이렇게 추론이 잘 되는 것은 아닙니다. 빈 배열을 초기값으로 넣으면 TypeScript가 배열 안에 어떤 값이 들어갈지 알 수 없어 never[]로 추론하는 경우가 생깁니다. 활용 08번과 09번에서 이 문제를 따로 다루는 이유도 여기에 있습니다.

상태 타입은 단순히 useState<string>처럼 제네릭을 쓰는 문법 문제가 아닙니다. 화면이 가질 수 있는 값을 어디까지 허용할지 정하는 작업입니다. 게시글 목록이 비어 있을 수 있는지, 선택된 게시글이 아직 없을 수 있는지, 로딩 상태와 에러 상태를 별도로 둘 것인지 같은 판단이 상태 타입에 반영됩니다.

type PostListState =
  | { status: 'loading' }
  | { status: 'success'; posts: PostListItem[] }
  | { status: 'error'; message: string };

이런 형태는 처음에는 길어 보이지만, 화면 상태를 섞어서 다루지 않게 해줍니다. 로딩 중에는 게시글 배열이 없고, 성공했을 때만 posts가 있으며, 실패했을 때는 에러 메시지를 보여준다는 규칙이 타입에 그대로 남습니다. 활용 편의 상태 구간에서는 이런 식으로 값의 모양뿐 아니라 화면의 가능한 상태까지 함께 정리합니다.

API 타입을 잡아야 실무 TypeScript가 시작된다

React 컴포넌트 안에서 props와 state를 정리하는 것만으로도 TypeScript의 장점은 느낄 수 있습니다. 하지만 실무에서 타입 설계가 본격적으로 어려워지는 지점은 API입니다. 서버에서 내려오는 데이터는 화면에서 바로 쓰기 좋게 정리되어 있지 않은 경우가 많습니다. 필드 이름이 길거나, nullable 값이 섞여 있거나, 화면에서는 필요 없는 값이 함께 내려올 수 있습니다.

이때 서버 응답 타입과 화면 표시 타입을 무조건 하나로 합치면 처음에는 편해 보입니다. 문제는 화면 요구사항이 바뀔 때 생깁니다. 서버에서는 created_at으로 내려오는데 화면에서는 createdAtText처럼 가공된 문자열을 쓰고 싶을 수 있습니다. 또는 서버 응답에는 작성자 객체가 들어오지만 화면에서는 작성자 이름만 필요할 수 있습니다.

type PostResponse = {
  id: number;
  title: string;
  writer: {
    name: string;
  };
  created_at: string;
  comments_count: number;
};

type PostListItem = {
  id: number;
  title: string;
  authorName: string;
  createdAtText: string;
  commentCount: number;
};

function mapPostListItem(post: PostResponse): PostListItem {
  return {
    id: post.id,
    title: post.title,
    authorName: post.writer.name,
    createdAtText: post.created_at,
    commentCount: post.comments_count,
  };
}

이런 분리는 처음에는 번거롭게 보일 수 있습니다. 하지만 프로젝트가 커질수록 서버 응답 타입과 화면 표시 타입을 구분하는 쪽이 수정 범위를 좁히는 데 유리합니다. API 응답 형식이 바뀌었을 때 매핑 함수와 응답 타입을 중심으로 확인하면 되고, 화면 컴포넌트는 이미 정리된 표시용 타입을 기준으로 유지할 수 있습니다.

반대로 모든 화면이 서버 응답을 그대로 사용한다면 굳이 표시용 타입을 먼저 만들 필요는 없습니다. 타입을 분리하는 기준은 “언젠가 바뀔 수 있으니까 미리 나누자”가 아니라, 실제로 필드명을 바꾸거나 날짜를 포맷하거나 중첩 객체를 평평하게 만드는 변환이 있는지입니다. 변환이 생기는 순간부터 분리의 이점이 뚜렷해집니다.

활용 17번부터 21번까지는 이 구간을 다룹니다. API 응답 타입 설계, 서버 응답 타입과 화면 표시 타입 분리, Axios 응답 타입, Axios 에러 타입, fetch 함수 타입 지정이 이어집니다. 이 구간부터는 TypeScript가 단순 문법 도구가 아니라 데이터 흐름을 정리하는 도구로 보이기 시작합니다.

서버 상태와 전역 상태는 같은 문제가 아니다

상태 관리 라이브러리를 TypeScript와 함께 쓰기 시작하면 타입이 갑자기 복잡해진다고 느낄 수 있습니다. 하지만 먼저 구분해야 할 것은 라이브러리 이름이 아니라 상태의 성격입니다. 서버에서 가져와 캐싱해야 하는 데이터와, 클라이언트 안에서 여러 컴포넌트가 공유해야 하는 UI 상태는 같은 문제가 아닙니다.

TanStack Query는 서버 상태를 다룰 때 중심이 됩니다. 게시글 목록을 가져오고, 상세를 조회하고, 작성 요청을 보내고, 성공 후 목록을 다시 갱신하는 흐름이 여기에 들어갑니다. TypeScript를 함께 쓰면 query function의 반환 타입, select로 가공한 데이터 타입, mutation variables 타입을 확인할 수 있습니다.

Redux Toolkit은 전역 상태와 action payload의 흐름을 명확히 할 때 유용합니다. 예를 들어 로그인 사용자 정보, 전역 모달 상태, 여러 페이지에서 공유하는 설정값처럼 클라이언트 내부에서 오래 유지되는 값에 적합합니다. 이때 RootState, AppDispatch, PayloadAction 같은 타입이 등장합니다. 이름만 보면 어렵지만, 결국 store에서 꺼내는 값과 dispatch로 보내는 값의 모양을 정리하는 역할입니다.

Zustand는 작은 store를 빠르게 만들 때 자주 선택됩니다. Redux Toolkit보다 구조가 가볍지만, store 안에 들어가는 state와 action 타입은 여전히 필요합니다. 특히 selector를 나눠 쓰기 시작하면 어떤 값이 반환되는지 타입이 안정적으로 잡혀야 컴포넌트에서 불필요한 확인 코드를 줄일 수 있습니다.

활용 22번부터 32번까지는 서버 상태와 전역 상태 구간입니다. TanStack Query, Redux Toolkit, Zustand를 한 줄로 비교해서 어느 것이 더 좋다고 결론내기보다, 각각 어떤 상태를 책임지는지 나눠 보는 것이 핵심입니다.

판단 기준은 단순합니다. 원본이 서버에 있고 다시 가져오거나 갱신해야 하는 값이면 TanStack Query 쪽에서 먼저 생각합니다. 여러 화면에서 공유하지만 서버와 직접 동기화되지 않는 선택값, 모달 상태, 편집 중인 UI 상태라면 Redux Toolkit이나 Zustand 같은 클라이언트 상태 관리로 분리할 수 있습니다. 이 기준을 잡아두면 라이브러리를 추가할 때마다 타입 위치가 흔들리지 않습니다.

폼과 검증 타입은 입력값을 믿을 수 있게 만든다

폼은 TypeScript를 적용할 때 생각보다 까다로운 영역입니다. 화면에는 문자열로 입력되지만 서버에는 숫자로 보내야 할 수도 있고, 체크박스나 select 값은 UI 상태와 요청 데이터의 형태가 달라질 수 있습니다. 또 사용자가 입력한 값은 TypeScript 타입만으로는 신뢰할 수 없습니다. 타입 검사는 개발 중 코드의 형태를 확인하지만, 런타임에 실제 사용자가 어떤 값을 입력했는지 검증해주지는 않습니다.

그래서 React Hook Form과 Zod를 함께 사용하는 흐름이 자주 등장합니다. React Hook Form은 입력 상태와 제출 흐름을 관리하고, Zod는 실제 값이 원하는 조건을 만족하는지 검증합니다. 여기서 TypeScript는 Zod schema로부터 타입을 추론해 폼 데이터의 형태를 코드에서 안전하게 사용하도록 도와줍니다.

import { z } from 'zod';

const postFormSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(10),
  categoryId: z.coerce.number(),
});

type PostFormValues = z.infer<typeof postFormSchema>;

이 구조에서 PostFormValues를 직접 따로 작성하지 않고 schema에서 추론할 수 있습니다. 다만 폼 데이터 타입과 API 요청 타입이 항상 같지는 않습니다. 화면에서는 categoryId를 문자열로 선택하다가 제출 직전에 숫자로 바꿀 수도 있고, 화면에는 임시 첨부파일 정보가 있지만 API에는 업로드된 파일 ID만 보내야 할 수도 있습니다.

활용 33번부터 37번까지는 이 지점을 다룹니다. React Hook Form 기본 타입, Zod 연결, Zod schema에서 타입 추론, resolver 타입 오류, 폼 데이터 타입과 API 요청 타입 분리까지 이어집니다. 폼 구간에서는 “입력값 타입”, “검증된 값 타입”, “요청으로 보낼 값 타입”을 구분하는 감각이 중요합니다.

Next.js App Router에서는 타입 경계가 더 중요해진다

Next.js App Router로 넘어가면 타입을 붙여야 할 경계가 더 많아집니다. React 컴포넌트 내부의 props와 state만 보는 것이 아니라, 라우트 params, searchParams, server component props, route handler의 요청과 응답, 환경 변수까지 확인해야 합니다.

예를 들어 게시글 상세 페이지에서는 URL의 id가 문자열로 들어옵니다. 화면에서는 숫자 ID처럼 쓰고 싶어도 라우팅 경계에서는 문자열이라는 사실을 먼저 받아들여야 합니다. 이 값을 어디에서 숫자로 변환할지, 변환에 실패했을 때 어떤 처리를 할지 정하지 않으면 타입만 맞춘 것처럼 보이고 실제 예외 처리는 비어 있게 됩니다.

type PostPageProps = {
  params: {
    id: string;
  };
};

export default function PostPage({ params }: PostPageProps) {
  const postId = Number(params.id);

  if (Number.isNaN(postId)) {
    return <p>잘못된 게시글 주소입니다.</p>;
  }

  return <PostDetail postId={postId} />;
}

이 예시에서 핵심은 params.id를 처음부터 숫자로 단정하지 않는 것입니다. 라우팅에서 들어오는 값은 문자열이고, 화면에서 숫자 ID가 필요하다면 변환 지점을 코드에 남겨야 합니다. TypeScript 타입은 이 경계를 숨기는 도구가 아니라 드러내는 도구에 가깝습니다.

server component와 client component의 타입을 분리하는 것도 중요합니다. 서버 컴포넌트에서는 서버에서 가져온 데이터를 props로 내려줄 수 있고, 클라이언트 컴포넌트에서는 이벤트 핸들러와 상태를 다룹니다. 두 컴포넌트가 같은 파일에서 섞이거나, 서버에서만 가능한 코드를 클라이언트 컴포넌트로 가져오면 타입 문제와 런타임 문제가 함께 생길 수 있습니다.

활용 38번부터 43번까지는 Next.js App Router에서 TypeScript를 사용하는 구조를 다룹니다. params와 searchParams 타입, server component props, route handler 타입, 환경 변수 타입 관리, client component와 server component 타입 분리가 이 구간의 핵심입니다.

활용 편은 기능 흐름대로 읽는 것이 좋다

TypeScript 활용 편 학습 순서와 실무 타입 설계 구간 로드맵

활용 편 글이 45개로 나뉘어 있으면 양이 많아 보일 수 있습니다. 하지만 하나씩 따로 떨어진 글이라기보다, React 프로젝트에 타입을 붙이는 범위가 넓어지는 흐름으로 보면 부담이 줄어듭니다. 처음에는 컴포넌트 하나의 props를 정리하고, 다음에는 이벤트와 상태를 잡고, 이후 API 응답과 서버 상태, 전역 상태, 폼, Next.js 경계로 확장됩니다.

구간 해당 글 핵심 기준
컴포넌트 타입 활용 02~05 props, children, optional props의 사용 규칙을 정리한다
이벤트와 상태 타입 활용 06~16 사용자 입력과 화면 변화의 타입 범위를 정한다
API 타입 활용 17~21 서버 응답, 화면 표시 데이터, 요청 함수를 분리한다
서버 상태 활용 22~26 TanStack Query의 query, mutation, queryKey 타입을 정리한다
전역 상태 활용 27~32 Redux Toolkit과 Zustand store의 상태와 action 타입을 잡는다
폼과 검증 활용 33~37 React Hook Form, Zod, API 요청 타입의 역할을 나눈다
Next.js 구조 활용 38~43 라우팅, 서버 컴포넌트, route handler의 타입 경계를 확인한다
종합 적용 활용 44~45 공통 컴포넌트와 게시판 화면에 타입 설계를 연결한다

처음부터 Next.js route handler나 Zod resolver 타입 오류를 먼저 보면 어렵게 느껴질 수 있습니다. 반대로 props, event, state 흐름을 먼저 잡아두면 뒤의 도구들이 훨씬 자연스럽게 이어집니다. 결국 모든 구간은 “값이 어디서 들어와서 어디로 이동하는가”라는 질문으로 연결됩니다.

이미 React 프로젝트를 진행 중이라면 반드시 활용 02번부터 순서대로 볼 필요는 없습니다. 지금 막히는 위치가 API 응답이면 API 구간을 먼저 보고, 폼 검증 오류가 반복된다면 React Hook Form과 Zod 구간부터 봐도 됩니다. 다만 처음부터 전체 흐름을 잡고 싶다면 컴포넌트, 상태, API, 서버 상태, 폼, Next.js 순서가 가장 안정적입니다.

타입을 많이 쓰는 것보다 경계를 정확히 잡는 것이 먼저다

TypeScript를 실무에서 쓰다 보면 타입을 촘촘하게 적는 것 자체가 목표처럼 느껴질 때가 있습니다. 하지만 타입이 많다고 좋은 코드가 되는 것은 아닙니다. 더 중요한 것은 타입이 필요한 경계에 정확히 놓이는 것입니다.

대표적인 경계는 외부에서 값이 들어오는 지점입니다. 컴포넌트 props, 사용자 입력, API 응답, URL params, 환경 변수, 라이브러리 callback이 여기에 해당합니다. 이 지점들은 코드 작성자가 값을 완전히 통제하기 어렵습니다. 그래서 타입을 적고, 필요하면 검증을 붙이고, 화면에서 쓰기 좋은 형태로 변환하는 과정이 필요합니다.

반대로 함수 내부에서 바로 계산하고 끝나는 임시 값까지 지나치게 타입을 반복해서 적으면 코드가 무거워질 수 있습니다. TypeScript의 타입 추론이 충분히 잘 작동하는 곳은 추론을 활용하고, 추론만으로 의도가 드러나지 않는 곳에는 타입을 명시하는 균형이 필요합니다.

실무에서는 “이 타입을 지우면 다른 사람이 코드를 이해하기 어려워지는가”를 기준으로 삼으면 판단이 쉽습니다. props, API 응답, 요청 함수, form submit 값처럼 외부와 맞닿은 값은 명시하는 쪽이 낫고, 한 함수 안에서 바로 만들어 바로 쓰는 값은 추론에 맡겨도 충분한 경우가 많습니다.

타입 오류가 보일 때마다 any로 덮는 방식은 초반에는 빠르게 느껴집니다. 하지만 시간이 지나면 TypeScript가 알려줘야 할 위험 신호를 직접 꺼버리는 결과가 됩니다. 특히 API 응답, form 값, action payload처럼 여러 코드에 영향을 주는 값에 any가 들어가면 이후 수정에서 문제가 늦게 발견됩니다.

정리: TypeScript 실무 활용의 기준은 데이터 흐름이다

TypeScript 활용 편의 중심은 문법 암기가 아닙니다. React 프로젝트 안에서 데이터가 어떻게 이동하는지 보고, 그 이동 지점마다 필요한 타입을 붙이는 것입니다. props는 컴포넌트의 입력 규칙을 정하고, state는 화면이 가질 수 있는 값의 범위를 정합니다. 이벤트 타입은 사용자 입력 흐름을 안전하게 다루게 해주고, API 타입은 서버 데이터와 화면 데이터를 분리하는 기준이 됩니다.

TanStack Query, Redux Toolkit, Zustand, React Hook Form, Zod, Next.js는 각각 다른 문제를 해결합니다. 이 도구들을 TypeScript와 함께 사용할 때는 라이브러리별 문법만 외우기보다 어떤 데이터 흐름을 책임지는지 먼저 봐야 합니다. 서버에서 가져오는 값인지, 클라이언트에서 공유하는 값인지, 사용자가 입력한 값인지, URL에서 들어오는 값인지에 따라 타입 설계의 위치가 달라집니다.

활용 편은 React에서 TypeScript를 사용하는 기본 구조로 이어집니다. 다음 단계에서는 파일 확장자, 컴포넌트 작성 방식, props와 state를 어디에 적는지부터 실제 React 코드 기준으로 정리하면 됩니다. 이 로드맵을 기준으로 보면 각 글은 따로 떨어진 주제가 아니라, 하나의 화면을 TypeScript로 안전하게 만들어 가는 과정으로 연결됩니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기