Tuesday, January 5, 2021

Flutter 웹에서 SVG가 보이지 않는 미스터리: ImageCodecException의 근본 원인과 렌더러별 완벽 해법

Flutter는 모바일, 웹, 데스크톱을 아우르는 크로스플랫폼 개발의 새로운 지평을 열었습니다. 특히 Flutter Web은 단일 코드베이스로 미려하고 빠른 웹 애플리케이션을 만들 수 있다는 점에서 많은 개발자들의 기대를 한 몸에 받고 있습니다. 하지만 이 강력한 프레임워크를 사용하다 보면, 마치 고요한 호수에 던져진 돌멩이처럼 예기치 못한 문제들이 개발의 흐름을 깨뜨리곤 합니다. 그중에서도 가장 당황스러운 순간은 어제까지 잘 작동하던 기능이 아무런 코드 변경 없이 갑자기 말썽을 부릴 때일 것입니다.

바로 'SVG 이미지 렌더링' 문제가 그렇습니다. 벡터 기반의 SVG(Scalable Vector Graphics)는 어떤 해상도에서도 깨지지 않는 선명함과 작은 파일 크기라는 장점 덕분에 현대 웹 디자인에서 필수적인 요소로 자리 잡았습니다. Flutter에서도 websafe_svgflutter_svg와 같은 훌륭한 패키지들 덕분에 SVG를 손쉽게 사용할 수 있습니다. 그런데 어느 날, 로컬 개발 환경에서 앱을 실행했더니 멀쩡히 잘 보이던 SVG 아이콘들이 모두 사라지고, 콘솔에는 붉은색의 ImageCodecException: Failed to decode image data. 에러 메시지만이 가득한 상황을 마주하게 될 수 있습니다. 더욱 미스터리한 점은, flutter build web 명령으로 프로덕션 빌드를 생성하여 웹 서버에 올리면 아무 일도 없었다는 듯이 SVG가 완벽하게 표시된다는 것입니다. 대체 무엇이 문제일까요?

이 글에서는 수많은 Flutter 웹 개발자들을 혼란에 빠뜨렸던 '특정 환경에서만 SVG가 보이지 않는 문제'의 근본적인 원인을 심도 있게 파헤치고, 명쾌한 해결책을 단계별로 제시합니다. 이 문제는 단순히 SVG 패키지의 버그나 코드 실수가 아닙니다. 바로 Flutter 웹이 작동하는 핵심 원리, 즉 **웹 렌더러(Web Renderer)**와 깊은 관련이 있습니다. 이 글을 끝까지 읽으시면 여러분은 단순히 문제를 해결하는 것을 넘어, Flutter 웹의 동작 방식을 더 깊이 이해하고 앞으로 발생할 수 있는 유사한 문제들에 능동적으로 대처할 수 있는 전문가로 거듭나게 될 것입니다.

문제의 재구성: 왜 내 환경에서만 발생할까?

문제를 해결하기 위해선 먼저 현상을 정확히 이해해야 합니다. 이 SVG 렌더링 이슈의 특징은 다음과 같습니다.

  • 에러 메시지: ImageCodecException: Failed to decode image data. 이 에러는 보통 이미지 파일이 손상되었거나, 형식이 잘못되었거나, 해당 코덱이 데이터를 해석할 수 없을 때 발생합니다. 하지만 SVG 파일은 브라우저에서 직접 열면 잘 보이고, 다른 환경에서는 정상적으로 렌더링되므로 파일 자체의 문제는 아님을 알 수 있습니다.
  • 특정 패키지: 주로 websafe_svg와 같은, 웹의 표준 기능을 활용하여 SVG를 렌더링하려는 패키지에서 문제가 관찰됩니다.
  • 환경에 따른 차이:
    • 문제 발생 O: VS Code나 Android Studio에서 'Run and Debug' 기능을 사용하여 Chrome으로 앱을 실행할 때.
    • 문제 발생 X: 터미널에서 flutter run -d chrome 명령을 직접 실행할 때.
    • 문제 발생 X: flutter build web으로 빌드한 결과물을 Nginx나 Apache 같은 웹 서버에 배포했을 때.

