Friday, December 23, 2022

플러터에서 SVG 사용하기: 패키지 없이 CustomPaint로 그리는 법

현대적인 앱 개발에서 벡터 그래픽, 특히 SVG(Scalable Vector Graphics) 형식은 매우 중요한 자산입니다. 래스터 이미지(PNG, JPG 등)와 달리 SVG는 수학적인 공식으로 이미지를 정의하기 때문에 어떤 해상도에서도 깨짐 없이 선명하게 표현됩니다. 또한, 파일 크기가 작고 코드 레벨에서 색상이나 형태를 쉽게 수정할 수 있다는 장점 덕분에 아이콘, 로고, 간단한 일러스트레이션에 널리 사용됩니다.

하지만 구글의 크로스플랫폼 프레임워크인 플러터(Flutter)는 기본적으로 SVG를 렌더링하는 기능을 내장하고 있지 않습니다. 이로 인해 많은 개발자들이 플러터 프로젝트에서 SVG를 어떻게 효율적으로 사용할 수 있을지 고민하게 됩니다. 해결책은 크게 두 가지로 나뉩니다. 첫 번째는 가장 대중적인 방법인 전용 패키지를 사용하는 것이고, 두 번째는 SVG 데이터를 플러터의 네이티브 드로잉 코드로 변환하는 것입니다. 이 글에서는 두 가지 방법을 모두 심도 있게 살펴보고, 특히 패키지 의존성을 추가하고 싶지 않은 경우를 위한 강력한 대안인 CustomPaint 활용법을 자세히 알아보겠습니다.

표준적인 접근법: `flutter_svg` 패키지 활용

플러터 커뮤니티에서 SVG를 사용한다고 하면 가장 먼저 떠오르는 해결책은 단연 flutter_svg 패키지입니다. 이 패키지는 플러터 생태계에서 사실상의 표준으로 자리 잡았으며, SVG 파일을 파싱하고 플러터 위젯으로 렌더링하는 데 필요한 모든 기능을 제공합니다.

`flutter_svg` 패키지의 장점

  • 사용 편의성: 몇 줄의 코드만으로 앱의 어느 곳에서든 SVG 이미지를 손쉽게 표시할 수 있습니다.
  • 다양한 소스 지원: 로컬 애셋(assets/ 폴더), 네트워크 URL, 심지어 메모리에 있는 SVG 문자열 데이터로부터도 이미지를 불러올 수 있습니다.
  • 높은 호환성: 대부분의 SVG 표준을 지원하여 그라데이션, 클리핑, 마스크 등 복잡한 SVG 파일도 비교적 정확하게 렌더링합니다.
  • 추가 기능: 이미지의 색상을 동적으로 변경하거나, 스크린 리더를 위한 시맨틱 레이블을 추가하는 등 유용한 부가 기능을 제공합니다.

`flutter_svg` 사용 방법

사용법은 매우 직관적입니다. 먼저, 프로젝트의 pubspec.yaml 파일에 패키지를 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  flutter_svg: ^2.0.10+1 # 최신 버전은 pub.dev에서 확인하세요

패키지를 추가한 후 터미널에서 flutter pub get 명령을 실행하여 의존성을 설치합니다. 이제 코드에서 SvgPicture 위젯을 사용하여 SVG를 렌더링할 수 있습니다.

1. 로컬 애셋에서 SVG 불러오기

가장 일반적인 사용 사례입니다. 먼저 pubspec.yaml에 애셋 폴더를 등록해야 합니다.

flutter:
  assets:
    - assets/images/

그런 다음, SvgPicture.asset 생성자를 사용합니다.

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

class MyIconWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SvgPicture.asset(
      'assets/images/my_icon.svg',
      width: 50,
      height: 50,
      colorFilter: ColorFilter.mode(Colors.blue, BlendMode.srcIn), // 아이콘 색상 변경
      semanticsLabel: 'A blue icon' // 접근성을 위한 설명
    );
  }
}

2. 네트워크에서 SVG 불러오기

