React TypeScript에서 .tsx 파일과 props 타입은 어떻게 구성하나요?
JSX를 쓰는 TypeScript 파일은 .tsx로 두고 컴포넌트가 받는 값과 호출할 함수를 props 타입에 함께 선언합니다. 외부에서 받는 계약과 내부 state를 구분하는 것이 핵심입니다.
적용 조건·범위: 단독 타입 검사에도 React 타입 선언과 JSX 설정이 필요합니다. React Vite 시작 글의 React+TypeScript 환경을 준비한 뒤 아래 수정 예제를 src/App.tsx에 저장합니다. 기존 파일의 내용과 섞지 않습니다. 아래 mount 연결을 함께 추가하면 화면에서도 확인할 수 있습니다.
공식 문서 참고: React: TSX·props·children의 TypeScript 타입
직접 확인: 오류에서 수정까지
React TypeScript 기본 구조: tsx 파일과 props 타입 기준 · 독립 실습
얻을 결과: TaskItemProps에 Task와 onToggle 계약을 명시하고 잘못된 props를 찾습니다.
예제 크게 보기 (새 탭)단독 타입 검사에도 React 타입 선언과 JSX 설정이 필요합니다. React Vite 시작 글의 React+TypeScript 환경을 준비한 뒤 아래 수정 예제를 src/App.tsx에 저장합니다. 기존 파일의 내용과 섞지 않습니다. 아래 mount 연결을 함께 추가하면 화면에서도 확인할 수 있습니다.
독자 질문: TaskItem이 받을 데이터와 클릭 함수를 어디에 선언할까요?
얻을 결과: TaskItemProps에 Task와 onToggle 계약을 명시하고 잘못된 props를 찾습니다.
React와 타입 선언이 설치된 프로젝트의 .tsx 파일에서 실행합니다.
type Task = { id: string; title: string; completed: boolean };
type TaskItemProps = { task: Task; onToggle: (id: string) => void };
function TaskItem({ task, onToggle }: TaskItemProps) { return <button onClick={() => onToggle(task.id)}>{task.title}</button>; }
const item = <TaskItem task={{ id: '1', title: '공부', completed: false }} onToggle={42} />;
예상 오류 (TS2322): onToggle에는 number가 아니라 (id: string) => void 함수가 필요합니다.
오류 예제를 다음 코드로 교체합니다.
type Task = { id: string; title: string; completed: boolean };
type TaskItemProps = { task: Task; onToggle: (id: string) => void };
function TaskItem({ task, onToggle }: TaskItemProps) { return <button onClick={() => onToggle(task.id)}>{task.title}</button>; }
const item = <TaskItem task={{ id: '1', title: '공부', completed: false }} onToggle={(id) => console.log(id)} />;
npx tsc -p . --noEmit
기존 최소 예제 확인 기록: TypeScript 7.0.2와 설치된 React 타입 선언의 별도 최소 예제에서 오류 TS2322, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트·React 앱 빌드는 미확인.
다음 학습: 18회차에서 input·form 이벤트와 ref 타입을 지정합니다.
.ts와 .tsx
JSX가 들어가는 파일은 .tsx 확장자를 사용합니다.
export default function App() {
return (
<main>
TaskFlow
</main>
);
}
일반 타입·함수 파일은 .ts로 유지할 수 있습니다.
types/task.ts
core/tasks.ts
guards/task.ts
components/TaskItem.tsx
components/TaskList.tsx
React 때문에 모든 TypeScript 파일을 .tsx로 바꾸지 않습니다.
props 타입
type GreetingProps = {
name: string;
};
export default function Greeting({
name,
}: GreetingProps) {
return (
<p>
안녕하세요, {name}
</p>
);
}
컴포넌트가 받을 수 있는 값을 props 타입에서 확인할 수 있습니다.
함수 props
type ButtonProps = {
label: string;
onClick: () => void;
};
인수가 필요한 callback:
type TaskItemProps = {
task: Task;
onToggle:
(id: string) =>
void;
};
호출하는 쪽과 받는 쪽의 계약이 연결됩니다.
type-only import
타입만 가져오는 import는 import type으로 작성할 수 있습니다.
import type {
Task,
} from '../types/task';
runtime import와 타입 import의 의도를 분명하게 할 수 있습니다.
React.FC는 꼭 필요한가
다음 방식도 가능합니다.
const TaskItem:
React.FC<TaskItemProps> =
({ task }) => {
...
};
하지만 일반 함수 컴포넌트에 반드시 React.FC를 붙일 필요는 없습니다.
export default function TaskItem({
task,
}: TaskItemProps) {
...
}
props parameter에 타입을 지정하면 충분합니다.
프로젝트 스타일과 children 처리 정책에 따라 선택합니다.
props 타입 위치
작은 컴포넌트 전용 props는 같은 파일에 둘 수 있습니다.
type TaskItemProps = {
task: Task;
};
여러 컴포넌트가 공유하는 공개 타입이면 별도 파일을 검토합니다.
타입 파일을 무조건 모두 분리할 필요는 없습니다.
독립 예제
type Product = {
id: string;
name: string;
};
type ProductCardProps = {
product: Product;
onSelect:
(id: string) =>
void;
};
export default function ProductCard({
product,
onSelect,
}: ProductCardProps) {
return (
<article>
<h2>
{product.name}
</h2>
<button
type="button"
onClick={() =>
onSelect(
product.id,
)
}
>
선택
</button>
</article>
);
}
자주 하는 실수
props 전체를 any로 둡니다
function TaskItem(
props: any,
) {
...
}
TypeScript 장점이 사라집니다.
컴포넌트 내부 state 타입을 props에 섞습니다
props는 외부 사용 계약을 설명해야 합니다.
내부 구현 세부사항까지 모두 public props에 노출하지 않습니다.
학습 완료 체크리스트
onToggle의 함수와 함수 실행 결과를 prop으로 넘기는 일은 어떻게 다를까요? 호출하는 쪽과 컴포넌트가 기대하는 string ID 매개변수·void 반환 계약을 각각 확인해 보세요.
수정 예제의 화면 연결
export default function App() { return item; }
개발 서버에서 공부 버튼을 누르면 Console에 1이 출력됩니다.
작은 변형으로 확인하기
정상 예제의 onToggle 매개변수를 number로 명시하면 string ID 계약과 충돌합니다. 함수 타입을 string으로 되돌려 확인하세요.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.
TaskFlow Core의 Task 타입을 TaskList와 TaskItem props에 연결합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
React TypeScript 기본 구조: tsx 파일과 props 타입 기준 · 단계별 TaskFlow
React props로 전달한 작업을 완료 전환하거나 삭제하세요. 추가 폼은 다음 단계에 생깁니다.
예제 크게 보기 (새 탭)변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
TypeScript 외부 API 데이터 unknown과 type guard 안전 검증의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.
React TypeScript 실행 환경에서 src/components/TaskItem.tsx와 TaskList.tsx를 만듭니다. TaskList는 TaskItem을 ./TaskItem에서 import하고 상위 컴포넌트의 onToggle/onDelete를 전달합니다. 코어 함수만 있는 16회차 폴더에는 먼저 React 실행 환경을 준비해야 합니다.
이전 체크포인트
typescript-step-16-runtime-validation
이번 목표
TaskFlow Core의 Task 타입을 TaskList와 TaskItem props에 연결합니다.
전체 파일·코드 보기
src/components/TaskItem.tsx
import type {
Task,
} from '../types/task';
type TaskItemProps = {
task: Task;
onToggle:
(id: string) =>
void;
onDelete:
(id: string) =>
void;
};
export default function TaskItem({
task,
onToggle,
onDelete,
}: TaskItemProps) {
return (
<li>
<button
type="button"
onClick={() =>
onToggle(
task.id,
)
}
>
{task.completed
? '완료 취소'
: '완료'}
</button>
<span>
{task.title}
</span>
<button
type="button"
onClick={() =>
onDelete(
task.id,
)
}
>
삭제
</button>
</li>
);
}
TaskList
import type {
Task,
} from '../types/task';
type TaskListProps = {
tasks: Task[];
onToggle:
(id: string) =>
void;
onDelete:
(id: string) =>
void;
};
export default function TaskList({
tasks,
onToggle,
onDelete,
}: TaskListProps) {
return (
<ul>
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
검증
npx tsc --noEmit
npm run build
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 typescript-step-16-runtime-validation의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- React 의존성·main.tsx·App.tsx를 연결하고 TaskItem·TaskList의 누락 import를 포함한 전체 파일을 적용합니다. 기존 vanilla TS main 파일은 비교용으로 보존합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
index.html— 전체 교체package.json— 전체 교체CHANGES.md— 전체 교체README.md— 전체 교체src/components/TaskItem.tsx— 추가src/components/TaskList.tsx— 추가src/App.tsx— 추가src/main.tsx— 추가package-lock.json— 전체 교체
실행과 확인
npm ci
npm run dev
npm run typecheck
npm test
npm run build
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 작업 두 개와 완료/삭제 버튼을 렌더링합니다. 추가 폼은 다음 events 회차에서 연결합니다.
기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과; React 폼·버튼·초점 DOM 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
index.html — 전체 파일
<!doctype html>
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>TaskFlow TypeScript</title><link rel="stylesheet" href="./style.css"></head><body><h1>TaskFlow TypeScript</h1><p>Task 계약 기반 작업 관리</p><main id="ts-taskflow"></main><p><a href="./javascript.html">기존 JavaScript 최종판</a></p><script type="module" src="./src/main.tsx"></script></body></html>
package.json — 전체 파일
{
"name": "taskflow-typescript-checkpoints",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
"start": "vite --host 127.0.0.1",
"typecheck": "tsc --noEmit",
"build": "tsc --noEmit && vite build",
"test": "node scripts/test.mjs"
},
"devDependencies": {
"typescript": "7.0.2",
"vite": "8.3.0",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
}
}
src/components/TaskItem.tsx — 전체 파일
import type {
Task,
} from '../types/task.js';
type TaskItemProps = {
task: Task;
onToggle:
(id: string) =>
void;
onDelete:
(id: string) =>
void;
};
export default function TaskItem({
task,
onToggle,
onDelete,
}: TaskItemProps) {
return (
<li>
<button
type="button"
onClick={() =>
onToggle(
task.id,
)
}
>
{task.completed
? '완료 취소'
: '완료'}
</button>
<span>
{task.title}
</span>
<button
type="button"
onClick={() =>
onDelete(
task.id,
)
}
>
삭제
</button>
</li>
);
}
src/components/TaskList.tsx — 전체 파일
import TaskItem from './TaskItem.js';
import type {
Task,
} from '../types/task.js';
type TaskListProps = {
tasks: Task[];
onToggle:
(id: string) =>
void;
onDelete:
(id: string) =>
void;
};
export default function TaskList({
tasks,
onToggle,
onDelete,
}: TaskListProps) {
return (
<ul>
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
src/App.tsx — 전체 파일
import { useState } from 'react';
import type { Task } from './types/task.js';
import { toggleTask, deleteTask } from './core/tasks.js';
import TaskList from './components/TaskList.js';
export default function App() {
const [tasks,setTasks]=useState<Task[]>([{id:'1',title:'TypeScript 공부',completed:false},{id:'2',title:'React props 확인',completed:true}]);
const onToggle=(id:string)=>setTasks(current=>toggleTask(current,id));
const onDelete=(id:string)=>setTasks(current=>deleteTask(current,id));
return <><p>전체 {tasks.length} · 완료 {tasks.filter(task=>task.completed).length}</p><TaskList tasks={tasks} onToggle={onToggle} onDelete={onDelete}/></>;
}
src/main.tsx — 전체 파일
import { createRoot } from 'react-dom/client';
import App from './App.js';
const root=document.querySelector('#ts-taskflow');
if(!root) throw new Error('TaskFlow mount missing');
createRoot(root).render(<App/>);
완료 체크포인트
typescript-step-17-react-props
다음 학습
다음 글에서는 input change, form submit, useRef에 필요한 React 이벤트와 DOM element 타입을 연결합니다.
다음 글: React TypeScript 이벤트 폼 useRef 타입 지정
글 소개·학습 준비·전체 목차

TypeScript · 17 / 22 · React 연결 · tsx와 props
JSX 파일과 컴포넌트 props의 타입 계약은 어디에 둘까요? JSX를 작성하는 파일은 .tsx로 두고, 컴포넌트가 받는 객체와 호출할 callback을 props 타입에 함께 선언합니다. 독립 버튼 예제로 string ID를 받는 함수와 잘못된 숫자 prop을 비교하며 내부 state와 외부 계약을 구분합니다.
중요한 것은 React 문법을 다시 처음부터 배우는 것이 아니라 “컴포넌트가 어떤 데이터를 받고 어떤 함수를 호출할 수 있는지”를 TypeScript 타입으로 표현하는 것입니다.
이번 글에서 배우는 내용
- .ts와 .tsx 차이
- props 객체 타입
- 함수 props
- type-only import
- React.FC를 필수로 사용하지 않는 기준
- Task 타입을 컴포넌트 경계에 연결하는 방법
단독 학습 선수 지식
React 함수 컴포넌트·JSX·props와 TypeScript 객체/함수 타입을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 22개 · 필수 21개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.