
Remotion에서 '하나를 고쳐도 고쳐지지 않는' 버그의 근본 원인을 lazyComponent로 해결했다
요약
Remotion 프레임워크에서 특정 컴포지션의 폰트 설정 오류가 전체 렌더링 타임아웃으로 이어지는 문제를 lazyComponent(동적 임포트)를 통해 해결한 사례를 다룹니다. 정적 임포트로 인한 번들링 연쇄 구조를 끊기 위해 컴포지션을 메타데이터와 본체로 분리하는 설계 방식을 제안합니다.
핵심 포인트
- 정적 임포트 방식은 특정 컴포지션의 오류가 전체 렌더링에 영향을 주는 연쇄 구조를 만듦
- lazyComponent를 사용하여 렌더링 대상 외의 코드는 읽어오지 않도록 격리 가능
- 효과적인 격리를 위해 메타데이터(경량)와 컴포넌트 본체(무거운 의존성) 파일 분리 필요
- TypeScript 사용 시 schema 타입과 lazyComponent 반환 타입 간의 불일치 문제 주의
무엇이 일어났는가
Remotion(React로 영상을 제작할 수 있는 프레임워크)에서, 지난번 loadFont()에 weights/subsets를 지정하지 않으면 프로덕션 렌더링이 타임아웃되는 문제를 각 컴포지션(Composition)의 지정을 좁힘으로써 해결했다.
하지만 이는 대증요법이었으며, "다음에 새로운 컴포지션을 만들 때 지정을 잊어버리면 재발한다"라는 문제를 남겨두고 있었다. 이번 세션에서는 이 재발 리스크를 구조 자체로 끊어내는 근본적인 대책으로서, Root.tsx를 lazyComponent화 했다.
근본 원인: Root.tsx의 정적 임포트 (Static Import)
지난번의 교훈은 "렌더링 대상 파일을 고쳐도 고쳐지지 않는다"였다. 원인은 Remotion의 Root.tsx가 모든 컴포지션을 component={X}라는 정적 임포트(Static Import)로 항상 일괄 번들링하는 구성 그 자체에 있다. 이 구성이 남아있는 한, 특정 컴포지션의 폰트 설정 실수가 무관한 영상의 렌더링까지 끌어들이는 연쇄 구조는 사라지지 않는다.
Remotion의 <Composition>은 component 대신 lazyComponent(동적 임포트, Dynamic Import)를 전달할 수 있다. 이를 사용하면 렌더링 대상 이외의 컴포지션 코드는 일절 읽어오지 않게 되어, 연쇄 구조 자체를 끊을 수 있다.
다만 각 컴포지션의 현재 파일 구성에서는, Root.tsx가 필요로 하는 경량 정보(schema = props의 타입 정의, defaultProps = 기본값, calculateMetadata = 길이 등을 동적으로 계산하는 함수)와 무거운 컴포넌트 본체 + loadFont()가 동일한 한 파일에 공존하고 있어, 그대로서는 lazyComponent의 효과를 볼 수 없다. 그래서 각 컴포지션을 다음 두 파일로 분할하는 설계로 결정했다.
metadata.ts: 경량,loadFont비의존index.tsx: 컴포넌트 본체 +loadFont()
1개 컴포지션만으로 진행한 실험에서 알게 된 「올 오어 나싱 (All or Nothing)」
먼저 1개(이 폰트를 사용하지 않는 컴포지션)만으로 실험했다. 분할 자체는 성공했고 타입 체크도 통과했다. 하지만 실제로 npx remotion still로 렌더링해 보니, 표준 출력에 해당 컴포지션 자신은 사용하지 않을 터인 폰트(Shippori Mincho)의 네트워크 요청 로그가 여전히 나타났다.
원인은 다른 12개의 컴포지션이 여전히 정적 임포트 상태로 Root.tsx에 매달려 있었기 때문이었다. 즉, "하나만 고쳐서는 효과가 제로"라는, 부분 적용이 의미를 갖지 못하는 성질을 몸소 확인했다.
이 결과를 숨김없이 보고하자, 모든 컴포지션에 대한 전개가 즉시 결정되었다.
나머지 8개 컴포지션으로의 전개
Root.tsx에는 총 13개의 컴포지션이 있지만, loadFont()를 사용하고 있는 것은 실험했던 1개를 포함해 9개뿐이었다(나머지 4개는 폰트 로딩 자체를 하지 않으므로 대상 외). 그래서 나머지 8개 컴포지션에 동일한 분할을 적용했다.
컴포지션에 따라 loadFont()의 위치가 다르기 때문에(index.tsx 직เขียน, colors.ts, FigureScene.tsx 등), 매번 파일 구성을 확인하며 진행했다.
TypeScript 측면에서는 schema와 lazyComponent를 병용하면 타입 추론이 깨지는 문제가 있었다. schema (zod)로부터 추론되는 props 타입과 lazyComponent가 반환하는 동적 임포트의 Props 타입이 맞지 않아 무관한 타입 에러가 발생한다.
<Composition<typeof schema, Props>
id="SomeComposition"
lazyComponent={() => import("./index").then((m) => ({ default: m.SomeComposition }))}
...
이와 같이 명시적으로 제네릭(Generics)을 작성함으로써 해결했다.
검증: 폰트의 네트워크 요청 로그가 사라졌다
전체 전개 후, 타입 체크와 실제 렌더링으로 검증했다. 결정적인 증거로서, Shippori Mincho를 사용하지 않는 컴포지션을 렌더링해도 해당 폰트의 네트워크 요청 로그가 완전히 사라진 것을 확인했다.
요약
이전의 대증요법(weights/subsets를 제한하는 방식)은 재발 위험을 남겨두고 있었으나, Root.tsx의 정적 import (static import) 구성 자체가 연쇄적인 영향을 미치는 구조적 근본 원인임을 특정하였고, lazyComponent를 통한 컴포지션 (composition) 단위의 분리로 이를 해결했다. 단 하나의 컴포지션에만 부분 적용했을 때는 효과가 전혀 없다는 성질은 실험을 통해서야 비로소 알게 된 점으로, 구조적인 대책은 모든 대상에 전개하기 전까지는 의미를 갖지 못한다는 점은 기억해 둘 필요가 있다.
또 하나, 기술적인 측면과는 별개로 얻은 교훈이 있다. 타입 체크 (type check)와 렌더링 성공 여부만을 확인하고 커밋(commit) 및 푸시(push)까지 진행했으나, 9개 파일에 걸친 변경 사항을 실제로 화면에서 확인한 것은 그 이후였다. '망가지지 않았음을 확인하는 것'과 '외관이 변하지 않았음을 확인하는 것'은 별개의 문제라는 점을 지적을 받고 나서야 깨달았다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기