React Native에서 Cheerio 사용하기: TypeScript 타입 선언 완벽 가이드

반응형

 

 

 

React Native에서 Cheerio 사용하기: TypeScript 타입 선언 완벽 가이드

React Native 프로젝트에서 HTML 파싱을 위해 react-native-cheerio를 사용하다 보면, TypeScript 컴파일 오류가 발생하는 경우가 있습니다. 이는 해당 패키지에 타입 선언이 없기 때문인데, 이 문제를 해결하는 방법을 상세히 알아보겠습니다.

문제 상황

react-native-cheerio 패키지는 JavaScript로만 작성되어 있어서:

  • TypeScript 타입 선언이 없음
  • @types/react-native-cheerio도 존재하지 않음
  • 따라서 TypeScript에서 사용 시 컴파일 오류 발생

해결 방법: 직접 타입 선언 파일 생성

1. 타입 선언 파일 생성

프로젝트 루트에 types/react-native-cheerio.d.ts 파일을 생성합니다.

declare module 'react-native-cheerio' {
  // Cheerio 요소의 기본 인터페이스
  interface CheerioElement {
    [key: string]: any;
  }

  // Cheerio 인스턴스 (jQuery 스타일 API)
  interface CheerioInstance {
    // 선택자 메서드
    find(selector: string): CheerioInstance;

    // 텍스트 조작
    text(): string;
    text(text: string): CheerioInstance;

    // HTML 조작
    html(): string;
    html(html: string): CheerioInstance;

    // 속성 조작
    attr(name: string): string;
    attr(name: string, value: string): CheerioInstance;

    // 반복 처리
    each(fn: (index: number, element: CheerioElement) => void): CheerioInstance;

    // 배열 스타일 접근
    length: number;
    [index: number]: CheerioElement;
  }

  // Cheerio 메인 인터페이스
  interface CheerioStatic {
    // 메인 함수 호출 시그니처
    (selector?: any, context?: any, root?: any, options?: any): CheerioInstance;

    // HTML 파싱 메서드
    load(html: string, options?: any): CheerioInstance;

    // 버전 정보
    version: string;
  }

  const reactNativeCheerio: CheerioStatic;
  export default reactNativeCheerio;
}

2. tsconfig.json 설정

tsconfig.json에서 타입 선언 파일을 인식하도록 설정합니다:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": [
    "types/**/*"
  ]
}

사용 방법

타입 선언 파일을 생성한 후, 다음과 같이 사용할 수 있습니다:

import cheerio from 'react-native-cheerio';

const htmlString = '<div><p>Hello World</p></div>';
const $ = cheerio.load(htmlString);

// 텍스트 추출
const text = $('p').text(); // "Hello World"

// HTML 조작
$('p').html('<strong>Bold Text</strong>');

// 속성 조작
$('div').attr('class', 'container');

// 반복 처리
$('p').each((index, element) => {
  console.log(`Element ${index}:`, element);
});

Import 스타일 선택

권장: Default Import

import cheerio from 'react-native-cheerio';

대안: Namespace Import

import * as cheerio from 'react-native-cheerio';

Default Import를 권장하는 이유:

  • 더 명확한 의미: "하나의 주요 기능을 가져온다"
  • 현대적 ES Module 스타일
  • 혼란 방지: Named exports와 구분됨

TypeScript 타입 선언 우선순위

TypeScript는 다음 순서로 타입 선언을 찾습니다:

  1. node_modules/@types/react-native-cheerio (DefinitelyTyped)
  2. react-native-cheerio/package.json의 "types" 필드
  3. react-native-cheerio/index.d.ts (패키지 루트)
  4. 프로젝트 내 타입 선언 파일 ← 우리가 만든 파일
  5. 없으면 'any' 타입으로 추론

프로젝트 구조 권장사항

your-project/
├── types/           # 외부 패키지 타입 선언
│   └── react-native-cheerio.d.ts
├── typings/         # 프로젝트 내부 커스텀 타입
│   └── custom.ts
└── src/
    └── ...

마무리

이제 React Native 프로젝트에서 TypeScript의 타입 안정성을 유지하면서 Cheerio를 사용할 수 있습니다. 타입 선언 파일을 통해 IDE의 자동완성과 타입 체크 기능도 정상적으로 작동합니다.

필요에 따라 더 많은 Cheerio 메서드를 타입 선언에 추가할 수 있으며, 이는 프로젝트의 요구사항에 맞춰 점진적으로 확장 가능합니다.

 

 

 

 

 

 

