Friday, July 27, 2018

타입스크립트와 리액트, 더 안정적인 애플리케이션을 향한 현명한 선택

현대 웹 및 앱 개발 생태계는 눈부신 속도로 발전하고 있습니다. 수많은 라이브러리와 프레임워크 속에서 개발자들은 더 나은 생산성, 유지보수성, 그리고 안정성을 갖춘 기술 스택을 끊임없이 탐색합니다. 이러한 흐름의 중심에 자바스크립트의 상위 집합(Superset)인 타입스크립트(TypeScript)가 자리 잡고 있습니다. 타입스크립트는 정적 타입을 지원함으로써 컴파일 단계에서 오류를 사전에 포착하고, 코드 자동 완성과 같은 강력한 개발자 도구 경험을 제공하여 대규모 애플리케이션 개발의 복잡성을 크게 낮춰줍니다.

특히 UI 라이브러리의 선두주자인 리액트(React)와 크로스플랫폼 모바일 개발 프레임워크인 리액트 네이티브(React Native)와 타입스크립트의 조합은 이제 선택이 아닌 필수로 여겨지고 있습니다. 컴포넌트 기반 아키텍처를 지향하는 리액트 생태계에서, 각 컴포넌트가 주고받는 props의 타입을 명확히 정의하는 것은 예기치 않은 버그를 방지하고 협업의 효율성을 극대화하는 핵심 요소입니다. 이 글에서는 새로운 리액트 및 리액트 네이티브 프로젝트를 시작할 때, 어떻게 타입스크립트를 가장 최신의 안정적인 방법으로 설정하고, 그 구조 속에서 효율적으로 개발을 시작할 수 있는지에 대한 심층적인 가이드를 제공하고자 합니다.

1. 리액트(React) 웹 프로젝트에 타입스크립트 적용하기

과거에는 리액트 프로젝트에 타입스크립트를 적용하기 위해 `react-scripts-ts`와 같은 별도의 스크립트 버전을 사용하는 방식이 존재했지만, 이 방법은 현재 공식적으로 지원 중단(deprecated)되었습니다. 이제는 Create React App(CRA)에서 공식적으로 타입스크립트 템플릿을 지원하므로 훨씬 간결하고 표준화된 방법으로 프로젝트를 시작할 수 있습니다.

1.1. 최신 CRA를 이용한 타입스크립트 프로젝트 생성

터미널 또는 명령 프롬프트를 열고 다음 명령어를 입력하여 타입스크립트가 기본으로 설정된 새로운 리액트 프로젝트를 생성합니다. `npx`는 별도의 패키지 설치 없이 최신 버전의 `create-react-app`을 실행해주는 유용한 도구입니다.


npx create-react-app my-react-ts-app --template typescript

위 명령어는 `my-react-ts-app`이라는 이름의 디렉토리를 생성하고, 그 안에 타입스크립트 개발에 필요한 모든 설정과 기본 파일들을 자동으로 구성해줍니다. 명령어 실행이 완료되면 다음과 같은 안내에 따라 프로젝트 디렉토리로 이동하고 개발 서버를 시작할 수 있습니다.


cd my-react-ts-app
npm start

이제 브라우저에서 `http://localhost:3000`으로 접속하면 타입스크립트로 작성된 기본 리액트 앱이 실행되는 것을 확인할 수 있습니다.

1.2. 생성된 프로젝트 구조 심층 분석

타입스크립트 템플릿으로 생성된 프로젝트는 일반 자바스크립트 프로젝트와 몇 가지 중요한 차이점을 보입니다. 핵심적인 파일들을 살펴보겠습니다.

  • tsconfig.json: 이 파일은 타입스크립트 컴파일러의 설정을 정의하는 핵심 파일입니다. CRA가 리액트 개발에 최적화된 기본 설정을 제공합니다. 예를 들어, `jsx` 옵션을 통해 JSX 문법을 어떻게 처리할지, `target` 옵션을 통해 어떤 버전의 ECMAScript로 컴파일할지 등을 지정합니다. 개발자는 필요에 따라 이 파일을 수정하여 컴파일러 옵션을 조정할 수 있습니다.
    
    // tsconfig.json 예시 (일부)
    {
      "compilerOptions": {
        "target": "es5", // 컴파일 결과물 ECMAScript 버전
        "lib": [
          "dom",
          "dom.iterable",
          "esnext"
        ],
        "allowJs": true, // .js 파일 임포트 허용 여부
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true, // 엄격한 타입 검사 모드 활성화
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true, // 타입 검사만 하고 실제 파일은 생성하지 않음 (Babel이 처리)
        "jsx": "react-jsx" // JSX 처리 방식
      },
      "include": [
        "src" // 타입스크립트 컴파일 대상 디렉토리
      ]
    }
            
  • src/App.tsx: 기존의 `App.js` 대신 `.tsx` 확장자를 사용합니다. `.tsx`는 타입스크립트 파일 내에서 JSX 문법을 사용할 수 있음을 의미합니다. 파일을 열어보면 함수 컴포넌트가 타입스크립트 문법으로 작성되어 있는 것을 볼 수 있습니다.
  • src/react-app-env.d.ts: 이 파일은 타입스크립트 선언(Declaration) 파일입니다. CRA 환경에서 SVG나 CSS 모듈과 같은 비-자바스크립트 에셋들을 타입스크립트가 인식할 수 있도록 참조 타입을 정의해줍니다. 개발자가 직접 수정할 일은 거의 없지만, 프로젝트 전역에서 필요한 타입 선언이 있을 때 `.d.ts` 파일을 활용할 수 있습니다.