이러한 단서들을 종합해보면, 문제는 Flutter 코드나 SVG 파일, 혹은 SVG 패키지 자체에 있다기보다는, **'VS Code의 디버그 실행 환경'**과 **'터미널 직접 실행 또는 프로덕션 빌드 환경'** 사이에 존재하는 근본적인 차이점에서 비롯된다는 강력한 추론이 가능합니다. 그 차이의 핵심에 바로 '웹 렌더러'가 있습니다.

Flutter 웹의 두 얼굴: HTML과 CanvasKit 렌더러

Flutter 웹 애플리케이션은 브라우저에서 사용자에게 화면을 보여주기 위해 두 가지의 다른 '렌더링 엔진' 또는 '렌더러'를 사용합니다. 어떤 렌더러를 사용하느냐에 따라 앱의 성능, 동작 방식, 그리고 호환성이 크게 달라질 수 있으며, 이것이 바로 우리 문제의 핵심 열쇠입니다.

1. HTML 렌더러 (The HTML Renderer)

HTML 렌더러는 이름에서 알 수 있듯이 표준 HTML, CSS, 그리고 Canvas 2D API를 조합하여 Flutter 위젯을 화면에 그립니다. Flutter 위젯 트리는 이에 대응하는 HTML 요소 트리(DOM Tree)로 변환됩니다. 예를 들어, Flutter의 Text 위젯은 HTML의 <p><span> 태그로, Stack 위젯은 CSS의 position: absolute 속성을 가진 <div> 태그들로 변환되는 식입니다.

  • 장점:
    • 작은 다운로드 크기: 사용자의 브라우저가 이미 가지고 있는 HTML/CSS 렌더링 엔진을 그대로 활용하므로, 앱의 초기 로딩에 필요한 다운로드 용량이 매우 작습니다.
    • 텍스트 선택 및 SEO: 위젯이 표준 HTML 요소로 변환되므로, 사용자는 웹페이지의 텍스트처럼 자연스럽게 텍스트를 선택하고 복사할 수 있습니다. 또한, 검색 엔진 크롤러가 페이지의 콘텐츠를 더 쉽게 인덱싱할 수 있어 검색 엔진 최적화(SEO)에 유리합니다.
    • HTML 요소 연동: 기존 HTML 요소를 Flutter 위젯 트리에 쉽게 통합(Platform Views)할 수 있어, 레거시 웹 코드나 특정 HTML 라이브러리와의 연동이 용이합니다.
  • 단점:
    • 성능 한계: 복잡한 애니메이션이나 수많은 위젯이 동시에 렌더링될 경우, 브라우저의 DOM 조작 및 리페인트 비용으로 인해 성능 저하가 발생할 수 있습니다.
    • 브라우저 간 불일치: 각기 다른 브라우저(Chrome, Firefox, Safari 등)의 HTML/CSS 구현 방식의 미세한 차이로 인해, 모든 브라우저에서 100% 동일한 픽셀-퍼펙트(pixel-perfect)한 UI를 보장하기 어려울 수 있습니다.

2. CanvasKit 렌더러 (The CanvasKit Renderer)