서버에서 동적으로 SVG 아이콘을 받아와야 할 때 유용합니다.

SvgPicture.network(
  'https://example.com/path/to/icon.svg',
  placeholderBuilder: (BuildContext context) => Container(
    padding: const EdgeInsets.all(30.0),
    child: const CircularProgressIndicator(),
  ),
)

이처럼 flutter_svg는 강력하고 편리한 도구입니다. 프로젝트 전반에 걸쳐 다수의 SVG를 사용하거나, 복잡한 벡터 그래픽을 다루거나, 네트워크를 통해 이미지를 로드해야 하는 상황이라면 이 패키지는 의심할 여지 없이 최고의 선택입니다.

가벼운 대안: SVG를 `CustomPaint`로 변환하기

flutter_svg 패키지는 훌륭하지만, 모든 상황에 최적인 것은 아닙니다. 예를 들어, 앱에서 단지 한두 개의 간단한 아이콘을 SVG 형태로 사용하고 싶을 뿐인데, 이를 위해 외부 패키지 의존성을 추가하는 것이 부담스러울 수 있습니다. 패키지는 앱의 최종 빌드 크기를 미세하게나마 증가시키고, 관리해야 할 또 다른 요소를 추가하기 때문입니다. 이런 경우, SVG를 플러터의 네이티브 드로잉 코드로 직접 변환하는 방법이 빛을 발합니다.

`CustomPaint`와 `CustomPainter`의 개념

플러터는 CustomPaint 위젯과 CustomPainter 클래스를 통해 개발자가 직접 캔버스(Canvas)에 그림을 그릴 수 있는 저수준(low-level) API를 제공합니다. 이를 이용하면 선, 원, 사각형은 물론 복잡한 경로(Path)까지 원하는 대로 그릴 수 있습니다. SVG의 핵심은 결국 점과 선의 집합인 '경로 데이터'이므로, 이 데이터를 플러터의 Path 객체로 변환하면 동일한 이미지를 코드로 그려낼 수 있습니다.

이 방법의 장점

  • 제로 의존성(Zero Dependency): 외부 패키지를 추가할 필요가 전혀 없습니다. 앱을 더 가볍고 간결하게 유지할 수 있습니다.
  • 잠재적인 성능 이점: 이미 컴파일된 Dart 코드로 그림을 그리는 것은 런타임에 SVG 파일을 읽고 XML을 파싱하는 과정보다 빠를 수 있습니다. 특히 간단하고 정적인 아이콘의 경우 더욱 그렇습니다.
  • 완벽한 제어: 생성된 코드는 순수한 Dart 코드이므로, 개발자가 모든 요소를 완벽하게 제어할 수 있습니다. 경로의 일부만 색을 바꾸거나, 특정 부분에만 애니메이션을 적용하는 등 창의적인 활용이 가능합니다.
  • 최적화된 앱 크기: 소수의 아이콘만 사용하는 경우, SVG 파일과 패키지 라이브러리를 포함하는 것보다 생성된 Dart 코드가 차지하는 공간이 더 작을 수 있습니다.

변환 도우미: FlutterShapeMaker

SVG의 복잡한 경로 데이터를 수동으로 Dart 코드로 옮기는 것은 매우 지루하고 오류가 발생하기 쉬운 작업입니다. 다행히 이 변환 과정을 자동화해주는 훌륭한 웹 도구가 있습니다. 바로 FlutterShapeMaker입니다.

FlutterShapeMaker는 SVG 파일의 내용을 붙여넣기만 하면 즉시 해당 이미지를 그리는 CustomPainter 코드를 생성해주는 웹사이트입니다. 사용법은 놀라울 정도로 간단합니다.

단계별 가이드: FlutterShapeMaker로 SVG를 코드로 변환하기

이제 실제 예제를 통해 SVG 아이콘을 CustomPaint 코드로 변환하고 앱에 적용하는 전 과정을 살펴보겠습니다.

1단계: SVG 파일 준비

