React

React에서 TypeScript를 사용하는 기본 구조: tsx 파일과 props 타입 기준 잡기

2026.06.11·약 14분

React에 TypeScript를 붙인다고 해서 React를 완전히 다른 방식으로 작성하는 것은 아닙니다. 컴포넌트, props, state, JSX라는 큰 구조는 그대로 두고, 그 안에 들어오는 값의 형태를 미리 적어두는 쪽에 가깝습니다. 처음 막히는 지점도 대부분 타입 문법 자체보다 파일 구조입니다. 어떤 파일은 .tsx로 만들고, 어떤 파일은 .ts로 둬야 하는지, props 타입은 컴포넌트 안에 적어도 되는지부터 헷갈리기 쉽습니다.

이번 글은 TypeScript 실무 활용 시리즈의 두 번째 글입니다. 앞 단계에서 TypeScript를 어떤 순서로 익힐지 로드맵을 잡았다면, 이번에는 React 프로젝트 안에서 TypeScript가 실제로 어디에 붙는지 확인합니다. props, children, 이벤트, Hook 타입을 깊게 다루기 전에 파일과 컴포넌트의 기본 골격을 먼저 정리하는 글입니다.

먼저 잡아둘 기준

JSX가 들어가는 React 컴포넌트 파일은 보통 .tsx를 사용하고, JSX가 없는 타입 정의나 유틸 함수 파일은 .ts를 사용합니다. 작은 컴포넌트의 props 타입은 처음부터 별도 파일로 빼기보다 컴포넌트 가까이에 두는 편이 흐름을 따라가기 쉽습니다.

React에서 TypeScript를 쓴다는 의미와 파일 구조

React TypeScript 프로젝트에서 components types tsx ts 파일이 나뉘는 기본 폴더 구조

React에서 TypeScript를 사용한다는 말은 화면을 그리는 방식이 바뀐다는 뜻이 아닙니다. 버튼을 컴포넌트로 만들고, 부모 컴포넌트에서 자식 컴포넌트로 props를 넘기고, 배열을 map으로 렌더링하는 방식은 그대로입니다. 달라지는 부분은 그 값들이 어떤 형태여야 하는지 코드에 같이 적는다는 점입니다.

예를 들어 JavaScript에서는 UserCard 컴포넌트가 name을 문자열로 받을지, age를 숫자로 받을지 코드를 실행하기 전까지 놓치기 쉽습니다. TypeScript에서는 이 값을 props 타입으로 미리 적어두기 때문에, 잘못된 값을 넘기는 순간 에디터나 빌드 단계에서 먼저 확인할 수 있습니다.

이때 React가 TypeScript에게 넘겨주는 가장 큰 단서가 파일 확장자입니다. JSX를 작성하는 컴포넌트 파일은 보통 .tsx를 사용합니다. 반대로 JSX 없이 타입만 정의하거나 값을 계산하는 유틸 함수라면 .ts를 사용합니다.

src/
  main.tsx
  App.tsx
  components/
    UserCard.tsx
    PrimaryButton.tsx
  types/
    user.ts
  utils/
    formatDate.ts

이 구조에서 main.tsx, App.tsx, UserCard.tsx는 JSX를 다룰 가능성이 높습니다. 그래서 .tsx가 자연스럽습니다. 반면 types/user.ts는 타입만 모아둘 수 있고, utils/formatDate.ts는 날짜 문자열을 가공하는 함수만 담을 수 있습니다. 이런 파일에는 JSX가 없으므로 .ts로 충분합니다.

처음부터 폴더 구조를 너무 세밀하게 나눌 필요는 없습니다. 작은 프로젝트에서는 컴포넌트 파일 안에 props 타입을 같이 두는 구조가 더 읽기 쉽습니다. 여러 페이지나 여러 컴포넌트에서 같은 타입을 반복해서 쓰기 시작할 때 types 폴더로 분리하면 됩니다. 처음 생성한 컴포넌트 하나를 수정하는 단계라면 파일 안에서 타입과 JSX를 함께 확인하고, 같은 타입이 두 번 이상 필요해지는 순간 분리를 고민하는 정도면 충분합니다.

.ts와 .tsx는 어디서 갈리는가

.ts.tsx의 차이는 “React 파일인가 아닌가”보다 “JSX가 들어가는가”로 판단하면 덜 헷갈립니다. React 프로젝트 안에 있어도 JSX를 반환하지 않는 파일은 .ts일 수 있습니다. 반대로 JSX를 반환하는 컴포넌트 파일은 .tsx로 두는 것이 일반적입니다.

export type User = {
  id: number;
  name: string;
  age: number;
  isOnline: boolean;
};

위 파일은 타입만 정의합니다. 화면 요소를 반환하지 않고 JSX도 없습니다. 이런 경우 파일명은 user.ts가 자연스럽습니다.