CanvasKit 렌더러는 완전히 다른 접근 방식을 취합니다. 이것은 Google의 고성능 2D 그래픽 엔진인 **Skia**를 WebAssembly(WASM)와 WebGL을 사용하여 브라우저 안에서 직접 실행하는 기술입니다. Flutter 모바일 앱이 Skia를 사용하여 네이티브 UI를 그리는 것과 거의 동일한 방식으로, CanvasKit은 브라우저의 HTML <canvas> 요소 하나에 앱의 모든 UI를 픽셀 단위로 직접 그려냅니다.

  • 장점:
    • 최상의 성능: 복잡한 그래픽과 애니메이션에서 네이티브에 가까운 부드럽고 일관된 고성능을 제공합니다. WASM으로 컴파일된 Skia 엔진이 GPU 가속을 통해 직접 픽셀을 제어하기 때문입니다.
    • 픽셀-퍼펙트 일관성: 앱의 UI가 모바일 Flutter 앱과 거의 100% 동일하게 보입니다. 렌더링 로직이 브라우저의 DOM에 의존하지 않고 Skia 엔진에 의해 완전히 제어되므로, 모든 브라우저에서 동일한 결과를 보장합니다.
    • 고급 그래픽 기능: Skia가 제공하는 모든 셰이더, 필터, 그래픽 효과를 웹에서도 그대로 사용할 수 있습니다.
  • 단점:
    • 큰 다운로드 크기: Skia 엔진을 WASM 형태로 다운로드해야 하므로, 앱의 초기 로딩 시 약 2~3MB 정도의 추가적인 다운로드가 필요합니다. 이는 초기 로딩 속도에 영향을 줄 수 있습니다.
    • SEO 및 접근성 한계: 모든 것이 하나의 캔버스 위에 그려지기 때문에, 전통적인 방식의 텍스트 선택이나 검색 엔진 크롤링이 더 어렵습니다. (최신 Flutter 버전에서는 이 부분이 많이 개선되었지만, 여전히 HTML 렌더러만큼 자연스럽지는 않을 수 있습니다.)
    • 호환성 이슈: 바로 이 부분이 우리 문제의 핵심입니다. CanvasKit은 브라우저의 표준 렌더링 파이프라인을 우회하고 직접 그래픽을 제어합니다. 이 과정에서 특정 브라우저 기능이나 웹 표준과의 상호작용이 HTML 렌더러와 다르게 동작할 수 있습니다. websafe_svg와 같은 패키지는 브라우저의 네이티브 SVG 렌더링 기능(예: <img> 태그나 CSS background-image)을 사용하도록 설계되었을 수 있는데, CanvasKit 환경에서는 이러한 방식이 예상대로 작동하지 않아 ImageCodecException을 유발하는 것입니다.

자동 렌더러 선택 (auto)

Flutter 웹의 기본 설정은 auto입니다. 이 설정은 사용자의 디바이스 환경에 따라 최적의 렌더러를 자동으로 선택해 줍니다.

  • 데스크톱 브라우저: 빠른 CPU와 GPU, 넉넉한 메모리를 활용하여 최상의 성능을 내기 위해 **CanvasKit**을 우선적으로 선택합니다.
  • 모바일 브라우저: 초기 로딩 속도와 다운로드 크기가 더 중요한 모바일 환경을 고려하여 **HTML 렌더러**를 우선적으로 선택합니다.

이제 모든 퍼즐 조각이 맞춰졌습니다. VS Code에서 디버그 모드로 실행할 때, Flutter는 개발용 데스크톱 환경의 성능을 최대한 활용하기 위해 기본적으로 **CanvasKit 렌더러**를 사용합니다. 바로 이 CanvasKit 렌더러가 websafe_svg 패키지와 충돌하여 SVG 디코딩에 실패하는 것입니다. 반면, 터미널에서 그냥 실행하거나(이 경우 HTML 렌더러가 선택될 수 있음) 모바일 기기에서 접속하거나, 혹은 빌드 후 서버에 배포된 버전을 접속할 때는 (환경에 따라) HTML 렌더러가 사용되어 SVG가 정상적으로 보이게 되는 것입니다.

체계적인 문제 해결 접근법: 3단계 솔루션

원인을 파악했으니 이제 해결책을 찾아볼 차례입니다. 우리는 간단한 임시방편부터, 영구적인 설정, 그리고 가장 근본적인 해결책까지 세 가지 방법을 모두 알아보겠습니다.