먼저 사용할 SVG 파일을 준비합니다. 텍스트 편집기나 VS Code로 SVG 파일을 열면 아래와 같은 XML 코드를 볼 수 있습니다. 이 도구는 단색의 간단한 경로로 구성된 SVG에 가장 적합합니다. 그라데이션이나 여러 색상, 복잡한 필터가 포함된 SVG는 제대로 변환되지 않을 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>

2단계: FlutterShapeMaker 방문 및 코드 붙여넣기

FlutterShapeMaker 웹사이트에 접속합니다. 왼쪽에 있는 "Paste your SVG code here"라는 텍스트 영역에 위에서 복사한 SVG XML 코드를 모두 붙여넣습니다.

3단계: 코드 생성 및 복사

SVG 코드를 붙여넣는 즉시, 오른쪽 영역에 해당 이미지를 그리는 Dart 코드가 자동으로 생성됩니다. "Get Code" 버튼을 누르거나 오른쪽 코드 영역의 "Copy" 버튼을 클릭하여 생성된 코드를 클립보드에 복사합니다.

4단계: 플러터 프로젝트에 코드 통합하기

이제 복사한 코드를 플러터 프로젝트에 붙여넣을 차례입니다. 보통 별도의 파일(예: lib/painters/check_icon_painter.dart)을 만들어 관리하는 것이 좋습니다.

생성된 코드는 다음과 같은 형태일 것입니다.

// lib/painters/check_icon_painter.dart
import 'package:flutter/material.dart';

class CheckIconPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Path path = Path();
    path.moveTo(size.width * 0.5, size.height * 0.08333333);
    // ... 수많은 path.lineTo, path.cubicTo 등의 호출 ...
    path.close();

    Paint paint = Paint()..color = Colors.black;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

5단계: `CustomPaint` 위젯으로 화면에 그리기

이제 방금 만든 CheckIconPainterCustomPaint 위젯을 통해 화면에 표시할 수 있습니다. CustomPaint 위젯의 painter 속성에 우리가 만든 커스텀 페인터 인스턴스를 전달하고, size 속성으로 아이콘의 크기를 지정합니다.

import 'package:flutter/material.dart';
import 'package:your_app/painters/check_icon_painter.dart'; // 파일 경로에 맞게 수정

class IconDisplayScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPaint SVG Icon'),
      ),
      body: Center(
        child: CustomPaint(
          size: Size(100, 100), // 원하는 크기를 지정
          painter: CheckIconPainter(),
        ),
      ),
    );
  }
}

이 코드를 실행하면 화면 중앙에 100x100 크기의 검은색 체크 아이콘이 그려지는 것을 확인할 수 있습니다. 패키지 없이, 오직 순수한 Dart 코드로만 SVG를 렌더링한 것입니다!

생성된 코드 커스터마이징하기

이 방법의 진정한 힘은 생성된 코드를 자유자재로 수정할 수 있다는 점에서 나옵니다. 예를 들어, 아이콘의 색상을 동적으로 변경하고 싶다고 가정해 봅시다. CheckIconPainter 클래스를 다음과 같이 수정할 수 있습니다.

// lib/painters/check_icon_painter.dart (수정된 버전)
import 'package:flutter/material.dart';

class CheckIconPainter extends CustomPainter {
  final Color iconColor; // 색상을 받을 속성 추가

  CheckIconPainter({this.iconColor = Colors.black}); // 생성자에서 색상을 받도록 수정

  @override
  void paint(Canvas canvas, Size size) {
    Path path = Path();
    // ... FlutterShapeMaker가 생성한 path 데이터는 그대로 둡니다 ...
    path.moveTo(size.width * 0.5, size.height * 0.08333333);
    // ...
    path.close();

    // 하드코딩된 색상 대신 생성자에서 받은 iconColor를 사용합니다.
    Paint paint = Paint()..color = this.iconColor;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CheckIconPainter oldDelegate) {
    // 색상이 변경되었을 때만 다시 그리도록 최적화
    return oldDelegate.iconColor != iconColor;
  }
}