참고: src/types/react-native-cheerio.d.ts 전체 코드

 

/**
 * ================================================================
 * react-native-cheerio 모듈에 대한 TypeScript 타입 선언 파일
 * ================================================================
 *
 * 🎯 목적: TypeScript 컴파일 오류 해결
 * - react-native-cheerio 패키지는 JavaScript로만 작성됨 (타입 없음)
 * - @types/react-native-cheerio도 존재하지 않음
 * - 따라서 직접 타입 선언 파일을 생성하여 타입 안정성 확보
 *
 * 🔍 react-native-cheerio 패키지 분석:
 *
 * 📁 실제 파일 구조:
 * - index.js: exports = module.exports = require('./lib/cheerio');
 * - lib/cheerio.js: var Cheerio = module.exports = function(selector, context, root, options) { ... }
 *
 * 📦 CommonJS 패턴:
 * module.exports = function cheerio() { ... }
 * cheerio.load = function(html) { ... }  // 함수에 메서드 추가
 * cheerio.version = "1.0.0"              // 함수에 속성 추가
 *
 * 🔄 CommonJS vs ES Modules 호환성:
 *
 * 📌 CommonJS (react-native-cheerio의 실제 방식):
 * - 내보내기: module.exports = function
 * - 특징: 하나의 객체/함수를 내보냄
 * - 런타임: 동기적 로딩
 *
 * 📌 ES Modules (현대적 방식):
 * - Named exports: export const func1 = ..., export const func2 = ...
 * - Default export: export default function
 * - 특징: 정적 분석 가능, 트리 쉐이킹 지원
 *
 * 🪄 TypeScript/Babel의 호환성 마법:
 *
 * CommonJS 패키지를 ES Module 스타일로 import 가능:
 * - import * as cheerio from 'pkg' → const cheerio = require('pkg')
 * - import cheerio from 'pkg' → const cheerio = require('pkg')
 * - 결과: 둘 다 동일한 함수를 가져옴!
 *
 * 🎯 왜 이런 호환성이 필요한가?
 * 1. 역사적 이유: CommonJS가 먼저 존재
 * 2. 점진적 마이그레이션: 기존 패키지들의 ES Module 전환 지원
 * 3. 개발자 편의: 원하는 import 스타일 선택 가능
 *
 * 🤔 import 스타일 선택 기준:
 *
 * ❌ 기존 방식: import * as cheerio from 'react-native-cheerio'
 * - CommonJS라서 이렇게 써야 한다는 고정관념
 * - 실제로는 둘 다 동작함
 *
 * ✅ 권장 방식: import cheerio from 'react-native-cheerio'
 * - 논리적으로 더 명확: "하나의 주요 기능을 가져온다"
 * - Named exports와 헷갈리지 않음
 * - 현대적 ES Module 스타일
 *
 * 📊 import 스타일 비교:
 *
 * Named exports 패키지 (여러 기능):
 * export const func1 = ...
 * export const func2 = ...
 * → import { func1, func2 } from 'pkg' (선택적)
 * → import * as all from 'pkg' (전체를 namespace로)
 *
 * CommonJS 패키지 (하나의 주요 기능):
 * module.exports = function
 * → import cheerio from 'pkg' (권장)
 * → import * as cheerio from 'pkg' (가능하지만 혼란 야기)
 *
 * 🏗️ 타입 선언 방식 선택:
 *
 * 옵션 1: export = (CommonJS 스타일)
 * declare module 'react-native-cheerio' {
 *   interface CheerioStatic { ... }
 *   const cheerio: CheerioStatic;
 *   export = cheerio;  // TypeScript 전용 문법
 * }
 * 사용: import * as cheerio from 'react-native-cheerio'
 *
 * 옵션 2: export default (ES Module 스타일) ← 현재 선택
 * declare module 'react-native-cheerio' {
 *   interface CheerioStatic { ... }
 *   const cheerio: CheerioStatic;
 *   export default cheerio;  // 표준 ES Module 문법
 * }
 * 사용: import cheerio from 'react-native-cheerio'
 *
 * 🎯 현재 선택한 이유:
 * 1. 논리적 명확성: 하나의 주요 기능 → default import
 * 2. 현대적 스타일: ES Module 표준 문법 사용
 * 3. 혼란 방지: Named exports와 구분되는 명확한 패턴
 * 4. 사용자 편의: cheerio.load() 직관적 사용
 *
 * 📂 TypeScript 타입 선언 파일 참조 우선순위:
 * 1️⃣ node_modules/@types/react-native-cheerio (DefinitelyTyped) - ❌ 없음
 * 2️⃣ react-native-cheerio/package.json의 "types" 필드 - ❌ 없음
 * 3️⃣ react-native-cheerio/index.d.ts (패키지 루트) - ❌ 없음
 * 4️⃣ 프로젝트 내 타입 선언 파일 (이 파일!) - ✅ 여기서 발견!
 * 5️⃣ 없으면 'any' 타입으로 추론
 *
 * 🔥 타입 선언 충돌과 Module Augmentation:
 *
 * 💥 우선순위에 의한 덮어쓰기:
 * - 높은 우선순위가 낮은 우선순위를 완전히 덮어씀 (병합 안됨!)
 * - 예: @types/react + 프로젝트/react.d.ts → @types/react만 사용
 *
 * 🔧 Module Augmentation (모듈 확장):
 * - 기존 타입에 새로운 속성/메서드 추가
 * - 핵심 요구사항: 파일에 import/export 문이 있어야 함!
 *
 * 예시:
 * // react-augmentation.d.ts
 * import React from 'react';  // 이것이 augmentation 모드로 전환!
 *
 * declare module 'react' {
 *   interface HTMLAttributes<T> {
 *     customProp?: string;  // 기존 interface에 속성 추가
 *   }
 * }
 *
 * 🔍 Declaration vs Augmentation 모드:
 * - Declaration 모드 (import/export 없음): 새로운 모듈 선언 → 우선순위 적용
 * - Augmentation 모드 (import/export 있음): 기존 모듈 확장 → 병합
 *
 * 📊 타입 선언 우선순위 시스템 상세:
 *
 * 🎯 모듈별 개별 우선순위 (declare module 기준):
 * TypeScript는 각 모듈명에 대해 개별적으로 우선순위를 적용합니다.
 *
 * 예시 상황:
 * // @types/lodash/index.d.ts
 * declare module 'lodash' { export function map(): any; }
 * declare module 'lodash/map' { export default function(): any; }
 *
 * // lodash/index.d.ts (패키지 자체)
 * declare module 'lodash' { export function filter(): any; }
 * declare module 'lodash/debounce' { export default function(): any; }
 *
 * 🔍 우선순위 적용 결과:
 * - 'lodash' 모듈: @types/lodash 버전 사용 (패키지 버전 무시)
 * - 'lodash/map' 모듈: @types/lodash 버전 사용
 * - 'lodash/debounce' 모듈: lodash 패키지 버전 사용 (@types에 없으므로)
 *
 * ⚠️ 전역 선언의 충돌 (declare const/function/class/var):
 * 모듈 우선순위와 달리 전역 선언은 충돌 시 컴파일 에러 발생!
 *
 * 충돌 예시:
 * // @types/jquery/index.d.ts
 * declare const $: JQueryStatic;
 *
 * // 사용자의 types/jquery.d.ts
 * declare const $: MyCustomType;  // ❌ 컴파일 에러!
 * // Error: Duplicate identifier '$'
 *
 * 🛡️ 전역 충돌 방지 방법:
 * 1. ES Module 사용: export {} 추가하여 파일을 모듈로 만들기
 * 2. skipLibCheck: true 설정 (tsconfig.json)
 * 3. 전역 이름 피하기: Node, Array, String 등 내장 객체명 사용 금지
 * 4. 적절한 타입 정의 설치: @types/node 등
 *
 * 🏆 2025년 TypeScript 프로젝트 구조 베스트 프랙티스:
 *
 * 📁 현재 프로젝트 구조 평가:
 * ✅ types/ - 외부 패키지 타입 선언 (.d.ts 파일들)
 * ✅ typings/ - 프로젝트 내부 커스텀 타입들 (.ts 파일들)
 * → 업계 표준과 완벽히 일치하는 구조! 👍
 *
 * 🎯 결론:
 * - react-native-cheerio는 CommonJS 방식의 단일 함수 패키지
 * - export default로 타입 선언하여 import cheerio 스타일 사용
 * - 논리적으로 명확하고 현대적인 ES Module 패턴 적용
 * - TypeScript 컴파일 오류 해결과 동시에 타입 안정성 확보
 */