해결책 1: 커맨드 라인에서 렌더러 강제 지정 (임시 해결)

가장 빠르고 간단하게 문제를 회피하는 방법은 Flutter 앱을 실행할 때 사용할 렌더러를 명시적으로 지정하는 것입니다. 디버깅 시에만 문제가 발생하므로, 디버깅을 시작할 때 HTML 렌더러를 사용하도록 강제할 수 있습니다.

VS Code의 통합 터미널이나 시스템 터미널을 열고 다음 명령어를 입력하세요.


flutter run -d chrome --web-renderer html

이 명령어는 Flutter에게 'Chrome 브라우저에서 앱을 실행하되, 렌더러는 무조건 HTML 렌더러를 사용하라'고 지시합니다. 이 명령으로 앱을 실행하면, VS Code의 디버그 실행과 마찬가지로 핫 리로드(Hot Reload)를 포함한 모든 개발 기능을 정상적으로 사용하면서도 SVG가 깨지지 않는 것을 확인할 수 있습니다.

  • 장점: 즉시 문제를 해결하고 개발을 이어나갈 수 있습니다. 다른 프로젝트 파일은 전혀 건드릴 필요가 없습니다.
  • 단점: 매번 디버깅을 시작할 때마다 이 명령어를 입력해야 하는 번거로움이 있습니다. 또한, 이는 근본적인 호환성 문제를 해결하는 것이 아니라 우회하는 방법에 가깝습니다.

해결책 2: 프로젝트 설정 파일에 렌더러 고정 (영구 해결)

매번 커맨드 라인 옵션을 주는 것이 번거롭다면, 프로젝트 자체에 기본 웹 렌더러를 지정할 수 있습니다. 이 방법은 프로젝트의 모든 개발자가 동일한 렌더링 환경을 공유하게 만들어 일관성을 높여줍니다.

프로젝트의 루트 디렉터리에 있는 web/index.html 파일을 여세요. 이 파일은 여러분의 Flutter 앱을 담는 껍데기 역할을 하는 HTML 파일입니다. 파일 내부에서 <body> 태그 바로 위에 있는 <script> 섹션을 찾을 수 있습니다. 보통 다음과 같은 코드가 보일 것입니다.


<!-- web/index.html -->
<!DOCTYPE html>
<html>
<head>
  ...
</head>
<body>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

이제 main.dart.js 스크립트를 로드하기 **전**에, 전역 변수를 설정하여 Flutter 웹 엔진에게 사용할 렌더러를 알려주는 코드를 한 줄 추가합니다.


<!-- web/index.html (수정 후) -->
...
<body>
  <script>
    // 이 줄을 추가하여 기본 렌더러를 'html'로 설정합니다.
    window.flutterWebRenderer = "html";

    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
...

이렇게 window.flutterWebRenderer = "html"; 라인을 추가하고 저장하기만 하면 됩니다. 이제부터는 VS Code에서 디버그를 실행하든, 터미널에서 flutter run을 실행하든, 항상 HTML 렌더러가 기본으로 사용됩니다. SVG 문제는 더 이상 발생하지 않을 것입니다.

  • 장점: 프로젝트에 설정을 영구적으로 적용하여 문제를 완전히 해결합니다. 팀원들과의 협업 시에도 렌더링 환경을 통일할 수 있습니다.
  • 단점: CanvasKit 렌더러의 성능적 이점을 포기하게 됩니다. 만약 앱이 복잡한 애니메이션이나 그래픽을 많이 사용한다면, HTML 렌더러에서는 데스크톱 환경에서 성능 저하를 겪을 수 있습니다.

해결책 3: 렌더러에 독립적인 `flutter_svg` 패키지 사용 (근본적인 해결)