이제 이 페인터를 사용할 때 색상을 지정할 수 있습니다.

// 파란색 아이콘
CustomPaint(
  size: Size(100, 100),
  painter: CheckIconPainter(iconColor: Colors.blue),
)

// 빨간색 아이콘
CustomPaint(
  size: Size(100, 100),
  painter: CheckIconPainter(iconColor: Colors.red),
)

이처럼 간단한 수정만으로도 재사용 가능하고 유연한 아이콘 위젯을 만들 수 있습니다. 애니메이션을 추가하거나 여러 경로에 다른 색상을 적용하는 등 활용 가능성은 무궁무진합니다.

어떤 방법을 선택해야 할까? `flutter_svg` vs `CustomPaint`

두 가지 방법을 모두 살펴본 지금, 어떤 상황에서 어떤 방법을 선택하는 것이 현명할지 정리해 보겠습니다.

`flutter_svg` 패키지를 사용해야 하는 경우:

  • 많은 수의 SVG를 사용할 때: 앱 전반에 걸쳐 수십, 수백 개의 SVG 아이콘이나 이미지를 사용한다면 매번 코드를 변환하는 것보다 패키지를 사용하는 것이 훨씬 효율적이고 관리하기 편합니다.
  • 복잡한 SVG를 다룰 때: 그라데이션, 여러 색상, 텍스트, 복잡한 그룹과 필터가 포함된 SVG는 `CustomPaint`로 변환하기 어렵거나 불가능할 수 있습니다. `flutter_svg`는 이러한 복잡한 파일에 대한 지원이 훨씬 뛰어납니다.
  • 네트워크 이미지가 필요할 때: 서버에서 동적으로 SVG를 불러와야 한다면 `SvgPicture.network`를 제공하는 `flutter_svg`가 유일한 현실적인 선택입니다.
  • 빠른 개발 속도가 중요할 때: 단순히 애셋 경로만 지정하면 되므로 개발 속도가 매우 빠릅니다.

`CustomPaint` 변환 방식을 사용해야 하는 경우:

  • 소수의 간단한 아이콘만 필요할 때: 단 몇 개의 아이콘을 위해 패키지 의존성을 추가하고 싶지 않을 때 완벽한 대안입니다.
  • 앱의 경량화가 매우 중요할 때: 의존성을 하나라도 줄여서 앱의 크기를 최소화하고 싶을 때 좋은 선택입니다.
  • 아이콘에 대한 완벽한 제어가 필요할 때: 아이콘의 특정 경로에 애니메이션을 적용하거나, 사용자의 상호작용에 따라 형태를 미세하게 바꾸는 등 고도의 커스터마이징이 필요할 때 강력한 힘을 발휘합니다.
  • 성능을 극한으로 최적화하고 싶을 때: 정적인 아이콘의 경우, 파싱 과정이 없는 컴파일된 Dart 코드가 미세한 성능 우위를 가질 수 있습니다.

결론

플러터에서 SVG를 사용하는 방법에는 정답이 없습니다. 프로젝트의 요구사항과 개발자의 선호에 따라 가장 적합한 도구를 선택하는 것이 중요합니다. flutter_svg 패키지는 대부분의 상황에서 편리하고 강력한 만능 해결책을 제공합니다. 하지만 앱의 경량성을 유지하고 싶거나, 소수의 아이콘에 대해 완전한 제어권을 갖고 싶다면, FlutterShapeMaker와 같은 도구를 활용하여 SVG를 CustomPaint 코드로 변환하는 방법을 고려해볼 가치가 충분합니다.

이 두 가지 접근법의 장단점을 명확히 이해하고 상황에 맞게 적절히 활용한다면, 여러분의 플러터 애플리케이션을 더욱 풍부하고 효율적으로 만들 수 있을 것입니다. 직접 두 가지 방법을 모두 시도해보고 여러분의 프로젝트에 가장 잘 맞는 워크플로우를 찾아보시길 바랍니다.


0 개의 댓글:

Post a Comment