title: TypeScript 사용하기
TypeScript는 JavaScript 코드베이스에 타입 정의를 추가하는 인기 있는 방법이에요. TypeScript는 기본적으로 JSX를 지원하고, 프로젝트에 @types/react와 @types/react-dom을 추가하면 완전한 React Web 지원을 받을 수 있어요.
모든 프로덕션급 React 프레임워크는 TypeScript 사용을 지원해요. 설치를 위해 프레임워크별 가이드를 따라주세요:
React의 타입 정의 최신 버전을 설치하려면:
npm install --save-dev @types/react @types/react-domtsconfig.json에서 다음 컴파일러 옵션들을 설정해야 해요:
lib에 dom이 포함되어야 해요 (참고: lib 옵션이 지정되지 않으면 dom이 기본적으로 포함돼요).jsx는 유효한 옵션 중 하나로 설정해야 해요. 대부분의 애플리케이션에서는 preserve면 충분해요.jsx 문서를 참고하세요.JSX를 포함하는 모든 파일은 .tsx 파일 확장자를 사용해야 해요. 이건 TypeScript 전용 확장자로, 이 파일에 JSX가 포함되어 있다는 것을 TypeScript에게 알려줘요.
React와 함께 TypeScript를 작성하는 건 JavaScript와 함께 React를 작성하는 것과 매우 비슷해요. 컴포넌트를 작업할 때 핵심적인 차이점은 컴포넌트의 props에 타입을 제공할 수 있다는 거예요. 이 타입들은 정확성 검사와 에디터에서 인라인 문서를 제공하는 데 사용될 수 있어요.
빠른 시작 가이드의 MyButton 컴포넌트를 가져와서, 버튼의 title을 설명하는 타입을 추가할 수 있어요:
// App.tsx
function MyButton({ title }: { title: string }) {
return (
<button>{title}</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton title="I'm a button" />
</div>
);
}
import AppTSX from "./App.tsx";
export default App = AppTSX;
이 샌드박스들은 TypeScript 코드를 처리할 수 있지만, 타입 체커를 실행하지는 않아요. 이건 학습을 위해 TypeScript 샌드박스를 수정할 수 있지만, 타입 에러나 경고를 받지 않는다는 뜻이에요. 타입 체킹을 받으려면 TypeScript Playground를 사용하거나 더 완전한 기능을 갖춘 온라인 샌드박스를 사용할 수 있어요.
이 인라인 구문은 컴포넌트에 타입을 제공하는 가장 간단한 방법이지만, 설명해야 할 필드가 몇 개 생기면 다루기 어려워질 수 있어요. 대신, 컴포넌트의 props를 설명하기 위해 interface나 type을 사용할 수 있어요:
// App.tsx
interface MyButtonProps {
/** 버튼 안에 표시할 텍스트 */
title: string;
/** 버튼과 상호작용할 수 있는지 여부 */
disabled: boolean;
}
function MyButton({ title, disabled }: MyButtonProps) {
return (
<button disabled={disabled}>{title}</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton title="I'm a disabled button" disabled={true}/>
</div>
);
}
import AppTSX from "./App.tsx";
export default App = AppTSX;
컴포넌트의 props를 설명하는 타입은 필요에 따라 간단하거나 복잡할 수 있지만, type이나 interface로 설명되는 객체 타입이어야 해요. TypeScript가 객체를 설명하는 방법에 대해서는 Object Types에서 배울 수 있고, 여러 다른 타입 중 하나가 될 수 있는 prop을 설명하기 위한 Union Types와 더 고급 사용 사례를 위한 Creating Types from Types 가이드에도 관심이 있을 수 있어요.
@types/react의 타입 정의에는 내장 Hooks에 대한 타입이 포함되어 있어서, 추가 설정 없이 컴포넌트에서 사용할 수 있어요. 컴포넌트에서 작성하는 코드를 고려하도록 만들어져 있어서, 대부분의 경우 추론된 타입을 얻게 되고, 이상적으로는 타입을 제공하는 세부 사항을 처리할 필요가 없어요.
(쉽게 말해서, TypeScript가 알아서 타입을 추론해주기 때문에 매번 타입을 명시하지 않아도 된다는 뜻이에요!)
하지만, Hooks에 타입을 제공하는 방법에 대한 몇 가지 예시를 살펴볼 수 있어요.
useState {/typing-usestate/}useState Hook은 초기 state로 전달된 값을 재사용해서 값의 타입이 무엇이어야 하는지 결정해요. 예를 들어:
// 타입을 "boolean"으로 추론해요
const [enabled, setEnabled] = useState(false);
이건 enabled에 boolean 타입을 할당하고, setEnabled는 boolean 인자 또는 boolean을 반환하는 함수를 받는 함수가 돼요. state에 명시적으로 타입을 제공하고 싶다면, useState 호출에 타입 인자를 제공해서 할 수 있어요:
// 명시적으로 타입을 "boolean"으로 설정해요
const [enabled, setEnabled] = useState<boolean>(false);
이 경우에는 그다지 유용하지 않지만, 유니온 타입이 있을 때 타입을 제공하고 싶을 수 있는 일반적인 경우가 있어요. 예를 들어, 여기서 status는 몇 가지 다른 문자열 중 하나가 될 수 있어요:
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
또는, state 구조화 원칙에서 권장하는 것처럼, 관련된 state를 객체로 그룹화하고 객체 타입을 통해 다양한 가능성을 설명할 수 있어요:
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success', data: any }
| { status: 'error', error: Error };
const [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });
(이 패턴은 "판별 유니온(discriminated union)"이라고 불리는데, status 필드를 통해 현재 어떤 상태인지 구분할 수 있어서 매우 유용해요. 예를 들어 status가 'success'일 때만 data에 접근할 수 있다는 걸 TypeScript가 알게 돼요.)
useReducer {/typing-usereducer/}useReducer Hook은 reducer 함수와 초기 state를 받는 더 복잡한 Hook이에요. reducer 함수의 타입은 초기 state에서 추론돼요. useReducer 호출에 타입 인자를 제공해서 state에 타입을 제공할 수도 있지만, 대신 초기 state에 타입을 설정하는 것이 더 나은 경우가 많아요:
// App.tsx
import {useReducer} from 'react';
interface State {
count: number
};
type CounterAction =
| { type: "reset" }
| { type: "setCount"; value: State["count"] }
const initialState: State = { count: 0 };
function stateReducer(state: State, action: CounterAction): State {
switch (action.type) {
case "reset":
return initialState;
case "setCount":
return { ...state, count: action.value };
default:
throw new Error("Unknown action");
}
}
export default function App() {
const [state, dispatch] = useReducer(stateReducer, initialState);
const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
const reset = () => dispatch({ type: "reset" });
return (
<div>
<h1>Welcome to my counter</h1>
<p>Count: {state.count}</p>
<button onClick={addFive}>Add 5</button>
<button onClick={reset}>Reset</button>
</div>
);
}
import AppTSX from "./App.tsx";
export default App = AppTSX;
몇 가지 핵심적인 곳에서 TypeScript를 사용하고 있어요:
interface State는 reducer state의 형태를 설명해요.type CounterAction은 reducer에 dispatch될 수 있는 다양한 액션들을 설명해요.const initialState: State는 초기 state에 타입을 제공하고, useReducer가 기본적으로 사용하는 타입이기도 해요.stateReducer(state: State, action: CounterAction): State는 reducer 함수의 인자와 반환 값에 타입을 설정해요.initialState에 타입을 설정하는 것의 더 명시적인 대안은 useReducer에 타입 인자를 제공하는 거예요:
import { stateReducer, State } from './your-reducer-implementation';
const initialState = { count: 0 };
export default function App() {
const [state, dispatch] = useReducer<State>(stateReducer, initialState);
}
useContext {/typing-usecontext/}useContext Hook은 컴포넌트를 통해 props를 전달하지 않고도 컴포넌트 트리 아래로 데이터를 전달하는 기술이에요. provider 컴포넌트를 만들고, 종종 자식 컴포넌트에서 값을 소비하기 위한 Hook을 만들어서 사용해요.
context가 제공하는 값의 타입은 createContext 호출에 전달된 값에서 추론돼요:
// App.tsx
import { createContext, useContext, useState } from 'react';
type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");
const useGetTheme = () => useContext(ThemeContext);
export default function MyApp() {
const [theme, setTheme] = useState<Theme>('light');
return (
<ThemeContext value={theme}>
<MyComponent />
</ThemeContext>
)
}
function MyComponent() {
const theme = useGetTheme();
return (
<div>
<p>Current theme: {theme}</p>
</div>
)
}
import AppTSX from "./App.tsx";
export default App = AppTSX;
이 기술은 말이 되는 기본값이 있을 때 작동하지만, 기본값이 없는 경우도 가끔 있고, 그런 경우에는 null이 합리적인 기본값으로 느껴질 수 있어요. 하지만, 타입 시스템이 코드를 이해하도록 하려면, createContext에 명시적으로 ContextShape | null을 설정해야 해요.
이건 context 소비자의 타입에서 | null을 제거해야 하는 문제를 일으켜요. 우리의 권장 사항은 Hook이 존재 여부에 대한 런타임 검사를 하고, 존재하지 않을 때 에러를 던지도록 하는 거예요:
import { createContext, useContext, useState, useMemo } from 'react';
// 이건 더 간단한 예시지만, 여기에 더 복잡한 객체를 상상할 수 있어요
type ComplexObject = {
kind: string
};
// 기본값을 정확하게 반영하기 위해 타입에 `| null`로 context가 생성돼요.
const Context = createContext<ComplexObject | null>(null);
// Hook의 검사를 통해 `| null`이 제거돼요.
const useGetComplexObject = () => {
const object = useContext(Context);
if (!object) { throw new Error("useGetComplexObject must be used within a Provider") }
return object;
}
export default function MyApp() {
const object = useMemo(() => ({ kind: "complex" }), []);
return (
<Context value={object}>
<MyComponent />
</Context>
)
}
function MyComponent() {
const object = useGetComplexObject();
return (
<div>
<p>Current object: {object.kind}</p>
</div>
)
}
(이 패턴은 매우 유용해요! Hook 내부에서 null 체크를 해주기 때문에, Hook을 사용하는 컴포넌트에서는 null 체크를 하지 않아도 돼요. Provider 밖에서 사용하면 명확한 에러 메시지를 받게 되어 디버깅도 쉬워져요.)
useMemo {/typing-usememo/}React Compiler는 자동으로 값과 함수를 메모이제이션해서, 수동 useMemo 호출의 필요성을 줄여줘요. 컴파일러를 사용해서 메모이제이션을 자동으로 처리할 수 있어요.
useMemo Hook은 함수 호출로부터 메모이제이션된 값을 생성/재접근하고, 두 번째 매개변수로 전달된 의존성이 변경될 때만 함수를 다시 실행해요. Hook 호출의 결과는 첫 번째 매개변수의 함수 반환 값에서 추론돼요. Hook에 타입 인자를 제공해서 더 명시적으로 할 수 있어요.
// visibleTodos의 타입은 filterTodos의 반환 값에서 추론돼요
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
useCallback {/typing-usecallback/}React Compiler는 자동으로 값과 함수를 메모이제이션해서, 수동 useCallback 호출의 필요성을 줄여줘요. 컴파일러를 사용해서 메모이제이션을 자동으로 처리할 수 있어요.
useCallback은 두 번째 매개변수로 전달된 의존성이 같은 한 함수에 대한 안정적인 참조를 제공해요. useMemo처럼, 함수의 타입은 첫 번째 매개변수의 함수 반환 값에서 추론되고, Hook에 타입 인자를 제공해서 더 명시적으로 할 수 있어요.
const handleClick = useCallback(() => {
// ...
}, [todos]);
TypeScript strict 모드에서 작업할 때 useCallback은 콜백의 매개변수에 타입을 추가해야 해요. 콜백의 타입은 함수의 반환 값에서 추론되고, 매개변수 없이는 타입을 완전히 이해할 수 없기 때문이에요.
코드 스타일 선호도에 따라, 콜백을 정의하는 동시에 이벤트 핸들러의 타입을 제공하기 위해 React 타입의 *EventHandler 함수들을 사용할 수 있어요:
import { useState, useCallback } from 'react';
export default function Form() {
const [value, setValue] = useState("Change me");
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
setValue(event.currentTarget.value);
}, [setValue])
return (
<>
<input value={value} onChange={handleChange} />
<p>Value: {value}</p>
</>
);
}
@types/react 패키지에서 제공되는 꽤 방대한 타입 세트가 있어요. React와 TypeScript가 어떻게 상호작용하는지 익숙해지면 읽어볼 만해요. DefinitelyTyped의 React 폴더에서 찾을 수 있어요. 여기서 더 일반적인 타입 몇 가지를 다룰게요.
React에서 DOM 이벤트로 작업할 때, 이벤트의 타입은 종종 이벤트 핸들러에서 추론될 수 있어요. 하지만, 이벤트 핸들러에 전달할 함수를 추출하고 싶을 때는 이벤트의 타입을 명시적으로 설정해야 해요.
// App.tsx
import { useState } from 'react';
export default function Form() {
const [value, setValue] = useState("Change me");
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.currentTarget.value);
}
return (
<>
<input value={value} onChange={handleChange} />
<p>Value: {value}</p>
</>
);
}
import AppTSX from "./App.tsx";
export default App = AppTSX;
React 타입에는 많은 종류의 이벤트가 제공돼요 - 전체 목록은 여기에서 찾을 수 있고, DOM에서 가장 인기 있는 이벤트들을 기반으로 해요.
찾고 있는 타입을 결정할 때, 먼저 사용 중인 이벤트 핸들러의 호버 정보를 볼 수 있어요. 그러면 이벤트의 타입이 표시돼요.
이 목록에 포함되지 않은 이벤트를 사용해야 하는 경우, 모든 이벤트의 기본 타입인 React.SyntheticEvent 타입을 사용할 수 있어요.
컴포넌트의 children을 설명하는 두 가지 일반적인 방법이 있어요. 첫 번째는 JSX에서 children으로 전달될 수 있는 모든 가능한 타입의 유니온인 React.ReactNode 타입을 사용하는 거예요:
interface ModalRendererProps {
title: string;
children: React.ReactNode;
}
이건 children의 매우 넓은 정의예요. 두 번째는 문자열이나 숫자 같은 JavaScript 기본값이 아닌 JSX 요소만인 React.ReactElement 타입을 사용하는 거예요:
interface ModalRendererProps {
title: string;
children: React.ReactElement;
}
참고로, TypeScript를 사용해서 children이 특정 타입의 JSX 요소라고 설명할 수는 없어요. 그래서 타입 시스템을 사용해서 <li> children만 받는 컴포넌트를 설명할 수 없어요.
이 TypeScript playground에서 타입 체커와 함께 React.ReactNode와 React.ReactElement 둘 다의 예시를 볼 수 있어요.
React에서 인라인 스타일을 사용할 때, React.CSSProperties를 사용해서 style prop에 전달되는 객체를 설명할 수 있어요. 이 타입은 모든 가능한 CSS 속성의 유니온이고, style prop에 유효한 CSS 속성을 전달하는지 확인하고 에디터에서 자동 완성을 받는 좋은 방법이에요.
interface MyComponentProps {
style: React.CSSProperties;
}
이 가이드는 React와 함께 TypeScript를 사용하는 기본을 다뤘지만, 배울 것이 훨씬 많아요.
문서의 개별 API 페이지에는 TypeScript와 함께 사용하는 방법에 대한 더 심층적인 문서가 포함될 수 있어요.
다음 리소스들을 추천해요:
The TypeScript handbook은 TypeScript의 공식 문서이고, 대부분의 주요 언어 기능을 다뤄요.
The TypeScript release notes는 새로운 기능을 심층적으로 다뤄요.
React TypeScript Cheatsheet는 React와 함께 TypeScript를 사용하기 위한 커뮤니티 관리 치트시트로, 많은 유용한 엣지 케이스를 다루고 이 문서보다 더 넓은 범위를 제공해요.
TypeScript Community Discord는 TypeScript와 React 문제에 대해 질문하고 도움을 받기 좋은 곳이에요.