1.3. 타입스크립트로 리액트 컴포넌트 작성하기: 실전 예제

타입스크립트의 가장 큰 장점은 컴포넌트의 props와 state에 명확한 타입을 부여하는 것입니다. 간단한 `UserProfile` 컴포넌트를 만들어보겠습니다.

Props 타입 정의

먼저, 컴포넌트가 받을 props의 타입을 `interface` 또는 `type` 키워드를 사용하여 정의합니다.


// src/components/UserProfile.tsx

import React from 'react';

// UserProfile 컴포넌트가 받을 props의 타입을 정의합니다.
interface UserProfileProps {
  name: string;
  email: string;
  age: number;
  isMember?: boolean; // '?'는 optional prop을 의미합니다.
}

const UserProfile: React.FC<UserProfileProps> = ({ name, email, age, isMember }) => {
  return (
    <div style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
      <h2>{name}</h2>
      <p>Email: {email}</p>
      <p>Age: {age}</p>
      {isMember && <p>Status: Premium Member</p>}
    </div>
  );
};

export default UserProfile;

위 코드에서 `React.FC`는 함수 컴포넌트(Function Component)의 타입을 나타냅니다. 제네릭(``)을 사용하여 이 컴포넌트의 props가 `UserProfileProps` 인터페이스를 준수해야 함을 명시합니다. 이렇게 하면 `UserProfile` 컴포넌트를 사용할 때 `name`, `email`, `age` prop을 전달하지 않거나 잘못된 타입(예: `age`에 문자열 전달)을 전달하면 개발 환경에서 즉시 오류를 표시해줍니다.

State 타입 정의

`useState` 훅을 사용할 때도 타입을 지정할 수 있습니다. 타입스크립트는 초기값을 기반으로 타입을 추론하지만, 초기값이 `null`이거나 더 복잡한 타입일 경우 명시적으로 지정하는 것이 좋습니다.


import React, { useState } from 'react';

type User = {
    id: number;
    name: string;
}

const UserLoader: React.FC = () => {
    // useState의 제네릭을 사용하여 state의 타입을 명시합니다.
    // User 타입 또는 null 값을 가질 수 있습니다.
    const [user, setUser] = useState<User | null>(null);
    const [isLoading, setIsLoading] = useState<boolean>(false);

    const fetchUser = () => {
        setIsLoading(true);
        setTimeout(() => {
            setUser({ id: 1, name: 'John Doe' });
            setIsLoading(false);
        }, 1000);
    }

    if (isLoading) {
        return <div>Loading...</div>;
    }
    
    return (
        <div>
            {user ? (
                <p>Welcome, {user.name}</p>
            ) : (
                <button onClick={fetchUser}>Load User</button>
            )}
        </div>
    )
}

`useState<User | null>(null)`와 같이 제네릭을 사용하면, `user` 상태는 `User` 타입의 객체이거나 `null`일 수 있음을 명확히 합니다. 덕분에 `setUser` 함수에 다른 타입의 값(예: `setUser(123)`)을 전달하려고 하면 타입 에러가 발생합니다.

2. 리액트 네이티브(React Native)에 타입스크립트 적용하기

리액트 네이티브 프로젝트 역시 타입스크립트를 손쉽게 적용할 수 있습니다. 과거 `create-react-native-app`에 스크립트 버전을 지정하던 방식은 더 이상 사용되지 않으며, 현재는 리액트 네이티브의 기본 CLI가 타입스크립트 템플릿을 공식 지원합니다.

2.1. React Native CLI를 이용한 타입스크립트 프로젝트 생성

React Native 개발 환경이 이미 설정되어 있다고 가정합니다. (Node.js, Watchman, Xcode/Android Studio 등) 터미널에서 다음 명령어를 실행하여 타입스크립트 템플릿을 기반으로 한 새 프로젝트를 생성합니다.


