React에서 TypeScript 사용하기
부모 컴포넌트가 prop 이름을 word에서 currentWord로 바꾸었습니다. 자식 컴포넌트 3개는 여전히 word를 읽고 있고, 아무것도 이를 지적하지 않습니다. 앱은 빌드되고, 페이지는 로드되며, 그 자식 컴포넌트 중 하나를 렌더링하는 버튼을 클릭한 첫 번째 사용자가 빈 화면을 보게 됩니다. TypeScript는 JavaScript 위에 타입 레이어를 추가합니다. 모든 값은 선언되거나 추론된 타입을 가지며, 컴파일러는 코드가 실행되기 전에 타입이 맞지 않는 부분을 지적합니다.
React에서 이것은 데이터가 컴포넌트 사이를 이동하는 경계, 바로 그런 버그가 숨어 있는 곳에서 효과를 발휘합니다. 타입 지정된 prop은 계약입니다. 부모는 올바른 형태를 보내야 하고, 자식은 도착한 것을 신뢰할 수 있으며, 편집기는 양쪽 모두를 자동 완성해줍니다.
이 장에서는 React 관련 부분을 다룹니다. 상태, props, children, 함수 props, 컴포넌트 반환값을 타입 지정하는 방법을 설명합니다. 이 장은 TypeScript의 기초, union 타입, 커스텀 타입, 제네릭을 이미 알고 있다고 가정합니다. 이 섹션의 처음 부분에서 이 모든 것을 다시 한 번 다루고, 그 다음 Assembly: Endgame 게임을 TypeScript로 다시 만들면서 모든 것을 실무에 적용합니다.
설정: Vite의 react-ts 템플릿
Vite는 React 템플릿의 TypeScript 버전을 제공합니다. 프로젝트 생성 시 한 가지 플래그만 있으면 완벽하게 설정된 프로젝트를 얻을 수 있습니다.
npm create vite@latest my-react-app -- --template react-ts컴포넌트 파일은 .jsx 대신 .tsx 확장자를 사용하고 (TypeScript와 JSX), JSX가 없는 일반 모듈은 .js 대신 .ts를 사용합니다. 그 외 설정에 관한 모든 것은 항상 그대로 작동합니다. Vite는 타입을 제거하고 순수한 JavaScript를 브라우저로 보냅니다.
여기서 한 가지 주의할 점이 있습니다. 타입 오류는 편집기에 표시되고 번들링 전에 tsc -b를 실행하는 npm run build를 실패시킵니다. 하지만 개발 서버는 타입을 검사하지 않고 제거합니다. npm run dev는 타입 오류가 가득한 프로젝트도 서빙할 수 있으므로 실행되는 앱이 타입이 통과한다는 증거가 아닙니다.
상태 타입 지정하기
useState는 보통 자동으로 타입이 지정됩니다. hook은 전달하는 초기값에서 타입을 추론하므로, 실제 시작 값으로 생성된 상태는 주석이 필요하지 않습니다.
const [currentWord, setCurrentWord] = useState(getRandomWord())getRandomWord가 문자열을 반환하면, currentWord는 string이고 setCurrentWord는 문자열만 받습니다. setCurrentWord(true)를 호출하면 TypeScript는 즉시 이를 지적합니다. boolean은 문자열이 예상되는 곳에 할당할 수 없습니다. 이것이 추론이 제대로 작동하는 것입니다. 대부분의 상태에서는 그냥 두면 됩니다.
추론이 실패하는 경우는 초기값이 빈 값이거나 상태의 미래 형태를 설명하기에 너무 느슨할 때입니다. 빈 배열은 그것이 무엇을 보관할지 말하지 않으므로 useState([])는 never[]를 추론합니다. 요소가 절대 아무것도 될 수 없는 배열이므로, 그것에 대한 모든 push는 오류입니다. 이것이 명시적 형태를 사용하는 곳입니다. useState는 제네릭 함수이고, 타입 인자를 꺾쇠괄호로 전달합니다.
const [guessedLetters, setGuessedLetters] = useState<string[]>([])이제 초기값이 빈 배열이더라도 상태는 문자열의 배열이며, 값과 setter 모두 이를 강제합니다. 같은 방식은 값이 null로 시작하고 나중에만 실제 값이 되는 nullable 상태도 다룹니다.
type Word = { text: string; difficulty: number }
const [selectedWord, setSelectedWord] = useState<Word | null>(null)union Word | null은 TypeScript에 이 상태가 법적으로 보관할 수 있는 두 형태를 알려줍니다. 이제 selectedWord를 읽을 때마다 .text를 건드리기 전에 null 케이스를 처리하도록 강제됩니다. 이것은 전형적인 런타임 충돌을 컴파일 타임의 알림으로 바꿉니다. 상태의 메커니즘 자체는 변하지 않습니다. TypeScript는 상태가 보관할 수 있는 내용을 고정할 뿐입니다.
컴포넌트 props 타입 지정하기
Props는 하나의 객체로 도착하므로, 타입을 지정하는 것은 그 객체에 주석을 다는 것입니다. prop이 1개나 2개인 컴포넌트의 경우, 인라인 주석이 잘 작동합니다.
function ConfettiContainer({ isGameWon }: { isGameWon: boolean }) {
// ...
}Props가 늘어나면 인라인 타입은 복잡해집니다. 표준 패턴은 명명된 **타입 별칭**이며, 관례상 ComponentNameProps로 불리며, 컴포넌트 위에 선언됩니다.
type GameStatusProps = {
isGameWon: boolean
wrongGuessCount: number
message?: string
}
function GameStatus({ isGameWon, wrongGuessCount, message }: GameStatusProps) {
// ...
}message의 ?는 선택 사항임을 표시합니다. 부모는 생략할 수 있으며, 컴포넌트 내부에서 타입은 string | undefined입니다. interface GameStatusProps { ... } 선언은 정확히 같은 위치에서 작동합니다. props를 타입 지정하는 것에 관해서는 둘이 상호 교환 가능하며, 이 과정에서는 다른 곳에서 구축하는 커스텀 타입과의 일관성을 위해 타입 별칭을 고수합니다.
컴포넌트가 children을 받으면, ReactNode로 타입 지정합니다. React가 렌더링할 수 있는 모든 것을 포함하는 넓은 타입입니다. 요소, 문자열, 숫자, fragment, 이들의 배열 등입니다.
import { type ReactNode } from 'react'
type CardProps = {
title: string
children: ReactNode
}children이 컴포넌트를 통해 어떻게 흐르는지는 Children and composition에서 다룹니다. ReactNode는 이들을 설명하는 타입입니다. props가 런타임에 동작하는 방식에 관한 모든 것은 같습니다. 타입 주석은 위에 계약을 추가합니다.
함수 props 타입 지정하기
컴포넌트는 종종 함수를 props로 받습니다. 클릭 핸들러, 값을 위쪽으로 보고하는 콜백이 그 예입니다. 함수 prop의 타입은 화살표 문법을 사용합니다. 매개변수 목록과 그 타입, 화살표, 그리고 반환 타입입니다.
type NewGameButtonProps = {
startNewGame: () => void
}
type LetterButtonProps = {
letter: string
onGuess: (value: string) => void
}() => void는 인자를 받지 않고 아무것도 반환하지 않는 함수의 형태를 설명합니다. 대부분의 이벤트 스타일 핸들러가 이 형태입니다. (value: string) => void는 자식이 문자열로 함수를 호출할 것이므로 부모의 구현이 하나를 받아야 한다고 말합니다. 매개변수 타입이 일치하지 않는 핸들러를 전달하면 오류가 JSX 호출 부분, 부모에서, 컴파일 타임에 나타납니다.
하지만 체크는 두 가지를 허용합니다. 그 둘 다 타입 체커가 뭔가 놓친 것처럼 보입니다. 핸들러는 타입이 나열하는 것보다 더 적은 매개변수를 선언할 수 있습니다. 따라서 onGuess={() => setOpen(true)}는 (value: string) => void를 만족합니다. 그리고 () => void는 값을 반환하는 함수를 받으므로 startNewGame={async () => { await saveScore() }}는 아무것도 기다리지 않는 promise를 넘기더라도 타입 체크를 통과합니다. 컴파일러는 형태의 모든 차이가 아니라 잘못된 매개변수 타입을 포착합니다.
반환 타입과 파생 값
함수 컴포넌트 위에 마우스를 올리면 TypeScript는 이미 그것이 React.JSX.Element를 반환한다고 알고 있습니다. 반환 문의 JSX에서 추론됩니다. (JSX namespace는 이제 전역이 아니라 react 모듈 내부에 있으므로, 손으로 주석을 작성하는 것은 이를 임포트하는 것을 의미합니다.)
이 추론을 그냥 두어도 되며, 많은 코드베이스가 그렇게 합니다. 반환 타입을 주석 다는 것은 엄격함에 대한 선택입니다. 컴포넌트가 항상 단일 요소를 반환한다는 것을 보장합니다. 어떤 팀은 더 큰 코드베이스에서 이를 중요하게 생각합니다. 그것은 React가 하는 약속보다 더 엄격합니다. 컴포넌트는 합법적으로 문자열, 숫자, 배열, 또는 null을 반환할 수 있기 때문입니다. 그리고 단순히 JSX.Element 주석은 이것을 넓혀서 그것들을 허용할 때까지 모두 거절합니다.
import { type JSX } from 'react'
function Header(): JSX.Element {
return <h1>Assembly: Endgame</h1>
}
function ConfettiContainer({ isGameWon }: { isGameWon: boolean }): JSX.Element | null {
if (!isGameWon) return null
return <Confetti />
}ConfettiContainer는 때때로 null을 반환함으로써 아무것도 렌더링하지 않습니다. 이것은 조건부 렌더링에서 사용된 같은 방식입니다. 따라서 주석 달린 반환 타입은 union JSX.Element | null입니다. 같은 습관은 컴포넌트 내부에서 파생된 값으로 확장됩니다. 화살표 함수의 반환 타입은 그 매개변수 괄호 뒤에 붙으며, JSX를 생성하는 데이터 위의 map은 JSX.Element[]를 생성합니다. 이 주석의 대부분은 추론이 이미 결론 지은 것을 다시 말할 뿐입니다. 일관되게 보상하는 것들은 경계에 앉습니다. props, 빈 또는 nullable 상태, 그리고 다른 컴포넌트가 의존하는 서명을 가진 함수입니다.
공유 타입 임포트하기
타입은 다른 바인딩처럼 내보내고 임포트되며, 이것은 한 정의가 전체 앱을 처리하도록 합니다. 데이터 모듈은 일반적으로 데이터와 함께 그 형태를 내보냅니다.
// languages.ts
export type Language = {
name: string
backgroundColor: string
color: string
}
// LanguageChips.tsx
import { type Language } from './languages'
type LanguageChipsProps = {
languages: Language[]
}임포트의 type 키워드는 그것을 타입 전용으로 표시하므로 컴파일된 JavaScript에서 완전히 사라집니다. Language같은 타입이 한 곳에 살면, 그 데이터를 건드리는 모든 컴포넌트는 같은 계약을 임포트합니다. 한 파일에서 형태를 변경하면 업데이트가 필요한 모든 호출 부분에서 표면화됩니다.
실제 시작값으로 생성된 상태는 자신의 라벨을 붙이며, 시작값이 빈 값이거나 null이면 useState<string[]>([])처럼 손으로 라벨을 작성합니다. Props는 각 prop과 그 타입을 나열하는 GameStatusProps같은 작은 명명된 타입을 얻습니다.
라벨이 붙으면 편집기는 앱을 실행하기 전에 잘못된 형태의 뭔가가 전달되는 순간 경고합니다.
이것이 핸드북입니다. 마크업을 반환하는 함수로 시작한 것은 이제 완전한 툴킷입니다. 컴포넌트와 상태, effect와 데이터, 재사용 가능한 컴포넌트 패턴, 라우팅, 성능에 대해 생각할 렌더링 모델, 그리고 누구든 다른 사람을 만나기 전에 경계에서 실수를 포착하는 타입 레이어입니다. 실제로 존재하기를 원하는 프로젝트를 고르고, npm create vite@latest로 스캐폴드하고, 프로젝트가 요청하기 시작할 때 이 장들을 다시 엽니다.

