Skip to main content
네이티브
입문네이티브React Native 플랫폼 · 6

React Native TurboModules: 플랫폼 기능은 어떻게 JavaScript API가 되는가

타입 명세, 생성된 연결 인터페이스와 플랫폼 구현이 하나의 네이티브 기능 계약을 이루는 과정을 이해하고, 지원하지 않는 플랫폼에서 모듈 조회가 앱 시작을 막는 문제를 교정한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

TurboModule은 다음 세 부분이 함께 있어야 동작한다.

Spec은 Specification(명세)의 줄임말로 함수와 데이터 타입을 선언한 파일이다. TypeScript와 Flow 표기는 JavaScript 코드에 타입 정보를 붙이는 두 방식이며, Codegen은 이 명세를 읽어 Android·iOS 연결 인터페이스를 만드는 코드 생성 도구다. TurboModuleRegistry는 모듈 이름으로 현재 실행 플랫폼에 등록된 모듈 객체를 찾는다.

TypeScript 또는 Flow Spec
  → Codegen이 만든 Android·iOS 연결 인터페이스
  → 그 인터페이스를 구현하고 등록한 플랫폼 코드

TurboModuleRegistry.get()은 모듈이 없으면 null을 반환하고, getEnforcing()은 반드시 있어야 한다는 가정이 깨지면 예외를 던진다. 어느 쪽이 더 안전한지는 이름이 아니라 제품의 플랫폼 지원 계약으로 결정한다.

이 글을 관통하는 상황: Android 선택 기능 때문에 iOS 화면이 열리지 않는다

가상의 영수증 앱은 초안 저장을 Android에서만 먼저 제공한다. Android에는 NativeReceiptStore 구현과 등록이 있고, iOS에는 의도적으로 아직 없다. Spec 인터페이스는 메서드 모양만 선언하지만, 같은 파일의 getEnforcing 호출은 모든 실행 플랫폼에 모듈이 반드시 있다고 가정한다.

코드의 payload는 영수증 초안을 JSON 문자열로 만든 입력이다. Promise<boolean>은 저장 결과가 나중에 성공 여부로 도착한다는 뜻이다. loadDraft는 저장한 문자열을 다시 읽고, 값이 없으면 null을 돌려주는 계약이다.

// specs/NativeReceiptStore.ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'
 
export interface Spec extends TurboModule {
  saveDraft(id: string, payload: string): Promise<boolean>
  loadDraft(id: string): Promise<string | null>
}
 
export default TurboModuleRegistry.getEnforcing<Spec>('NativeReceiptStore')

기록을 보기 전에 먼저 예측한다.

  1. TypeScript의 Spec이 존재한다는 사실이 iOS 구현의 존재도 보장하는가?
  2. getEnforcing은 모듈이 없을 때 null과 예외 중 무엇을 만드는가?
  3. 선택 기능이 없는 iOS에서 앱 전체 Render가 시작되지 않는 것이 의도한 제품 계약인가?

fixture, 즉 미리 준비한 가상 증거 묶음은 같은 JavaScript 코드를 양쪽 플랫폼에서 실행해 다음 결과를 기록했다. 실제 React Native의 고정 오류 문구가 아니라 결정적인 관찰만 정리한 기록이다.

Android module registration   NativeReceiptStore 있음
Android Registry lookup       NativeReceiptStore 모듈 객체 반환
Android App Render             완료
 
iOS module registration       NativeReceiptStore 없음
iOS Registry lookup           getEnforcing 예외
iOS App Render                시작 전 중단

이 사례의 iOS 미지원은 정상 제품 범위인데, 조회 방식은 모듈 부재를 프로그래밍 오류로 처리한다. 타입 선언은 존재하지만 플랫폼 구현·등록이 없고, import할 때 파일 최상위에서 즉시 실행되는 조회의 예외가 화면보다 먼저 발생한다.

TurboModule은 한 파일이 아니라 경계의 묶음이다

각 부분의 실패 표면도 다르다.

부분맡는 책임대표 실패
NativeReceiptStore.ts SpecJavaScript에 보일 함수·입출력 선언지원하지 않는 타입, 이름 불일치
Codegen 결과Spec을 플랫폼 언어 인터페이스로 연결생성 설정 누락, 낡은 산출물
Android 구현·등록Android 저장 기능과 모듈 제공구현 누락, React Native에 구현을 제공하는 등록 목록 누락
iOS 구현·등록iOS 저장 기능과 모듈 제공구현 누락, iOS 구현을 제공하는 연결자 누락
JavaScript 소비 코드부재·완료·오류를 제품 상태로 처리import 시 예외, 오류 UI 누락

자세한 생성 설정과 산출물 읽기는 다음 7편에서 다룬다. 현재 편에서는 “선언·생성·구현·등록·소비” 중 어느 표면이 비어 있는지 나누는 판단만 남긴다.

get과 getEnforcing은 제품 지원 계약이 다르다

따라서 둘의 선택은 다음처럼 나뉜다.

제품 계약조회부재 시 행동
Android·iOS 모두 필수getEnforcing테스트·개발에서 즉시 실패시키고 누락된 구현·등록을 수정
일부 플랫폼의 선택 기능getnull을 미지원 상태로 바꾸고 해당 동작만 비활성화

관통 사례는 두 번째다. Spec의 마지막 줄만 다음처럼 바꾼다.

export default TurboModuleRegistry.get<Spec>('NativeReceiptStore')

이제 코드를 검사할 때 보이는 import 결과의 타입은 Spec | null이다. 앱이 실제로 실행될 때의 값은 Spec 계약으로 취급하는 등록된 모듈 객체 또는 null이며, 앱 화면은 부재를 명시적으로 처리한다.

import { useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import NativeReceiptStore from './specs/NativeReceiptStore'
 
export default function App() {
  const [status, setStatus] = useState('초안 저장 대기')
  const isSupported = NativeReceiptStore !== null
 
  async function handleSave() {
    if (!NativeReceiptStore) {
      setStatus('이 기기에서는 초안 저장을 지원하지 않습니다.')
      return
    }
 
    setStatus('저장 중')
 
    try {
      const payload = JSON.stringify({ merchant: '동네 상점', total: 12900 })
      const saved = await NativeReceiptStore.saveDraft(
        'receipt-42',
        payload,
      )
 
      if (!saved) {
        setStatus('초안 저장 실패')
        return
      }
 
      const stored = await NativeReceiptStore.loadDraft('receipt-42')
      setStatus(stored === payload ? '초안 저장 확인 완료' : '초안 저장 확인 실패')
    } catch {
      setStatus('초안 저장 오류')
    }
  }
 
  return (
    <View style={{ padding: 24 }}>
      <Pressable disabled={!isSupported} onPress={handleSave}>
        <Text>{isSupported ? '초안 저장' : '초안 저장 미지원'}</Text>
      </Pressable>
      <Text>
        {isSupported ? status : '이 기기에서는 초안 저장을 지원하지 않습니다.'}
      </Text>
    </View>
  )
}

교정 후 같은 fixture에서 기대할 결과는 다음과 같다.

Android Registry lookup  NativeReceiptStore 모듈 객체 반환
Android App Render        완료
Android 저장 버튼         저장 중 → 초안 저장 확인 완료
 
iOS Registry lookup      null 반환
iOS App Render            완료
iOS 저장 버튼             미지원 문구 표시, 네이티브 함수 호출 없음

성공 조건은 iOS 앱이 단순히 종료되지 않는 데서 끝나지 않는다. Android는 실제 저장 결과를 다시 읽어 확인하고, iOS는 저장 함수를 호출하지 않은 채 명확한 미지원 상태를 보여야 한다. 제품 계약이 나중에 양쪽 필수로 바뀌면 iOS의 null을 허용하는 테스트도 실패하도록 함께 바꾼다.

지연 로드는 요청 시점을 없애지 않는다

그러나 Spec 파일이 평가되면서 TurboModuleRegistry.get이나 getEnforcing을 호출하면 그 시점에 모듈을 요청한다. 지연 로드는 다음을 자동으로 보장하지 않는다.

  • JavaScript가 해당 Spec 파일을 언제 import하는지
  • 모듈 생성자가 무거운 일을 하지 않는지
  • 첫 기능 호출의 초기화 비용이 작은지

따라서 모듈의 초기화 로그나 추적을 “앱 시작”과 “첫 조회·첫 함수 호출”로 나눠 본다. 지연 로드라는 설명만으로 시작 시간이나 첫 사용 응답성을 측정했다고 결론 내리지 않는다.

동기 호출은 목표가 아니라 선택지다

4편에서 본 것처럼 직접 호출과 실제 작업 비용은 다른 문제다.

작고 즉시 준비되는 값       → 측정 후 동기 반환을 검토할 수 있음
저장·네트워크·큰 계산        → 완료·오류를 나중에 전달하는 비동기 계약 검토
UI 스레드나 JS 응답을 막는 일 → 네이티브라는 이유만으로 동기화하지 않음

관통 사례의 저장은 완료 또는 실패가 나중에 결정될 수 있으므로 Promise<boolean>로 선언했다. 실제 저장 매체의 완료 의미와 스레드는 Android·iOS 구현 계약에서 각각 정해야 한다.

타입 명세는 런타임 데이터 검증과 같지 않다

외부 입력이나 오래 저장된 데이터라면 JavaScript 경계 또는 각 플랫폼 구현에서 업무 구조를 검증해야 한다. Codegen이 어떤 플랫폼 코드를 만들고 어떤 오류를 빌드에서 잡는지는 7편의 한 질문으로 분리한다.

같은 상황을 다시 진단한다

문제       Android 선택 기능의 모듈이 없는 iOS에서 App Render 전 예외 발생
잘못된 판단 TypeScript Spec이 있으므로 모든 플랫폼에도 구현이 자동으로 존재함
관찰       Android Registry는 NativeReceiptStore 모듈 객체 반환, iOS getEnforcing은 예외
원인       의도된 선택 기능 부재를 '반드시 존재' 조회로 처리함
행동       get으로 조회하고 null을 명시적인 플랫폼 미지원 UI로 번역
검증       Android 실제 저장 확인, iOS는 네이티브 호출 없이 미지원 상태 표시

풀 리퀘스트에서는 다음을 확인한다.

  • Spec, 생성된 인터페이스, 각 플랫폼 구현·등록을 하나의 계약으로 검토했는가?
  • 기능이 필수인지 선택인지에 따라 getEnforcingget을 골랐는가?
  • 지연 로드라는 이름 대신 초기화와 첫 호출 시점을 관찰했는가?
  • 동기·Promise 반환을 실제 작업 성격과 실행 계약으로 선택했는가?
  • 생성 타입과 런타임 payload 검증을 같은 것으로 보지 않았는가?
  • Android와 iOS의 지원·부재 결과를 각각 확인했는가?

기억할 문장은 짧다.

TurboModule은 Spec 하나가 아니라 선언·생성·구현·등록·소비가 이어진 플랫폼 API 계약이다.

다음 글에서는 이 Spec에서 Android·iOS 연결 코드를 만드는 Codegen의 책임을 살펴본다.

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01TypeScript Spec 파일만 만들면 Android·iOS에서 호출 가능한 TurboModule이 완성될까?

    정답

    아니다. Spec에서 생성한 플랫폼 인터페이스를 실제 Android·iOS 코드가 구현하고 앱에 등록해야 한다.

    왜 그런가

    JavaScript 타입 선언, 생성된 연결 코드와 플랫폼 구현·등록이 함께 하나의 모듈 계약을 이룬다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02Android에만 의도적으로 제공하는 선택 기능을 iOS에서도 import할 때 getEnforcing을 사용해야 할까?

    정답

    아니다. 지원하지 않는 플랫폼이 정상 상태라면 get으로 조회하고 null을 명시적인 미지원 UI로 바꾼다.

    왜 그런가

    getEnforcing은 모듈이 반드시 존재한다는 계약이 깨졌을 때 예외를 던지는 조회다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03TurboModule이 기본적으로 지연 로드되면 앱 시작 중 spec 파일에서 Registry 조회를 해도 모듈은 절대 요청되지 않을까?

    정답

    아니다. 지연 로드는 모든 모듈을 무조건 시작 때 올리지 않는다는 뜻이며, JavaScript가 Registry에서 모듈을 요청하는 시점에는 조회가 일어난다.

    왜 그런가

    모듈 체계의 지연 로드와 앱 코드가 해당 모듈을 언제 import하고 조회하는지는 함께 설계해야 한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04JSI 기반 TurboModule이면 저장 작업도 동기 함수로 만들어야 새 체계의 이점을 쓰는 것일까?

    정답

    아니다. 동기 호출은 가능한 선택지일 뿐이다. 오래 걸리거나 완료가 나중인 작업은 Promise 같은 비동기 계약을 사용할 수 있다.

    왜 그런가

    직접 연결 능력과 작업 성격에 맞는 동기·비동기 API 선택은 다른 판단이다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  5. 05saveDraft의 payload 타입을 string으로 선언하면 그 문자열 안의 JSON 구조도 런타임에 자동 검증될까?

    정답

    아니다. 생성된 경계는 문자열이라는 타입을 연결하지만 문자열 내부 데이터의 업무 구조는 앱과 네이티브 구현이 별도로 검증해야 한다.

    왜 그런가

    생성 가능한 타입 계약과 외부·저장 데이터의 런타임 유효성 검사는 다른 책임이다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Native ModulesReact Native · 공식 문서 · 확인 2026-08-09
  2. Turbo Native Modules - AndroidReact Native · 공식 문서 · 확인 2026-08-09
  3. Turbo Native Modules - iOSReact Native · 공식 문서 · 확인 2026-08-09
  4. AppendixReact Native · 공식 문서 · 확인 2026-08-09
  5. New Architecture is hereReact Native · 공식 문서 · 확인 2026-08-09
이 문서의 마지막까지 읽었습니다.