// 🔍 TypeScript 타입 선언 키워드들:
//
// 📌 declare의 의미:
// - "어딘가에 이미 존재한다"고 TypeScript에게 알려주는 키워드
// - 실제 구현 없이 타입 정보만 선언
// - 컴파일 시점에만 존재, JavaScript로 변환되면 사라짐
//
// 🌟 declare 사용 예시들:
// declare const globalVar: string;        // 전역 변수 타입 선언
// declare function globalFunc(): void;    // 전역 함수 타입 선언
// declare class GlobalClass { ... }       // 전역 클래스 타입 선언
// declare var window: Window;             // 브라우저 전역 객체
//
// 📌 module의 의미:
// - 모듈(패키지)의 네임스페이스를 정의
// - 특정 모듈명에 대한 타입 정보를 그룹화
// - import/export와 연결되는 실제 모듈 시스템
//
// 📌 declare module 조합:
// - "이 모듈명으로 import할 때 어떤 타입을 사용할지" 알려주는 선언
// - 실제 패키지 코드와는 무관하게 타입 정보만 제공함
//
// 📌 namespace의 의미:
// - 관련된 타입들을 그룹화하는 TypeScript 개념
// - 전역 스코프 오염을 방지하고 코드를 구조화
// - 점(.) 표기법으로 접근: MyNamespace.SomeType
// - 컴파일 시점에만 존재 (런타임에서 사라짐)
//
// 🌟 namespace 예시:
// declare namespace jQuery {
//   interface JQueryStatic {
//     (selector: string): JQuery;
//     ajax(settings: any): void;
//   }
//   interface JQuery {
//     click(): JQuery;
//     hide(): JQuery;
//   }
// }
// 사용: const $: jQuery.JQueryStatic = ...;
//      const element: jQuery.JQuery = ...;
//
// 🔑 핵심 차이점:
//
// 📦 객체 (런타임에 실제 존재):
// const Utils = {
//   formatDate: (date: Date) => date.toString(),
//   API_URL: 'https://api.com'
// };
// Utils.formatDate(new Date());  // 실행 가능! 실제 함수 호출
//
// 📦 namespace (컴파일 시점에만 존재):
// namespace Utils {
//   export type DateFormat = 'ISO' | 'US';
//   export interface ApiResponse { data: any }
// }
// const format: Utils.DateFormat = 'ISO';  // 타입으로만 사용
// Utils.formatDate(new Date());  // ❌ 에러! 런타임에 존재하지 않음
//
// 💡 module vs namespace 차이:
// - module: 실제 파일/패키지와 연결 (import/export 시스템)
// - namespace: 타입들의 논리적 그룹핑 (점 표기법 접근, 타입 전용)
//
// 🏛️ 과거 vs 현재:
// - 과거: namespace를 많이 사용 (TypeScript 초기)
// - 현재: ES Modules이 표준이 되면서 declare module을 더 선호
// - namespace는 여전히 타입 그룹화에 유용