npx react-native init MyRNTsApp --template react-native-template-typescript

이 명령어는 `MyRNTsApp`이라는 프로젝트를 생성하며, 리액트 네이티브 개발에 필요한 모든 파일과 함께 타입스크립트 관련 설정(`tsconfig.json`, 기본 `.tsx` 파일 등)을 자동으로 포함시킵니다. 웹 리액트 프로젝트와 마찬가지로, 이는 개발 초기 단계의 복잡한 설정 과정을 생략하고 바로 코드 작성에 집중할 수 있게 해줍니다.

2.2. 리액트 네이티브 프로젝트 구조와 특징

리액트 네이티브의 타입스크립트 프로젝트는 웹 프로젝트와 유사한 `tsconfig.json`, `.tsx` 파일들을 가지지만, 네이티브 개발을 위한 `ios`, `android` 디렉토리가 존재한다는 큰 차이점이 있습니다.

  • ios/ 및 android/: 각각 iOS와 안드로이드 플랫폼의 네이티브 프로젝트 파일들을 담고 있는 디렉토리입니다. 대부분의 자바스크립트/타입스크립트 개발 과정에서는 직접 수정할 일이 적지만, 네이티브 모듈을 연동하거나 특정 빌드 설정을 변경해야 할 때 사용됩니다.
  • App.tsx: 웹의 `div`, `p` 태그 대신 리액트 네이티브의 Core Components(`View`, `Text` 등)를 사용하여 UI를 구성하는 최상위 컴포넌트입니다.

2.3. 타입스크립트로 리액트 네이티브 컴포넌트 작성하기

리액트 네이티브에서도 props와 state 타이핑 방식은 리액트 웹과 거의 동일합니다. 다만, 사용되는 컴포넌트와 스타일링 방식에 차이가 있습니다.

Props 및 Style 타입 정의

알림 메시지를 표시하는 간단한 `Notification` 컴포넌트를 만들어 보겠습니다.


// src/components/Notification.tsx

import React from 'react';
import { View, Text, StyleSheet, StyleProp, ViewStyle } from 'react-native';

type NotificationType = 'success' | 'error' | 'info';

interface NotificationProps {
  message: string;
  type: NotificationType;
  style?: StyleProp<ViewStyle>; // 외부에서 스타일을 주입받을 수 있도록 함
}

const Notification: React.FC<NotificationProps> = ({ message, type, style }) => {
  const containerStyle = [
    styles.container,
    styles[type], // 'success', 'error', 'info'에 따라 동적으로 스타일 적용
    style, // 외부에서 받은 스타일 적용
  ];

  return (
    <View style={containerStyle}>
      <Text style={styles.text}>{message}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 15,
    borderRadius: 8,
    marginVertical: 10,
  },
  text: {
    color: '#fff',
    fontSize: 16,
    textAlign: 'center',
  },
  success: {
    backgroundColor: '#4CAF50',
  },
  error: {
    backgroundColor: '#F44336',
  },
  info: {
    backgroundColor: '#2196F3',
  },
});

export default Notification;

이 예제에서는 몇 가지 중요한 리액트 네이티브 타입스크립트 패턴을 보여줍니다.

  • `type NotificationType = ...`: `type` 키워드를 사용하여 특정 문자열들의 조합인 유니온 타입을 정의했습니다. `type` prop은 오직 'success', 'error', 'info' 중 하나의 값만 가질 수 있어 실수를 방지합니다.
  • `StyleProp<ViewStyle>`: `StyleSheet.create`로 생성된 스타일 객체나 일반 스타일 객체를 prop으로 받기 위한 타입입니다. `View` 컴포넌트에 적용될 스타일이므로 `ViewStyle` 타입을 사용합니다. `Text`에는 `TextStyle`, `Image`에는 `ImageStyle`을 사용합니다.
  • `styles[type]`: 타입스크립트는 `styles` 객체의 키가 `container`, `text`, `success` 등임을 알고 있고, `type` 변수가 `NotificationType`임을 알고 있으므로, `styles[type]`과 같은 동적 속성 접근이 타입-안전(type-safe)하게 이루어집니다. 만약 `type`에 'warning'과 같이 정의되지 않은 값이 들어오면 에러가 발생합니다.

3. 기존 자바스크립트 프로젝트에 타입스크립트 점진적으로 도입하기

이미 진행 중인 대규모 자바스크립트 리액트 프로젝트가 있다면, 한 번에 모든 코드를 타입스크립트로 전환하는 것은 현실적으로 어렵고 위험 부담이 큽니다. 다행히 타입스크립트는 점진적인 도입(Incremental Adoption)을 훌륭하게 지원합니다.

단계별 도입 전략

  1. 필수 패키지 설치: 프로젝트 루트에서 타입스크립트와 리액트 관련 타입 정의 패키지를 설치합니다.
    
    npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest
    # 리액트 네이티브의 경우 @types/react-native 추가
    # npm install --save-dev @types/react-native
        
  2. `tsconfig.json` 파일 생성: 프로젝트 루트에 `tsconfig.json` 파일을 생성합니다. `npx tsc --init` 명령어를 사용해 기본 설정 파일을 생성한 후, 프로젝트에 맞게 수정할 수 있습니다. 특히 `"allowJs": true` 옵션을 반드시 활성화해야 합니다. 이 옵션은 타입스크립트 파일(` .ts`/`.tsx`)에서 자바스크립트 파일(`.js`/`.jsx`)을 임포트할 수 있게 해줍니다.
    
    // tsconfig.json 핵심 설정 예시
    {
      "compilerOptions": {
        "jsx": "react-jsx",
        "allowJs": true, // 매우 중요! JS 파일과 함께 사용 가능하게 함
        "esModuleInterop": true,
        "strict": true, // 점진적 도입 시 처음에는 false로 시작하여 점차 활성화하는 전략도 유효
        // ... 기타 설정들
      }
    }
        
  3. 점진적 파일 변환:
    • 변환 대상을 선정합니다. 재사용성이 높고, 프로젝트의 핵심적인 역할을 하지만, 의존성이 적은 작은 컴포넌트(예: `Button`, `Input`)부터 시작하는 것이 좋습니다.
    • 선정한 파일의 확장자를 `.js`에서 `.tsx`(JSX가 있다면) 또는 `.ts`(순수 로직이라면)로 변경합니다.
    • 확장자 변경 후, 에디터나 컴파일러가 표시하는 타입 오류들을 하나씩 수정해 나갑니다. Props 타입을 정의하고, state 타입을 명시하고, 이벤트 핸들러의 인자 타입을 지정하는 등의 작업을 수행합니다.
    • 이 과정을 반복하여 점진적으로 타입스크립트가 적용되는 코드의 범위를 넓혀갑니다.
  4. 서드파티 라이브러리 타입 정의: `lodash`, `axios`, `styled-components`와 같은 외부 라이브러리를 사용한다면, 해당 라이브러리의 타입 정의 패키지를 설치해야 합니다. 대부분의 유명 라이브러리는 `DefinitelyTyped` 저장소에 타입 정의를 가지고 있으며, `@types/` 접두사를 붙여 설치할 수 있습니다.
    
    npm install --save-dev @types/lodash @types/styled-components
        
    만약 타입 정의가 제공되지 않는 라이브러리라면, 직접 간단한 `.d.ts` 선언 파일을 만들어 모듈을 선언해줄 수 있습니다.

결론: 왜 타입스크립트인가?

리액트와 리액트 네이티브 프로젝트에 타입스크립트를 도입하는 것은 단순히 새로운 문법을 배우는 것 이상의 의미를 가집니다. 이는 애플리케이션의 안정성과 유지보수성을 한 차원 높은 수준으로 끌어올리는 전략적인 투자입니다.

  • 런타임 에러의 사전 방지: `undefined is not a function`과 같은 흔한 런타임 에러의 상당수는 props나 데이터의 타입 불일치에서 발생합니다. 타입스크립트는 이런 오류들을 개발 단계에서 포착하여 프로덕션 환경의 안정성을 크게 향상시킵니다.
  • 개선된 개발자 경험(DX): VS Code와 같은 최신 에디터에서 제공하는 강력한 자동 완성, 타입 힌트, 리팩토링 기능은 개발 속도를 높이고 코드의 의도를 명확하게 만듭니다.
  • 자체적인 문서화: 잘 작성된 타입 정의는 그 자체로 훌륭한 문서 역할을 합니다. 다른 개발자(혹은 미래의 나 자신)가 코드를 이해하는 데 드는 시간을 획기적으로 줄여주며, 특히 대규모 팀에서의 협업 효율을 극대화합니다.

처음에는 타입을 정의하는 과정이 다소 번거롭게 느껴질 수 있지만, 프로젝트의 규모가 커지고 복잡해질수록 타입스크립트가 제공하는 안정성과 예측 가능성은 그 어떤 비용보다 값진 자산이 됩니다. 최신 CLI 도구들이 제공하는 간편한 타입스크립트 템플릿을 활용하여, 여러분의 다음 리액트 프로젝트를 더욱 견고하고 신뢰할 수 있는 기반 위에서 시작해 보시길 바랍니다.


0 개의 댓글:

Post a Comment