앞선 두 해결책은 CanvasKit 렌더러를 피하는 방식이었습니다. 하지만 만약 우리가 CanvasKit의 뛰어난 성능과 렌더링 일관성을 포기하고 싶지 않다면 어떻게 해야 할까요? 정답은 렌더러의 종류에 구애받지 않고 안정적으로 작동하는 SVG 처리 라이브러리를 사용하는 것입니다.

바로 **flutter_svg** 패키지가 그 주인공입니다.

websafe_svg가 웹의 표준 기능을 활용하려다 CanvasKit 환경에서 호환성 문제를 겪는 것과 달리, flutter_svg는 SVG 파일의 XML 데이터를 직접 파싱(parsing)한 후, 파싱된 경로와 도형 정보를 Flutter의 저수준 드로잉 API(CustomPainterCanvas)를 사용해 직접 그려냅니다. 이 방식은 Flutter가 모바일에서 화면을 그리는 방식과 유사하기 때문에, HTML 렌더러든 CanvasKit 렌더러든 상관없이 완벽하게 동일한 결과를 보장합니다.

flutter_svg로 마이그레이션하는 방법은 매우 간단합니다.

  1. 패키지 추가: pubspec.yaml 파일에 flutter_svg 의존성을 추가합니다. (이미 websafe_svg가 있다면 교체하거나 함께 사용할 수 있습니다.)
    
    dependencies:
      flutter:
        sdk: flutter
      
      # 기존 패키지 (문제가 발생할 수 있음)
      # websafe_svg: ^2.0.0 
    
      # 새로 추가할 권장 패키지
      flutter_svg: ^2.0.4 # 최신 버전으로 사용하세요
        
    터미널에서 flutter pub get을 실행하여 패키지를 설치합니다.
  2. 코드 수정: SVG를 사용하던 Dart 파일을 열고, 기존 코드를 flutter_svg의 위젯으로 교체합니다. 기존 코드 (websafe_svg 예시):
    
    import 'package:websafe_svg/websafe_svg.dart';
    
    // ... 위젯 내부
    WebsafeSvg.asset(
      'assets/images/my_icon.svg',
      width: 50,
      height: 50,
      color: Colors.blue,
    );
        
    수정된 코드 (flutter_svg):
    
    import 'package:flutter_svg/flutter_svg.dart';
    
    // ... 위젯 내부
    SvgPicture.asset(
      'assets/images/my_icon.svg',
      width: 50,
      height: 50,
      colorFilter: ColorFilter.mode(Colors.blue, BlendMode.srcIn), // 색상 적용 방식이 조금 다름
    );
        

    보시다시피 위젯 이름이 WebsafeSvg에서 SvgPicture로 바뀌고, 색상을 지정하는 속성명이 color에서 colorFilter로 변경된 것 외에는 사용법이 거의 동일합니다. `colorFilter`를 사용하는 방식이 조금 더 복잡해 보일 수 있지만, 이는 Flutter의 표준 드로잉 API와 일치하는 방식으로 더 정교한 색상 제어를 가능하게 합니다.

이 방법을 사용하면 web/index.html 파일을 수정할 필요가 없습니다. 렌더러 설정을 기본값인 `auto`로 그대로 두면, 데스크톱에서는 CanvasKit의 성능을, 모바일에서는 HTML 렌더러의 빠른 로딩 속도를 모두 누리면서도 SVG는 모든 환경에서 안정적으로 렌더링되는 이상적인 결과를 얻을 수 있습니다.

  • 장점: 문제의 근본 원인을 해결합니다. 렌더러에 구애받지 않으므로 어떤 환경에서도 일관된 동작을 보장합니다. Flutter 웹의 성능적 이점을 최대한 활용할 수 있습니다.
  • 단점: 기존에 websafe_svg를 광범위하게 사용했다면 코드 수정에 약간의 품이 들어갑니다. 하지만 장기적인 안정성과 유지보수성을 고려하면 충분히 투자할 가치가 있습니다.

HTML vs CanvasKit: 내 프로젝트에 맞는 렌더러 선택 전략