declare module 'react-native-cheerio' {
  // Cheerio 요소의 기본 인터페이스
  interface CheerioElement {
    [key: string]: any;
  }

  // Cheerio 인스턴스 (jQuery 스타일 API)
  interface CheerioInstance {
    // 선택자 메서드
    find(selector: string): CheerioInstance;

    // 텍스트 조작
    text(): string;
    text(text: string): CheerioInstance;

    // HTML 조작
    html(): string;
    html(html: string): CheerioInstance;

    // 속성 조작
    attr(name: string): string;
    attr(name: string, value: string): CheerioInstance;

    // 반복 처리
    each(fn: (index: number, element: CheerioElement) => void): CheerioInstance;

    // 배열 스타일 접근
    length: number;
    [index: number]: CheerioElement;

    // 필요시 더 많은 jQuery 스타일 메서드 추가 가능
    // addClass, removeClass, hasClass, css, etc.
  }

  // Cheerio 메인 인터페이스 (함수이면서 동시에 객체)
  interface CheerioStatic {
    // 메인 함수 호출 시그니처 (cheerio('selector') 형태)
    (selector?: any, context?: any, root?: any, options?: any): CheerioInstance;

    // load 메서드 - HTML 문자열을 파싱하여 Cheerio 인스턴스 반환
    load(html: string, options?: any): CheerioInstance;

    // version 속성 (package.json에서 export됨)
    version: string;
  }

  // react-native-cheerio를 default export로 선언
  // 사용법: import cheerio from 'react-native-cheerio'
  const reactNativeCheerio: CheerioStatic;
  export default reactNativeCheerio;
}

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY