react native expo가 에뮬레이터와 연결이 안된다.

황토소금·2024년 8월 16일

TIL

목록 보기
18/49

문제

npx expo run:android

명령어로 안드로이드 앱을 빌드하고 런을 했다.
그런데 다음과 같은 오류가 발생했다.

// 터미널에서
Failed to stop dev server (bundler: metro)
CommandError: Timeout waiting for 'metro' dev server to close

// 에뮬레이터에서
This development build encountered the following error.

The development server returned response error code: 404

터미널 확인하기

우선 터미널에서 보았을 때

npx expo run:android

명령어 이후에 빌드는 성공했다.

BUILD SUCCESSFUL in 22s

그리고

Waiting on http://localhost:8081
› Opening exp+onestep://expo-development-client/(생략) on Galaxy_S24_API_33

이후에 위와 같은 오류가 난 것이다.

Failed to stop dev server (bundler: metro)
CommandError: Timeout waiting for 'metro' dev server to close

그러면 일단 지금 보기에 앱 자체는 잘 실행되었고, development build인 expo앱을 특정 URI를 통해서 에뮬레이터에서 실행하는 것으로 보인다.
그런데 이 이후에 dev server를 멈추는데에 실패했고 번들러가 metro라고 한다.
metro dev server가 닫히기 기다리다가 타임아웃이 됐다고 한다.

그런데 찾아본 바로는 react native가 개발 모드에서 동작하는 방식은 메트로 개발 서버가 로컬에 떠있고 에뮬레이터 내 앱에서 이 로컬 개발 서버에 JS 파일을 요청하면 메트로가 응답하는 식이다. 🔗 [React Native] Metro? 메트로가 뭐야?
🔗 GPT 대화 🔗 Metro docs

그런데 앱을 실행하는데 필요한 metro를 오히려 종료하는데 실패했다니 왜 그럴까

  1. 이미 metro dev server가 켜져있어서 그걸 종료하는 걸까
  2. metro 문제로 인해 재실행하는 과정일까

찾아본 것

일단 난 npx expo run:android가 어떤 과정을 수행하는지 몰라서 조금 찾아보았다. expo-cli/src/run

// packages/@expo/cli/src/run/index.ts
// ...은 생략한 부분
...
export const expoRun: Command = async (argv) => {
...
  switch (platform) {
        case 'android': {
          const { expoRunAndroid } = await import('./android/index.js');
          return expoRunAndroid(argsWithoutPlatform);
        }
    ...

npx expo run:android를 했을 때 expoRunAndroid가 실행된다.

./android/index.js에서는 결국 runAndroidAsync.ts에서 runAndroidAsync를 임포트하여 실행한다.

// packages/@expo/cli/src/run/android/runAndroidAsync.ts
// ...은 생략한 부분
...
export async function runAndroidAsync(projectRoot: string, { install, ...options }: Options) {
...
	  const manager = await startBundlerAsync(projectRoot, {
    	port: props.port,
        // If a scheme is specified then use that instead of the package name.
        scheme: (await getSchemesForAndroidAsync(projectRoot))?.[0],
        headless: !props.shouldStartBundler,
  });
  ...
      await manager.getDefaultDevServer().openCustomRuntimeAsync(
        'emulator',
        {
          applicationId: props.packageName,
        },
        { device: props.device.device }
      );

      if (props.shouldStartBundler) {
        logProjectLogsLocation();
      } else {
        await manager.stopAsync();
      }
    }
...

runAndroidAsync에서 metro 번들러 관련된 부분만 모아보면 위와 같다.
여기서 metro를 종료시킬 때는 props.shouldStartBundler가 false 일 때이다.

그리고 GPT에게 물어보니 props.shouldStartBundler가 false가 될 경우는 다음과 같다고 한다.

  1. 이미 번들링된 자바스크립트 코드 사용
  • 사전 빌드된 APK 사용: 사용자가 이미 번들링된 자바스크립트 코드를 포함한 APK(Android 패키지 파일)를 제공한 경우, Metro 번들러가 필요하지 않을 수 있습니다. 이 경우, APK 파일을 직접 에뮬레이터나 디바이스에 설치하고 실행하면 되므로, props.shouldStartBundler는 false로 설정됩니다.
  • 앱이 오프라인 모드로 실행될 때: 앱이 인터넷 연결 없이, 로컬에서 번들링된 자바스크립트 파일만으로 실행될 때도 Metro 번들러가 필요하지 않습니다.
  1. 배포 빌드 또는 릴리스 빌드
  • 릴리스 빌드: 릴리스 모드(release variant)로 빌드된 앱은 일반적으로 이미 번들링된 자바스크립트 코드를 포함하고 있습니다. 이런 경우, Metro 번들러를 통해 자바스크립트 코드를 제공할 필요가 없으므로, props.shouldStartBundler가 false로 설정됩니다.
  • 프로덕션 환경: 프로덕션 환경에서는 Metro 번들러가 필요하지 않습니다. 앱이 디바이스에서 독립적으로 실행될 수 있도록 모든 리소스가 미리 번들링되어 포함됩니다.
  1. 명령어 옵션에 따른 설정
  • --no-bundler 옵션: 사용자가 명령어를 실행할 때 --no-bundler 옵션을 명시한 경우, Metro 번들러를 시작하지 않도록 설정할 수 있습니다. 이 경우, props.shouldStartBundler는 false가 됩니다.
  1. 포트 충돌 또는 사용 불가
  • 포트가 사용 중일 때: 앱을 실행하기 전에 Metro 번들러가 사용할 포트를 확인하는 과정에서, 해당 포트가 이미 다른 프로세스에서 사용 중이거나 사용할 수 없는 상황일 수 있습니다. 이런 경우, 번들러를 시작할 수 없으므로 props.shouldStartBundler가 false로 설정될 수 있습니다.

GPT가 말해준 네 가지 경우 중 1번은 apk가 아니므로 탈락, 2번도 릴리즈 모드가 아니니 탈락, 3번과 같은 옵션은 주지 않았으니 탈락...해서 4번만 남았다. 8081 포트가 이미 사용중이라는건가? 하고
lsof로 8081포트를 사용중인 프로세스를 보았더니 다른 프로세스가 8081을 쓰고 있더라

그런데 내 기억에 이럴 경우 다른 포트를 쓰겠냐고 물어봤던 것 같기도 해서 npx expo start로 개발 서버만 켜보았더니 이럴수가 8081은 이미 쓰고 있다며 8082 포트로 열어도 되냐고 물어본다.

일단 8081 포트를 쓰는 프로세스를 죽이고 다시 명령어를 실행하니 잘 된다.

앞으로 더 찾아볼 것

  1. 왜 metro가 종료되지 않았을까
  2. expo run 명령어로는 8081 포트가 사용 중일 때 다른 포트로 열지 물어보지 않는가
profile
안녕하세요, 반갑습니다.

0개의 댓글