type UserCardProps = {
  name: string;
  age: number;
  isOnline: boolean;
};

function UserCard({ name, age, isOnline }: UserCardProps) {
  return (
    <article>
      <h3>{name}</h3>
      <p>{age}세</p>
      <span>{isOnline ? '온라인' : '오프라인'}</span>
    </article>
  );
}

export default UserCard;

이 파일은 JSX를 반환합니다. 따라서 UserCard.tsx처럼 작성하는 것이 맞습니다. 파일 확장자를 이렇게 나눠두면 프로젝트를 읽을 때도 역할이 빨리 보입니다. .tsx는 화면과 연결된 파일, .ts는 타입·함수·상수처럼 화면과 직접 연결되지 않을 수 있는 파일로 구분할 수 있습니다.

실수하기 쉬운 부분은 컴포넌트 파일을 .ts로 만든 뒤 JSX를 작성하는 경우입니다. 이때는 타입 오류라기보다 파일 해석 방식이 맞지 않아 JSX 구문에서 문제가 생길 수 있습니다. 반대로 타입 정의 파일을 전부 .tsx로 만들 필요도 없습니다. JSX가 없는 파일은 .ts로 두는 습관을 잡으면 파일 역할이 불필요하게 섞이지 않습니다.

컴포넌트에 props 타입을 붙이는 기본 형태

React 부모 컴포넌트에서 props를 전달하고 자식 컴포넌트에서 TypeScript Props 타입으로 받는 흐름

React TypeScript에서 가장 먼저 익숙해져야 하는 형태는 props 타입입니다. 컴포넌트는 부모에게 값을 받고, 그 값을 기준으로 화면을 그립니다. TypeScript에서는 이 props가 어떤 값인지 컴포넌트 옆에 적어둡니다.

type ProductCardProps = {
  title: string;
  price: number;
  isSoldOut: boolean;
};

function ProductCard({ title, price, isSoldOut }: ProductCardProps) {
  return (
    <article>
      <h3>{title}</h3>
      <p>{price.toLocaleString()}원</p>
      <button disabled={isSoldOut}>
        {isSoldOut ? '품절' : '구매하기'}
      </button>
    </article>
  );
}

export default ProductCard;

여기서 핵심은 ProductCardProps입니다. title은 문자열, price는 숫자, isSoldOut은 boolean 값이어야 한다고 적어둔 것입니다. 그래서 이 컴포넌트를 사용하는 쪽에서 price="12000"처럼 문자열을 넘기면 타입 오류가 납니다.

function ProductList() {
  return (
    <section>
      <ProductCard title="무선 키보드" price={42000} isSoldOut={false} />
      <ProductCard title="마우스 패드" price={9000} isSoldOut={true} />
    </section>
  );
}

이렇게 부모 컴포넌트에서 값을 넘길 때 TypeScript는 자식 컴포넌트가 요구하는 props 타입과 실제 전달된 값을 비교합니다. 화면을 실행하기 전에 잘못된 타입을 잡는다는 말은 바로 이런 상황을 의미합니다.

처음에는 이 정도 구조만 익혀도 충분합니다. 컴포넌트 위에 type Props를 만들고, 함수 매개변수 뒤에 : Props를 붙이는 흐름입니다. 컴포넌트가 하나뿐이라면 Props처럼 짧게 써도 되지만, 한 파일 안에 여러 컴포넌트가 있거나 검색 편의가 필요하다면 ProductCardProps처럼 컴포넌트 이름을 붙이는 방식이 더 명확합니다. props 타입을 더 세밀하게 나누는 방식은 다음 글인 React 컴포넌트 props 타입 지정하기에서 따로 다루는 흐름이 자연스럽습니다.

타입을 컴포넌트 안에 둘지 파일로 분리할지 판단하기

React TypeScript를 처음 배울 때 자주 생기는 오해가 있습니다. 타입은 반드시 types 폴더에 모아야 한다고 생각하는 것입니다. 실제로는 그렇지 않습니다. 타입을 어디에 둘지는 타입의 재사용 범위로 판단하면 됩니다.

특정 컴포넌트에서만 쓰는 props 타입이라면 컴포넌트 파일 안에 두는 것이 읽기 쉽습니다. 파일을 열었을 때 이 컴포넌트가 어떤 값을 받는지 바로 보이기 때문입니다.

type ProfileBadgeProps = {
  nickname: string;
  level: number;
};

function ProfileBadge({ nickname, level }: ProfileBadgeProps) {
  return (
    <div>
      <strong>{nickname}</strong>
      <span>Lv.{level}</span>
    </div>
  );
}

반대로 여러 컴포넌트에서 같은 데이터 형태를 공유한다면 별도 파일로 분리할 수 있습니다. 예를 들어 사용자 정보가 목록, 상세, 헤더 프로필 영역에서 모두 사용된다면 User 타입을 한 곳에 두는 편이 관리하기 쉽습니다.