이제 우리는 두 렌더러의 차이와 문제 해결 방법을 모두 알게 되었습니다. 그렇다면 최종적으로 어떤 렌더러 전략을 선택해야 할까요? 다음 표는 여러분의 프로젝트 특성에 맞는 최적의 결정을 내리는 데 도움을 줄 것입니다.

고려 사항 HTML 렌더러 CanvasKit 렌더러 추천 전략
초기 로딩 속도 매우 빠름 (추가 다운로드 거의 없음) 느림 (2-3MB WASM 파일 다운로드 필요) 콘텐츠 중심의 블로그, 뉴스 사이트, 간단한 정보 페이지 등 초기 이탈률이 중요한 경우 HTML 렌더러를 명시적으로 선택하는 것을 고려할 수 있습니다.
런타임 성능 간단한 UI에서는 충분, 복잡한 애니메이션 시 성능 저하 가능 모바일 네이티브에 준하는 최상의 성능 제공 데이터 시각화, 온라인 편집 툴, 게임, 복잡한 인터랙션이 많은 RIA(Rich Internet Application)의 경우 CanvasKit이 필수적입니다.
렌더링 일관성 브라우저별 미세한 차이 발생 가능 모바일/데스크톱/모든 브라우저에서 픽셀-퍼펙트 보장 브랜드의 시각적 아이덴티티가 매우 중요하고, 모든 플랫폼에서 동일한 사용자 경험을 제공해야 한다면 CanvasKit이 유리합니다.
SEO 및 텍스트 선택 매우 우수 (표준 HTML 요소 사용) 개선되었으나 여전히 제한적일 수 있음 콘텐츠의 검색 가능성이 비즈니스의 핵심이라면 HTML 렌더러를 우선적으로 고려해야 합니다.
SVG 호환성 대부분의 패키지와 호환성이 좋음 websafe_svg 등 일부 패키지와 호환성 이슈 발생 렌더러에 상관없이 안정적인 앱을 만들고 싶다면, 렌더러를 선택하기보다 flutter_svg 패키지를 사용하는 것이 가장 현명한 전략입니다.

마무리하며: 버그를 넘어 Flutter 웹 전문가로

단순히 보이지 않던 SVG 이미지 문제에서 시작했지만, 우리는 그 원인을 추적하는 과정에서 Flutter 웹의 핵심 아키텍처인 HTML과 CanvasKit 렌더러의 존재와 그 차이점을 깊이 있게 탐구했습니다. 이제 ImageCodecException이라는 에러 메시지는 더 이상 여러분에게 미스터리가 아닐 것입니다. 그것은 Flutter 웹이 처한 환경에 따라 다른 얼굴을 보여주고 있다는 명확한 신호입니다.

이 글에서 제시한 세 가지 해결책을 통해 여러분은 어떤 상황에서도 유연하게 대처할 수 있는 무기를 얻었습니다. 간단한 디버깅을 위해 커맨드 라인 옵션으로 렌더러를 잠시 바꾸고, 프로젝트의 방향성에 맞게 index.html에서 렌더러를 고정시키거나, 혹은 flutter_svg를 사용하여 렌더러의 제약으로부터 완전히 자유로워질 수도 있습니다.

훌륭한 개발자는 단지 코드를 작성하는 사람이 아니라, 자신이 사용하는 기술의 내부 동작 원리를 이해하고 문제가 발생했을 때 체계적으로 원인을 분석하여 최적의 해결책을 찾아내는 사람입니다. 오늘 겪었던 이 작은 버그는 여러분을 Flutter 웹의 더 깊은 이해로 이끌어준 좋은 스승이었을 것입니다. 이제 여러분은 두 개의 렌더러를 자유자재로 다루며, 더 안정적이고 성능 좋은 Flutter 웹 애플리케이션을 만들 수 있는 진정한 전문가로 한 걸음 더 나아갔습니다.


0 개의 댓글:

Post a Comment