마지막 주제는 우리가 작성하는 React 코드를 더욱 견고하고 예측 가능하게 만들어주는 강력한 도구,
TypeScript
를 React 프로젝트에서 어떻게 더 잘 활용할 수 있는지에 대한 유용한 팁들을 나누려고 합니다.
TypeScript는 JavaScript에 정적 타입을 더함으로써, 코드를 작성하는 단계(컴파일 타임)에서 미리 오류를 발견하고, 코드 자동 완성과 리팩토링을 용이하게 하여 개발 생산성을 크게 향상시켜 줍니다.
React와 TypeScript를 함께 사용하는 것은 이제 매우 일반적인 선택이 되었죠.
하지만 단순히 타입을 지정하는 것을 넘어, 제네릭(Generics)을 활용하여 재사용 가능한 컴포넌트를 만들거나, 커스텀 훅의 타입을 효과적으로 추론하게 하거나, 폼 처리 시 타입 안전성을 높이는 등 TypeScript의 장점을 극대화하는 방법들이 있습니다.
오늘은 바로 이런 고급 활용 팁들을 중심으로, 여러분의 TypeScript + React 개발 경험을 한 단계 끌어올리는 데 도움을 드리고자 합니다.
컴포넌트를 만들다 보면, 다양한 타입의 데이터를 처리해야 하는 경우가 있습니다.
예를 들어, 숫자 배열을 받아서 목록을 보여주는 컴포넌트와 문자열 배열을 받아서 목록을 보여주는 컴포넌트를 따로 만들어야 할까요?
이럴 때 *제네릭(Generics)*을 사용하면, 타입을 마치 변수처럼 사용하여 다양한 타입을 지원하는 재사용 가능한 컴포넌트를 만들 수 있습니다.
에 타입을 결정할 수 있도록 하는 기능입니다.
마치 함수에 매개변수를 전달하듯, 타입 변수(보통 T, U, K 등으로 표시)를 사용하여 타입을 전달받을 수 있습니다.
// 제네릭 함수 예시: 어떤 타입의 값이든 받아서 그대로 반환function identity<T>(arg: T): T { return arg;}let output1 = identity<string>("hello"); // T는 string 타입으로 결정됨let output2 = identity<number>(123); // T는 number 타입으로 결정됨let output3 = identity("auto"); // 타입 인자 생략 시 추론됨 (string)
이제 이 제네릭 개념을 React 함수형 컴포넌트에 적용해 봅시다.
다양한 타입의 아이템 배열을 받아 목록으로 렌더링하는 List 컴포넌트를 만들어 보겠습니다.
import React from 'react';// 제네릭 Props 타입 정의// T라는 타입 변수를 받음 (사용 시 결정될 아이템의 타입)interface ListProps<T> { items: T[]; // items 배열은 T 타입의 요소들로 구성 renderItem: (item: T, index: number) => React.ReactNode; // 각 아이템을 렌더링하는 함수 // keyExtractor?: (item: T) => string | number; // key 추출 함수 (선택적)}// 제네릭 함수형 컴포넌트 정의// <T,> 처럼 쉼표를 찍는 이유는 JSX 태그와 구분하기 위함 (또는 <T extends {}> 사용)function List<T>({ items, renderItem }: ListProps<T>): React.ReactElement { return ( <ul> {items.map((item, index) => ( // key prop은 실제 사용 시 renderItem 내부나 여기서 적절히 제공해야 함 // 예: <li key={keyExtractor ? keyExtractor(item) : index}> <React.Fragment key={index}> {/* key는 실제 상황에 맞게 수정 필요 */} {renderItem(item, index)} </React.Fragment> ))} </ul> );}// --- 컴포넌트 사용 예시 ---interface User { id: number; name: string; }const users: User[] = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ];interface Product { sku: string; title: string; price: number; }const products: Product[] = [ { sku: 'P001', title: '노트북', price: 1500000 }, { sku: 'P002', title: '키보드', price: 80000 },];function App() { return ( <div> <h2>사용자 목록</h2> {/* T를 User 타입으로 지정하여 사용 */} <List<User> items={users} renderItem={(user) => <li key={user.id}>{user.name}</li>} /> <h2>상품 목록</h2> {/* T를 Product 타입으로 지정하여 사용 */} <List<Product> items={products} renderItem={(product) => ( <li key={product.sku}> {product.title} - {product.price.toLocaleString()}원 </li> )} /> </div> );}export default App; // List 컴포넌트 export 필요 시 추가
위 List 컴포넌트는 제네릭 타입 T를 사용하여 어떤 타입의 배열(items: T[])이든 처리할 수 있습니다.
renderItem prop 역시 T 타입을 인자로 받으므로, 각 타입에 맞는 렌더링 로직을 외부에서 주입할 수 있습니다.
이를 통해 User 목록과 Product 목록을 별도의 컴포넌트 없이 하나의 List 컴포넌트로 처리할 수 있게 되어 코드 재사용성이 크게 향상됩니다.
과거에는 React.FC<ListProps<T>> 와 같이 React.FC를 사용하는 경우도 많았지만, 최근에는 React.FC 사용 시 몇 가지 단점(children prop 암시적 포함 등) 때문에 함수 시그니처에 직접 타입을 명시하는 방식(위 예시처럼)이 더 선호되는 경향이 있습니다.
TypeScript는 함수 본문에서 반환되는 값을 분석하여 자동으로 반환 타입을 추론하는 능력이 뛰어납니다.
반환 타입이 복잡하지 않거나 명확하다면, 굳이 명시적으로 타입을 적지 않고 타입 추론에 맡기는 것이 코드를 더 간결하게 만들 수 있습니다.
(단, 라이브러리로 만들거나 복잡한 경우에는 명시하는 것이 좋을 수 있습니다.)
// 반환 타입 명시 생략 (TypeScript가 추론하도록 함)function useToggle(initialValue: boolean = false) { // 기본값 설정 가능 const [value, setValue] = useState(initialValue); const toggle = () => setValue(prev => !prev); // TypeScript는 반환 값을 보고 [boolean, () => void] 타입으로 추론함 return [value, toggle] as const; // 'as const' 추가: 튜플 타입을 더 정확하게 추론하고 readonly로 만듦}function ToggleComponent() { const [isOn, toggleIsOn] = useToggle(false); // isOn은 boolean, toggleIsOn은 () => void 타입으로 잘 추론됨 // ...}
as const를 사용하면 반환되는 배열을 읽기 전용 튜플(readonly tuple) 타입으로 더 정확하게 추론하여, 배열 구조 분해 할당 시 각 요소의 타입을 고정시키는 데 도움이 됩니다.
React에서 폼(Form)을 다룰 때 입력 값의 유효성을 검사하고, 폼 데이터의 타입을 안전하게 관리하는 것은 매우 중요하면서도 번거로운 작업입니다.
특히 사용자가 입력하는 값은 기본적으로 문자열인 경우가 많고, 이를 숫자나 다른 타입으로 변환하거나 특정 형식(이메일, URL 등)을 검증해야 할 때 타입 관련 오류가 발생하기 쉽습니다.
이럴 때 스키마 기반 유효성 검사 라이브러리인
오늘은 TypeScript와 React를 함께 사용할 때 개발 경험과 코드 품질을 향상시키는 몇 가지 유용한 팁과 패턴을 살펴보았습니다.
제네릭(Generics)
을 활용하면 타입을 변수처럼 사용하여 다양한 타입을 처리하는 재사용 가능하고 타입 안전한 컴포넌트를 만들 수 있습니다.
커스텀 훅
작성 시 TypeScript의 타입 추론 기능을 활용하고, 필요하다면 제네릭을 사용하여 유연성과 안정성을 모두 확보할 수 있습니다.
Zod나 Yup
같은 스키마 기반 유효성 검사 라이브러리를 사용하면, 폼 데이터의 유효성 검사와 타입 정의를 통합하여 관리하고, React Hook Form과 연동하여
폼 처리의 타입 안전성
을 크게 높일 수 있습니다.
TypeScript는 단순히 오류를 방지하는 것을 넘어, 코드의 의도를 명확히 하고, 리팩토링을 용이하게 하며, 더 나은 설계를 유도하는 강력한 도구입니다.
React와 함께 TypeScript의 다양한 기능들을 적극적으로 활용하여 더 견고하고 유지보수하기 좋은 애플리케이션을 만들어나가시길 바랍니다.
👨💻
이것으로 "React, 알고 쓰자" 시리즈의 마지막 주제까지 모두 다루었습니다. 그동안 함께해주셔서 감사합니다! 이 시리즈가 여러분의 React 개발 여정에 든든한 길잡이가 되었기를 바랍니다.