export type User = {
  id: number;
  nickname: string;
  email: string;
  level: number;
};
import type { User } from '../types/user';

type UserProfileProps = {
  user: User;
};

function UserProfile({ user }: UserProfileProps) {
  return (
    <section>
      <h3>{user.nickname}</h3>
      <p>{user.email}</p>
    </section>
  );
}

import type은 타입만 가져올 때 사용할 수 있는 형태입니다. 런타임에서 실제 값으로 쓰는 것이 아니라 타입 검사에만 필요한 경우라면 이런 식으로 구분해두면 코드의 의도가 분명해집니다. 다만 처음부터 모든 import를 완벽하게 나누려고 애쓸 필요는 없습니다. 타입과 실제 값이 다르게 취급된다는 점을 이해하는 정도로 시작해도 됩니다.

  • 컴포넌트 하나에서만 쓰는 props 타입은 컴포넌트 파일 안에 둡니다.
  • 여러 컴포넌트가 공유하는 데이터 타입은 types 폴더나 도메인별 파일로 분리합니다.
  • API 응답 타입처럼 화면 밖에서도 의미가 있는 타입은 컴포넌트 이름보다 데이터 이름을 기준으로 작성합니다.

처음부터 복잡하게 잡지 않아도 되는 타입들

React TypeScript를 시작하자마자 모든 타입을 깊게 외우려고 하면 오히려 흐름이 끊깁니다. 특히 React.FC, ReactNode, 이벤트 타입, Hook 제네릭은 자주 보이지만 처음 글에서 한꺼번에 다루기에는 범위가 넓습니다.

React.FC는 컴포넌트 타입을 표현할 때 사용할 수 있지만, 함수 컴포넌트를 작성할 때 반드시 써야 하는 문법은 아닙니다. 초반에는 props 타입을 직접 정의하고 함수 매개변수에 붙이는 형태가 더 직관적입니다. 코드 리뷰에서도 이 구조는 props가 어디에서 결정되는지 바로 보인다는 장점이 있습니다.

type NoticeProps = {
  message: string;
};

function Notice({ message }: NoticeProps) {
  return <p>{message}</p>;
}

이 형태만으로도 props 타입 검사는 충분히 동작합니다. 이후 컴포넌트 반환 타입을 명시해야 하는지, React.FC를 써도 되는지 같은 주제는 별도 기준으로 보는 편이 좋습니다.

children도 마찬가지입니다. 버튼이나 레이아웃 컴포넌트를 만들다 보면 자연스럽게 만나게 되지만, 일반 props와는 성격이 조금 다릅니다. 문자열만 받을 수도 있고, JSX 조각을 받을 수도 있고, 여러 요소를 받을 수도 있습니다. 그래서 children 타입 지정하기는 props 기본 구조를 익힌 뒤 따로 보는 편이 이해하기 쉽습니다.

이벤트 타입과 Hook 타입도 이 글에서는 깊게 다루지 않습니다. onClick, onChange, useState, useRef는 React TypeScript에서 반드시 지나가야 하는 주제지만, 먼저 컴포넌트와 props 타입의 위치가 익숙해야 합니다.

다음 글로 이어질 React 타입 학습 순서

이번 글에서 잡아야 할 기준은 단순합니다. JSX가 있으면 .tsx, JSX가 없으면 대부분 .ts로 시작합니다. 컴포넌트가 받는 값은 props 타입으로 정의하고, 그 타입을 함수 매개변수에 연결합니다. 타입은 무조건 분리하지 않고, 컴포넌트 안에서만 쓰이면 가까이에 둡니다.

JavaScript로 작성한 기존 컴포넌트를 TypeScript로 옮길 때도 같은 순서로 보면 됩니다. 먼저 파일 확장자를 .tsx로 바꾸고, 컴포넌트가 외부에서 받는 props를 확인한 뒤, 그 props를 객체 타입으로 적습니다. 그다음 에디터가 알려주는 오류를 따라가며 문자열·숫자·boolean·배열 같은 기본 형태부터 맞추면 됩니다.

이 기준이 잡히면 다음 단계부터는 훨씬 구체적인 문제로 넘어갈 수 있습니다. props 타입을 더 깔끔하게 작성하는 방법, 선택 props와 기본값을 처리하는 방법, children을 어떤 타입으로 받을지, 이벤트 객체 타입을 어떻게 잡을지, Hook에서 타입 추론이 깨질 때 어떻게 보완할지 같은 내용입니다.

React에서 TypeScript를 잘 쓰는 출발점은 거창한 타입 설계가 아닙니다. 컴포넌트가 어떤 값을 받는지 코드 옆에 적어두고, 그 값이 부모에서 제대로 전달되는지 확인하는 습관입니다. 이 작은 구조가 익숙해지면 이후 props, state, event, ref 타입도 같은 흐름 위에서 이해할 수 있습니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기