안녕하세요! Flutter로 모바일, 웹, 데스크톱 앱을 만들며 크로스 플랫폼의 강력함을 경험하고 계실 텐데요. 여기서 한 걸음 더 나아가, 여러분의 Flutter 앱을 라즈베리 파이나 젯슨 나노 같은 임베디드 장치에서 구동하는 상상, 해보셨나요? 오늘은 바로 그 새로운 가능성을 열어주는 flutter-elinux에 대해 깊이 알아보려 합니다.

flutter-elinux

flutter-elinux는 Sony가 개발한 비공식 Flutter SDK 확장 도구로, 임베디드 리눅스(eLinux) 환경에서 Flutter 앱을 빌드하고 디버깅할 수 있게 해줍니다. 덕분에 우리는 익숙한 Flutter의 개발 속도와 유연성을 임베디드 세계로 가져와, 풍부하고 매력적인 UI를 구현할 수 있게 되었습니다.


flutter-elinux, 무엇이 특별할까요?

임베디드 보드

flutter-elinux는 임베디드 시스템의 까다로운 요구사항을 만족시키기 위해 여러 인상적인 강점을 갖추고 있습니다.

  • 임베디드에 최적화된 경량 설계: 일반 리눅스 데스크톱 환경(X11, GTK 기반)보다 훨씬 가볍고, 최소한의 라이브러리만 사용하도록 설계되었습니다. 제한된 리소스의 임베디드 기기에서 시스템 부하를 줄이며 높은 성능을 발휘할 수 있는 비결이죠.
  • 폭넓은 아키텍처 지원: arm64x64 아키텍처를 모두 지원하여 라즈베리 파이, 젯슨 나노 등 다양한 임베디드 보드와 뛰어난 호환성을 자랑합니다.

크로스 빌딩 개념도

  • 강력한 크로스 빌딩: 개발용 PC(x64)에서 타겟 임베디드 기기(arm64)를 위한 실행 파일을 빌드할 수 있습니다. 타겟 기기에 직접 개발 환경을 구축하기 어려운 임베디드 개발의 생산성을 극적으로 향상시켜 줍니다.
  • 유연한 디스플레이 백엔드 지원: Wayland를 기본으로 사용하며, DRM(Direct Rendering Module)과 X11도 지원합니다. 다만 X11은 디버깅 용도로만 사용하고, 실제 제품에는 Wayland나 DRM을 사용하는 것이 권장됩니다.
  • 플러그인 생태계와 API 호환성: flutter-elinux-plugins 저장소를 통해 카메라, 비디오 플레이어 등 필수 플러그인을 제공합니다. 또한 공식 플러그인과 API 호환성을 유지하고, MethodChannel/EventChannel 같은 핵심 API도 동일하게 작동하여 일관된 개발 경험을 보장합니다.
  • 편리한 원격 개발 지원: custom-devices 기능을 통해 원격 타겟 장치에 앱을 설치하고 디버깅하는 워크플로우를 완벽하게 지원합니다. 이 기능은 잠시 후에 더 자세히 살펴보겠습니다.

flutter-elinux 시작하기

자, 이제 flutter-elinux를 직접 사용해볼까요?

 

1. 설치 준비

  • 운영체제: flutter-elinux는 공식적으로 리눅스 데스크톱(Ubuntu 20.04 이상 권장) 환경을 필요로 합니다. 하지만 리눅스 PC가 없어도 괜찮습니다. Windows 사용자라면 WSL(Windows Subsystem for Linux)을 사용하여 Windows 내에서 리눅스 환경을 손쉽게 구축할 수 있습니다.

WSL 로고WSL 실행 이미지

  • WSL이란?: WSL은 Windows에서 네이티브 리눅스 바이너리 실행 파일을 직접 실행할 수 있게 해주는 기능입니다. 별도의 가상 머신 없이도 리눅스 커널과 상호작용하며, 거의 완벽한 리눅스 환경을 제공하여 개발 생산성을 크게 높여줍니다.
  • WSL2 설치 및 설정: flutter-elinux 개발 환경, 특히 Docker와의 원활한 연동을 위해서는 WSL2가 필수적입니다.
    1. 설치: Windows PowerShell이나 명령 프롬프트를 관리자 권한으로 열고, 다음 명령어를 입력하면 WSL과 최신 Ubuntu 배포판이 WSL2 버전으로 설치됩니다.

      $ wsl --install버전 확인 및 업그레이드: 이미 WSL을 사용하고 있었다면, 아래 명령어로 현재 설치된 배포판의 버전이 2인지 확인하세요.
      $ wsl -l -v
      만약 VERSION이 1이라면, wsl --set-version <배포판_이름> 2 명령어를 통해 WSL2로 업그레이드할 수 있습니다.
    2. 초기 설정: 설치가 완료되면 Windows 시작 메뉴에서 Ubuntu를 찾아 실행하고, 초기 사용자 설정(사용자 이름 및 암호)을 마치면 모든 준비가 끝납니다. 이제 여러분의 Windows PC에서 flutter-elinux를 사용할 준비가 되었습니다.
  • 필수 라이브러리 설치: 터미널에서 아래 명령어로 필요한 도구들을 설치합니다.
$ sudo apt-get update
$ sudo apt-get install unzip curl clang cmake pkg-config

 

2.flutter-elinux 설치

flutter-elinux/opt 디렉토리에 설치하여 시스템 전역에서 사용하도록 설정합니다.

  1. 리포지토리 클론 및 이동:
    WSL 터미널에서 flutter-elinux 코드를 홈 디렉토리에 내려받은 후, /opt 디렉토리로 이동시킵니다.
$ cd ~
$ git clone https://github.com/sony/flutter-elinux.git
$ sudo mv flutter-elinux /opt/
  1. PATH 설정:
    /opt/flutter-elinuxbin 폴더를 PATH 환경 변수에 추가합니다.
$ export PATH="$PATH:/opt/flutter-elinux/bin"

: 터미널을 재시작할 때마다 이 설정을 유지하려면, WSL 터미널에서 nano ~/.bashrc 또는 nano ~/.zshrc 명령어로 쉘 설정 파일을 열고 맨 아래에 위 export 라인을 추가한 후 저장해주세요.

 

3. 설치 확인

아래 명령어로 설치가 잘 되었는지 확인합니다. elinux-waylandelinux-x11 장치가 보이면 성공입니다.

$ flutter-elinux devices

 

4. 프로젝트 생성 및 실행

  • 새 프로젝트 생성: 일반 Flutter 프로젝트처럼 create 명령어를 사용합니다.
$ flutter-elinux create my_elinux_app
  • 기존 프로젝트에 적용: 이미 프로젝트가 있다면, 해당 디렉토리로 이동해 아래 명령어로 elinux 지원을 추가할 수 있습니다.
$ flutter-elinux create .
  • 샘플 앱 실행: Wayland 환경에서 앱을 실행해봅시다.
    • 네이티브 리눅스 환경: Wayland 컴포지터(예: Sway, Weston)가 먼저 설치되고 실행되어 있어야 합니다.
    • WSL 환경 (Windows 11): 최신 버전의 WSL은 GUI 앱을 지원하는 WSLg가 기본적으로 포함되어 있습니다. WSLg는 자체적으로 Wayland 서버를 내장하고 있으므로, 별도의 컴포지터를 설치할 필요 없이 바로 Wayland 앱을 실행할 수 있습니다. Linux GUI 앱이 마치 네이티브 Windows 앱처럼 창으로 나타나는 편리함을 경험할 수 있습니다.

아래 명령어로 앱을 실행합니다.

$ cd my_elinux_app
$ flutter-elinux run -d elinux-wayland


심화 활용: 크로스 빌딩과 원격 디버깅

flutter-elinux의 진정한 힘은 개발 PC와 임베디드 기기를 넘나드는 크로스 빌딩과 원격 디버깅에서 드러납니다. 이 기능들은 임베디드 개발의 효율을 극적으로 높여주는 핵심입니다.

 

크로스 빌딩: 내 PC에서 임베디드 앱 빌드하기 (Docker + WSL)

크로스 빌딩은 개발 PC(x64)에서 타겟 임베디드 기기(arm64)용 실행 파일을 빌드하는 기술입니다. Windows 환경의 WSL 사용자를 기준으로, Docker를 활용해 크로스 빌딩 환경을 구축하는 방법을 자세히 알아보겠습니다.

 

1. Docker Desktop 설정

앞서 설명한 WSL2 환경이 준비되었다면, 이제 크로스 빌딩을 위해 Docker Desktop을 설정할 차례입니다. Docker는 컨테이너 기술을 통해 독립된 개발 환경을 손쉽게 만들어주는 강력한 도구입니다.

  • Docker Desktop 설치: Docker Desktop 공식 다운로드 페이지에서 Windows용 설치 파일을 받아 화면 지시에 따라 설치를 완료합니다. 설치 과정에서 "Use WSL 2 instead of Hyper-V" 옵션이 선택되어 있는지 확인하는 것이 좋습니다.
  • WSL 통합 설정: Docker Desktop 설치 후, Settings > Resources > WSL Integration 메뉴로 이동합니다. "Enable integration with my default WSL distro"를 켜고, Docker를 사용하려는 특정 배포판(예: Ubuntu)의 스위치를 활성화해주세요. "Apply & Restart" 버튼을 누르면 설정이 적용되며, 이제 WSL 터미널에서 docker 명령어를 바로 사용할 수 있습니다.

2. 크로스 빌딩 환경 구축 (Docker + QEMU)

x64 아키텍처의 PC에서 arm64용 코드를 컴파일하고 실행하기 위해, 호스트에는 크로스 컴파일러를, Docker 내부에는 QEMU 에뮬레이터와 타겟 시스템의 라이브러리(Sysroot)를 설정합니다.

  • 호스트(WSL)에 크로스 컴파일 도구 설치:
    먼저 WSL 터미널에 ARM64용 크로스 컴파일러를 설치합니다.
$ sudo apt-get update
$ sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
  • QEMU 설정:
    다른 아키텍처의 Docker 이미지를 실행하기 위해, WSL 터미널에서 아래 명령어로 QEMU 핸들러를 등록합니다. (최초 한 번만 실행)
$ sudo docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
  • ARM64 Sysroot 생성:
    이제 arm64용 Docker 이미지를 실행하여 빌드에 필요한 라이브러리 모음(Sysroot)을 만듭니다.
  1. Docker 컨테이너 실행: 타겟 환경과 유사한 ubuntu:20.04 이미지를 예시로 사용합니다. --platform 플래그로 아키텍처를 명시합니다.
    $ sudo docker run --platform linux/arm64 -itd --name arm64-builder arm64v8/ubuntu:20.04
    $ sudo docker exec -it arm64-builder /bin/bash
  2. 컨테이너 내 라이브러리 설치: 컨테이너에 접속된 상태에서, 임베디드 환경 빌드에 필요한 라이브러리들을 설치합니다.
    $ apt-get update
    $ apt-get install -y clang cmake build-essential pkg-config \
                       libegl1-mesa-dev libxkbcommon-dev libgles2-mesa-dev \
                       libwayland-dev wayland-protocols libdrm-dev libgbm-dev \
                       libinput-dev libudev-dev libsystemd-dev libfontconfig1-dev
    참고: 라이브러리 설치 중 시간대(Timezone) 설정 프롬프트가 나타날 수 있습니다. 만약 잘못 설정했거나 변경이 필요하다면, dpkg-reconfigure tzdata 명령어를 실행하여 Asia > Seoul 순서로 다시 설정할 수 있습니다.
  3. Sysroot 복사: 설치가 끝나면 exit로 컨테이너를 빠져나온 후, 컨테이너 안의 파일 시스템을 WSL 환경으로 복사합니다.
    $ sudo docker cp arm64-builder:/ ./ubuntu20-arm64-sysroot

3. 앱 크로스 빌딩 실행

모든 준비가 끝났습니다. 이제 flutter-elinux 명령어로 앱을 빌드합니다. --target-compiler-triple 옵션을 추가하여 크로스 컴파일러를 명시해주는 것이 중요합니다.

$ flutter-elinux build elinux --target-arch=arm64 --target-compiler-triple=aarch64-linux-gnu --target-sysroot=<Absolute_path_to>/ubuntu20-arm64-sysroot --debug
  • <Absolute_path_to> 부분은 위에서 Sysroot를 복사한 디렉토리의 절대 경로로 변경해야 합니다. (예: /home/user/my_elinux_app/ubuntu20-arm64-sysroot)
  • 문제 해결: 만약 빌드 중 bits/c++config.h 파일을 찾지 못하는 오류가 발생하면, --system-include-directories 옵션을 추가하여 호스트의 C++ 헤더 경로를 직접 지정해 해결할 수 있습니다.
# 예시: g++-aarch64-linux-gnu 9.x 버전을 사용하는 경우
$ flutter-elinux build elinux --target-arch=arm64 \
    --target-sysroot=<Absolute_path_to>/ubuntu20-arm64-sysroot \
    --target-compiler-triple=aarch64-linux-gnu \
    --system-include-directories=/usr/aarch64-linux-gnu/include/c++/9/aarch64-linux-gnu

빌드된 결과물은 프로젝트 내의 ./build/elinux/arm64/debug/bundle과 같은 경로에 생성됩니다.

원격 타겟 개발 (custom-devices)

custom-devices 기능은 원격 기기와의 연동을 매우 편리하게 만들어 줍니다.

  1. ~/.flutter_custom_devices.json 파일 생성: 홈 디렉토리에 원격 기기의 정보를 담은 JSON 파일을 만듭니다. 기기의 ID, 이름, IP 주소, 접속 정보 및 앱 설치/실행/제거에 필요한 명령어들을 정의할 수 있습니다.
{
  "custom-devices": [
    {
      "id": "jetson-nano",
      "label": "Jetson Nano",
      "sdkNameAndVersion": "JetPack 4.3",
      "enabled": true,
      "platform": "arm64",
      "backend": "x11",
      "ping": [
        "ping",
        "-w",
        "500",
        "-c",
        "1",
        "<deviceIP>"
      ],
      "pingSuccessRegex": "ttl=",
      "install": [
        "scp",
        "-P",
        "<devicePort>",
        "-r",
        "${localPath}",
        "<deviceUser>@<deviceIP>:/tmp/${appName}"
      ],
      "uninstall": [
        "ssh",
        "-p",
        "<devicePort>",
        "<deviceUser>@<deviceIP>",
        "rm -rf \"/tmp/${appName}\""
      ],
      "runDebug": [
        "ssh",
        "-p",
        "<devicePort>",
        "<deviceUser>@<deviceIP>",
        "export DISPLAY=:0 && /tmp/${appName}/${appName} -b ."
      ],
      "stopApp": [
        "ssh",
        "-p",
        "<devicePort>",
        "<deviceUser>@<deviceIP>",
        "ps aux | grep \"/tmp/${appName}\" | grep -v grep | awk '{print $2}' | xargs kill"
      ],
      "forwardPort": [
        "ssh",
        "-p",
        "<devicePort>",
        "-o",
        "ExitOnForwardFailure=yes",
        "-L",
        "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}",
        "<deviceUser>@<deviceIP>"
      ],
      "forwardPortSuccessRegex": "Linux"
    }
  ]
}
  1. 원격 빌드 및 실행: 이제 -d 옵션에 기기 ID를 지정하여 원격으로 앱을 빌드하고 실행할 수 있습니다.
# 원격 타겟용으로 빌드
$ flutter-elinux build elinux -d jetson-nano --target-arch=arm64 --target-compiler-triple=aarch64-linux-gnu --target-sysroot=<Absolute_path_to>/ubuntu20-arm64-sysroot --target-backend-type=x11 --debug

# 원격 기기에서 실행
$ flutter-elinux run -d jetson-nano

 

이 명령을 실행하면 flutter-elinux가 JSON 파일에 정의된 installrunDebug 명령을 순서대로 실행하여 앱을 배포하고 디버그 모드로 실행해 줍니다.


결론

오늘 우리는 flutter-elinux가 임베디드 리눅스 환경에서 Flutter 개발의 문을 어떻게 활짝 열어주는지 살펴보았습니다. 경량 설계, 폭넓은 호환성, 강력한 크로스 빌딩과 원격 개발 지원까지. flutter-elinux는 임베디드 개발자들이 마주하는 많은 어려움을 해결해 줄 강력한 잠재력을 지니고 있습니다.

물론 크로스 빌딩처럼 다소 복잡한 과정도 있지만, flutter-elinux가 제공하는 유연성과 생산성은 그만한 노력을 투자할 가치가 충분합니다. 새로운 임베디드 프로젝트를 구상 중이거나, 기존 시스템에 현대적인 UI/UX를 더하고 싶다면 flutter-elinux를 꼭 한번 고려해 보시길 바랍니다.

최근 Flutter 프로젝트에서 홈 화면 위젯을 개발할 기회가 있었습니다. 네이티브 개발 경험이 많지 않은 상태에서 위젯을 만들다 보니 여러 시행착오를 겪었습니다. 특히 Glance의 Text Composable의 제한적인 스타일링 기능과, 사용자가 위젯 크기를 조절할 때 자연스럽게 UI가 반응하도록 만드는 부분이 가장 까다로웠습니다.

이 글은 저처럼 위젯을 만들면서 비슷한 어려움을 겪는 분들께 도움이 되고자 작성했습니다. 제가 문제를 해결하며 찾은 방법들, 특히 커스텀 폰트 적용 및 세밀한 텍스트 스타일링다양한 위젯 크기에 대응하는 반응형 레이아웃 구현을 중심으로 실용적인 팁들을 공유하고자 합니다.


완성된 위젯 미리보기

이번 글에서 만들 뉴스 위젯의 모습입니다. 뉴스 데이터는 공공데이터포털의 국제방송교류재단_뉴스기사API를 이용하였습니다. 사용자가 위젯의 크기를 조절하면, 위젯 내부의 콘텐츠도 그에 맞춰 동적으로 변경됩니다. 아래는 크기를 다르게 조절했을 때의 예시입니다.

 


Flutter와 네이티브 위젯 연동: home_widget

Flutter 앱에서 안드로이드 홈 위젯과 데이터를 주고받기 위해 home_widget이라는 Flutter 플러그인을 사용했습니다. 이 패키지는 Flutter와 네이티브 위젯 간의 통신 다리 역할을 해주어, Flutter 코드에서 위젯의 데이터를 업데이트하거나 위젯의 액션에 반응할 수 있게 해줍니다. 더 자세한 사용법은 공식 문서에서 확인하실 수 있습니다.

home_widget은 데이터 통신을 담당할 뿐, 위젯의 UI는 각 플랫폼의 네이티브 코드로 직접 만들어야 합니다. 안드로이드에서는 Jetpack Glance를 사용해 위젯 UI를 구현했습니다. 이제부터 Glance로 위젯을 개발하는 과정을 본격적으로 살펴보겠습니다.


1. Glance로 위젯 개발 시작하기

위젯 개발을 위해 여러 방법을 찾아보던 중, Jetpack Compose와 유사한 선언형 방식으로 UI를 만들 수 있는 Glance를 사용하기로 했습니다. Compose에 익숙하다면 훨씬 쉽게 적응할 수 있다는 장점이 컸습니다.

Glance의 핵심 구성 요소

Glance로 위젯을 만들려면 두 가지 핵심 클래스를 알아야 합니다.

  • GlanceAppWidget: 위젯의 UI 콘텐츠를 정의하는 곳입니다. @Composable 함수를 사용해 UI를 구성합니다.
  • GlanceAppWidgetReceiver: 위젯의 업데이트 요청 등 시스템 브로드캐스트를 수신하는 AppWidgetProvider입니다.

기존 XML 방식보다 좋은 점

오랫동안 안드로이드 위젯은 XML 레이아웃과 RemoteViews를 조합해서 만들어야 했습니다. 이 방식은 몇 가지 불편한 점이 있었죠.

  • 절차적인 업데이트: RemoteViewssetTextViewText, setImageViewResource 같은 메서드를 일일이 호출하여 UI를 업데이트해야 했습니다. 코드가 길어지고 직관적이지 않았습니다.
  • 상태 관리의 어려움: 위젯의 상태가 변경될 때마다 UI를 수동으로 동기화하는 로직을 직접 관리해야 했습니다.
  • 제한적인 개발 환경: UI 변경 사항을 확인하려면 매번 위젯을 다시 설치해야 해서 개발 속도가 더뎠습니다.

Glance는 이런 문제들을 해결합니다.

  • 선언형 UI: "상태가 이러면, UI는 이렇다"라고 선언만 하면 되므로 코드가 간결하고 이해하기 쉽습니다.
  • 익숙한 Compose 문법: Jetpack Compose를 안다면 거의 모든 지식을 그대로 활용할 수 있습니다.
  • 편리한 상태 관리: GlanceStateDefinition으로 위젯의 상태를 쉽게 저장하고 불러올 수 있습니다.
  • 미리보기 지원: Android Studio에서 UI를 미리 볼 수 있어 개발 속도가 훨씬 빨라집니다.

Jetpack Compose와는 어떻게 다를까요?

Glance는 Compose와 비슷하지만, 위젯이라는 특수한 환경 때문에 몇 가지 차이가 있습니다.

  • 목적: Glance는 앱 UI 전체가 아닌 홈 화면 위젯 제작에 특화되어 있습니다.
  • API 제한: Row, Column, Text 등 기본적인 Composable만 사용할 수 있고, 복잡한 애니메이션이나 커스텀 드로잉은 지원되지 않습니다.
  • 실행 환경: Glance 코드는 UI를 직접 그리지 않고, RemoteViews 객체를 만드는 역할을 합니다. 실제 UI는 홈 화면 런처 프로세스에서 그려집니다.

2. 기본 레이아웃 구성하기

Glance의 레이아웃 구성은 Jetpack Compose와 거의 동일합니다. Column을 사용해 위젯을 세로로 쌓고, Row로 가로로 배열하며, TextImage로 콘텐츠를 표시합니다. Composable 함수에 GlanceModifier를 적용하여 패딩, 정렬, 배경 등 다양한 속성을 설정할 수 있습니다.

@Composable
fun MyWidgetContent() {
    Column(
        modifier = GlanceModifier.fillMaxSize().padding(16.dp).background(Color.White),
        verticalAlignment = Alignment.Vertical.CenterVertically,
        horizontalAlignment = Alignment.Horizontal.CenterHorizontally
    ) {
        Text("안녕하세요, Glance!")
        Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
            Image(
                provider = ImageProvider(R.drawable.ic_launcher_foreground),
                contentDescription = "icon",
                modifier = GlanceModifier.size(24.dp)
            )
            Text("위젯입니다.")
        }
    }
}

3. 심화 팁: UI 커스터마이징

Glance로 기본 레이아웃을 구성하고 보니 바로 첫 번째 문제에 부딪혔습니다. 디자인 시안에 맞춰 폰트를 적용하고 자간과 행간을 세밀하게 조절해야 했는데, Glance의 기본 Text Composable은 이를 지원하지 않았습니다. 이 문제를 해결하기 위해 찾은 방법은 텍스트를 직접 비트맵으로 그려서 Image로 표시하는 것이었습니다.

폰트, 자간(letter-spacing), 행간(line-spacing) 커스터마이징

TextPaintStaticLayout을 사용하면 원하는 폰트, 색상, 자간, 행간을 모두 적용한 텍스트를 비트맵으로 만들 수 있습니다. 코드는 조금 복잡해지지만, 디자인 제약에서 완전히 벗어날 수 있다는 큰 장점이 있습니다.

아래는 관련 기능을 모아둔 유틸리티 함수 예시입니다.

// MultiLineTextAsBitmap.kt
fun multiLineTextAsBitmap(
    context: Context,
    text: String,
    width: Int,
    textSize: Float,
    textColor: Int = Color.BLACK,
    fontResId: Int,
    lineSpacingMultiplier: Float = 1.0f,
    letterSpacing: Float = 0f,
    maxLines: Int = Int.MAX_VALUE,
    // ...
): Bitmap {
    val textPaint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply {
        this.color = textColor
        this.textSize = textSize * context.resources.displayMetrics.density
        this.typeface = ResourcesCompat.getFont(context, fontResId)
        this.letterSpacing = letterSpacing
    }
    val staticLayout =
        StaticLayout.Builder.obtain(text, 0, text.length, textPaint, width)
            .setLineSpacing(0f, lineSpacingMultiplier)
            // ...
            .build()

    val bitmap = createBitmap(staticLayout.width, staticLayout.height)
    val canvas = Canvas(bitmap)
    staticLayout.draw(canvas)
    return bitmap
}

이미지에 그라데이션 추가하기

이미지 위에 텍스트를 표시할 때 배경 때문에 글자가 잘 안 보인다면, 이미지 위에 어두운 그라데이션을 깔아 가독성을 높일 수 있습니다. 이 역시 비트맵을 직접 다루어 구현합니다.

// BitmapUtils.kt
fun createBitmapWithGradient(
    originalBitmap: Bitmap,
    colors: IntArray,
    positions: FloatArray?
): Bitmap {
    val resultBitmap = createBitmap(originalBitmap.width, originalBitmap.height)
    val canvas = Canvas(resultBitmap)

    canvas.drawBitmap(originalBitmap, 0f, 0f, null)

    val gradient = LinearGradient(
        0f, 0f, 0f, originalBitmap.height.toFloat(),
        colors, positions, Shader.TileMode.CLAMP
    )
    val paint = Paint().apply { shader = gradient }
    canvas.drawRect(0f, 0f, originalBitmap.width.toFloat(), originalBitmap.height.toFloat(), paint)

    return resultBitmap
}

4. 크기에 따라 변하는 반응형 위젯 만들기

두 번째 큰 문제는 사용자가 위젯 크기를 조절할 때 그에 맞춰 콘텐츠를 자연스럽게 보여주는 것이었습니다. 위젯이 너무 작아지면 글자가 깨지거나 잘리고, 너무 커지면 공간이 비어 보이는 문제를 해결해야 했습니다.

1단계: 위젯 크기 조절 옵션 켜기

먼저 위젯의 크기 조절이 가능하다고 시스템에 알려줘야 합니다. appwidget-provider XML 파일에서 android:resizeMode 속성을 설정합니다. 이번 예시에서는 위젯을 수평, 수직으로 모두 조절할 수 있도록 설정합니다.

  • horizontal: 수평 조절만 허용
  • vertical: 수직 조절만 허용
  • horizontal|vertical: 양방향 조절 허용
<!-- news_widget_info.xml -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:resizeMode="horizontal|vertical" />

2단계: 위젯 크기 변경 감지하기

GlanceAppWidget 클래스에서 sizeMode 속성을 오버라이드하여 위젯 크기를 어떻게 처리할지 정합니다. 반응형 UI를 만들려면 SizeMode.Exact를 사용하는 것이 좋습니다. 크기가 바뀔 때마다 Content() 함수가 다시 호출되어 UI를 새로 그릴 수 있습니다.

// NewsWidget.kt
class NewsWidget : GlanceAppWidget() {
    override val sizeMode: SizeMode = SizeMode.Exact
    // ...
}

3단계: 정확한 위젯 크기 가져오고 UI 구성 결정하기

Content() 함수 안에서 LocalSize.current를 쓰면 현재 크기를 알 수 있지만, 위젯이 처음 생길 때 등 일부 상황에서는 정확하지 않을 수 있습니다. 가장 확실한 방법은 AppWidgetManager를 통해 런처가 알려주는 크기 정보를 직접 가져오는 것입니다.

// NewsWidget.kt
fun getWidgetSizeInfo(context: Context, glanceId: GlanceId): DpSize? {
    if (glanceId !is AppWidgetId) return null
    val appWidgetManager = AppWidgetManager.getInstance(context)
    val options = appWidgetManager.getAppWidgetOptions(glanceId.appWidgetId)
    val maxWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0).dp
    val maxHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0).dp

    return if (maxWidth > 0.dp && maxHeight > 0.dp) DpSize(width = maxWidth, height = maxHeight) else null
}

위젯 크기 계산 로직 흐름도

위젯의 정확한 크기를 가져와 UI 설정을 계산하는 과정은 여러 단계를 거칩니다. 특히 위젯이 처음 생성될 때는 시스템이 크기 정보를 즉시 제공하지 않을 수 있어, 약간의 지연과 재시도 로직이 필요합니다. 이 부분이 처음에는 가장 까다롭게 느껴질 수 있습니다.

이렇게 얻은 크기 정보와 위젯의 타입(cellType) 등을 calculateWidgetUiConfig 함수에 전달하여 현재 상태에 가장 적합한 UI 설정을 계산합니다. 이 함수는 위젯의 너비와 높이에 따라 동적으로 폰트 크기, 아이템 개수, 제목 줄 수 등을 계산하여 WidgetUiConfig 객체를 반환합니다.

// config/WidgetUiConfigCalculator.kt
@Composable
fun calculateWidgetUiConfig(
    isPreviewMode: Boolean,
    actualSize: DpSize,
    fontScale: Float
): WidgetUiConfig {
    val widgetActualHeightDp = if (isPreviewMode) {
        REFERENCE_HEIGHT_2_CELL_DP
    } else {
        actualSize.height
    }
    val widgetActualWidthDp = actualSize.width

    val referenceWidthForScale = REFERENCE_WIDTH_2_CELL_DP
    val scaleFactor: Float = widgetActualWidthDp.value / referenceWidthForScale.value
    val titleFontSizeSp: TextUnit = (14f * scaleFactor / fontScale).sp

    // 이 레이아웃에서는 아이템은 1개, 제목은 최대 3줄로 설정합니다.
    val numItemsToDisplay = 1
    val itemTitleLineLimit = 3

    val totalVerticalPaddingDp = (WIDGET_HORIZONTAL_PADDING_FACTOR * 2 * scaleFactor).dp
    val logoHeightDp = (LOGO_MAX_HEIGHT_SMALL_DP * scaleFactor).dp
    val headerPaddingBottomDp = (WIDGET_HEADER_PADDING_BOTTOM_FACTOR * scaleFactor).dp
    val headerHeightDp = logoHeightDp + headerPaddingBottomDp

    val newsListAreaHeightDp = (widgetActualHeightDp - totalVerticalPaddingDp - headerHeightDp).coerceAtLeast(0.dp)

    val itemSpacingDp = (NEWS_ITEM_SPACING_FACTOR * scaleFactor).dp
    val totalSpacingHeightDp = 0.dp // 아이템이 하나이므로 간격은 0입니다.

    val availableHeightForItems = (newsListAreaHeightDp - totalSpacingHeightDp).coerceAtLeast(0.dp)
    val maxItemImageSizeDp = (availableHeightForItems / numItemsToDisplay).coerceAtLeast(30.dp)

    return WidgetUiConfig(
        scaleFactor,
        titleFontSizeSp,
        itemTitleLineLimit,
        numItemsToDisplay,
        widgetActualHeightDp,
        widgetActualWidthDp,
        maxItemImageSizeDp,
        logoHeightDp,
        headerPaddingBottomDp,
        fontScale
    )
}

4단계: 크기에 맞춰 UI 동적으로 렌더링하기

이제 LaunchedEffect로 위젯 크기를 가져온 뒤, calculateWidgetUiConfig 함수를 호출하여 UI 설정을 얻고 그에 맞는 UI를 보여주면 됩니다. 실제 렌더링 로직은 GlanceContent와 같은 별도의 Composable 함수에서 관리하는 것이 좋습니다.

// NewsWidget.kt
@Composable
internal fun GlanceContent(cellType: Int, currentState: HomeWidgetGlanceState, isPreviewMode: Boolean = false, glanceId: GlanceId) {
    // ... 위젯 크기 가져오는 로직 ...
    val actualSize = getWidgetSizeInfo(context, glanceId)

    if (isLoadingSize || actualSize == null) {
        LoadingWidget()
    } else {
        val uiConfig = calculateWidgetUiConfig(isPreviewMode, actualSize, fontScale)
        // ...

        // 위젯 UI를 렌더링합니다.
        NewsWidgetItemLayout(
            context = context,
            newsItem = itemsToActuallyDisplay.firstOrNull(),
            uiConfig = uiConfig
        )
    }
}

마무리하며

이번 글에서는 Flutter 프로젝트의 일부로 안드로이드 위젯을 만들면서 겪었던 문제들과 그 해결 과정을 공유해 보았습니다. 특히 Glance의 기본 기능을 넘어서는 UI 커스터마이징과 반응형 레이아웃 구현에 초점을 맞췄습니다. Glance 덕분에 선언적이고 직관적인 코드로 위젯을 만들 수 있었지만, 실무적인 요구사항을 만족시키기 위해서는 약간의 추가적인 노력이 필요했습니다.

다음 포스트에서는 위젯의 상태를 관리하고 사용자와 상호작용하며 데이터를 주기적으로 업데이트하는 방법, 그리고 다양한 크기의 위젯 레이아웃을 추가하는 방법에 대해서도 더 깊이 다뤄보겠습니다.


전체 코드 보기

이번 글에서 작성한 전체 코드는 아래 GitHub Gist에서 확인하실 수 있습니다.

전체 코드 보기 (GitHub Gist)

이번엔 라우팅(Routing)에 대해 해보도록 하겠습니다.

 

라우팅이 필요한 이유

웹에서는 링크를 통해 해당 페이지로 이동해야해서 라우팅 작업이 필수이지만,

앱에서도 필요할까 싶은 경우가 많을겁니다.

 

물론 단순히 뒤로 가고, 해당 페이지로 이동하는 작업만 필요할 때는 굳이 쓸 필요는 없는 건 맞습니다.

 

다만 추후에 앱에 딥링킹(Deep-Linking)작업을 하게 된다면 라우팅 작업이 의미가 커지게 됩니다.

 

딥링킹이란 외부에서 링크를 통해 앱을 들어가게 될 때, 해당 페이지로 이동하는 것인데

딥링크를 이용해서 어디로부터 유입되어서 앱을 설치했는지를 파악할 수 있어

마케팅쪽에서 중요한 작업이라고 할 수 있습니다.

 

기존에는 Firebase Dynamic Link를 이용해서 처리할 수도 있었지만 다이나믹 링크는 2025년 8월 25일에 서비스 종료 예정이기에, 다른 툴을 이용해야하는데 다른 툴들은 비용이 드는 경우가 많아 비용 절감을 위해서 자체 구현해보는 것도 좋을것입니다.

 

라우팅 라이브러리로 go_router를 선택한 이유

go_router를 쓴 이유라고 하면,

  • 웹과 비슷한 계층적 처리
  • flutter에서 직접 관리하는 라이브러리(routing 라이브러리중 많은 이용자수)
  • build_runner를 이용한 generated 처리 가능(코드 단순화)

이렇기에 go_router를 선택하게 되었습니다.

 

그래서 이번 포스팅에서는 추후 관리자 기능을 위해 로그인 페이지를 만들고 링크를 바꾸면 맞는 페이지가 나오게 하는것까지만 진행해보기로 해보겠습니다.

 

go_router 설치

우선 go_router를 설치하도록 하겠습니다.

설치방법은 여기서 확인 가능합니다만, 간단한 작업이므로 포스트에서도 같이 안내해드리겠습니다.

flutter pub add go_router

이렇게 하여 go_router를 설치해주시면 됩니다.

 

App Router 기본 세팅

import 'package:flutter_blog/main.dart';
import 'package:go_router/go_router.dart';

final appRouter = GoRouter(routes: [
  GoRoute(
      path: '/',
      builder: (context, state) {
        return const MyHomePage(title: 'Flutter Demo Home Page');
      })
]);

이제 GoRouter 기본 세팅을 해줍니다.

우선 저같은 경우는 lib폴더에 routes란 폴더를 하나 만들고, app_router.dart란 파일을 만들어서 다음과 같이 코드를 짰습니다.

 

routes는 여러 route들의 집합이고, 여기에 route정보를 넣어야 route가 가능해지게 됩니다.

그리고 GoRoute는 Route 정보를 넣어주는 것으로 path는 링크, builder를 통해 해당 링크를 들어가게 되면 보여질 페이지를 전달해주는 것입니다.

물론 GoRoute말고도 다른 방식이 있는데, 이 부분은 추후 탭페이지 만들 때 더 얘기해보겠습니다.

 

우선 저렇게 세팅한 다음 main.dart로 가서 router연결을 해주도록 합시다.

import 'package:flutter/material.dart';
import 'package:flutter_blog/routes/app_router.dart';

// 중략

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      routerConfig: appRouter,
    );
  }
}

 

기존에 MaterialApp을 MaterialApp.router로 변경해주고, home을 제거해주고 그 자리에 routerConfig를 추가해주고 아까 만들었던 appRouter를 넣어줍니다. 그리고 실행해봅니다. 그러면 실행 잘 되는 걸 확인 가능합니다.

 

로그인 링크 추가

그러면 이제 로그인 링크를 추가해보도록 합시다.

우선 이제 메인에 있는 MyHomePage 지우고, 본격적으로 폴더를 구분해두도록 해보겠습니다.

저는 features란 폴더를 만들고, 각각 home, log_in이란 폴더를 만들고, 각각 폴더에 home_screen, log_in_screen을 만들도록 하겠습니다. 모두 일단은 기본 screen으로 만들어줍니다.

home_screen

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("홈")),
      body: Text("홈 페이지"),
    );
  }
}

log_in_screen

import 'package:flutter/material.dart';

class LogInScreen extends StatefulWidget {
  const LogInScreen({Key? key}) : super(key: key);

  @override
  _LogInScreenState createState() => _LogInScreenState();
}

class _LogInScreenState extends State<LogInScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("로그인")),
      body: Text("로그인 페이지"),
    );
  }
}

 

그리고 app_router.dart로 가서 라우팅 세팅을 해줍니다.

import 'package:flutter_blog/features/home/home_screen.dart';
import 'package:flutter_blog/features/log_in/log_in_screen.dart';
import 'package:go_router/go_router.dart';

final appRouter = GoRouter(routes: [
  GoRoute(
      path: '/',
      builder: (context, state) {
        return const HomeScreen();
      }),
  GoRoute(
      path: '/login',
      builder: (context, state) {
        return const LogInScreen();
      }),
]);

 

그리고 실행해봅니다.

 

근데 이상하게 오류가 있습니다?

분명 주소를 뒤에 /login으로 붙였는데도 홈 페이지가 보입니다.

이거는 어떻게 해결해야할까요?

 

주소에 맞는 페이지가 안 나오는 문제 해결

import 'package:flutter/material.dart';
import 'package:flutter_blog/routes/app_router.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

void main() {
  setUrlStrategy(PathUrlStrategy()); // 추가
  runApp(const MyApp());
}

// 후락

main 실행할 때 해당 코드를 추가해줍니다.

기존 Flutter의 경우에는 hash방식의 url 정책을 이용하는데, 그러면 go_router에서 인식을 하지 못해

웹과 같은 path방식의 url 정책으로 변경해서 go_router에서 링크를 인식할 수 있게 해줍니다.

해당 작업을 적용하고 다시 실행해봅시다.

 

그러면 정상적으로 로그인 페이지가 보이게 됩니다!

 

다음 작업

다음 작업으로는 go_router와 riverpod을 이용하여 로그인이 안된 상태일 때는 로그인 페이지를, 로그인이 된 상태이면 홈 페이지가 보이게 설정하는 작업을 진행해보겠습니다.

해당 부분만 구현되면 routing 작업에서 큰 고비는 넘겼다고 볼만하겠네요!

이전 포스트에서 Flutter Web을 Github Pages에 올리는 방법에 대해 작성했는데, 해당 방식의 단점이라고 하면 Web으로 빌드된 파일로만 처리해야해서 프로젝트 코드를 모두 올릴 수가 없어 관리가 어렵다는 것이 있습니다.

 

그래서 web 브랜치를 새로 만들어서 web 브랜치에는 빌드된 웹페이지 파일을 올려서 해당 branch로 Github Pages가 되게 하고, main 브랜치flutter 프로젝트 파일을 올리는 방식으로 진행합니다.

 

이것을 구현하기 위해 Github Actions를 이용할 예정입니다. 해당 툴을 이용하게 되면 이전처럼 일일이 빌드하고, 배포할 필요 없이 git에 코드를 push하는 것만으로 빌드, 테스트, 배포까지 되어 시간을 단축할 수 있다는 장점이 있습니다. 이러한 개념을 CI(지속적 통합)/CD(지속적 배포)라고 합니다.

 

CI/CD에 대한 개념은 직접 적는 것보다는 해당 글을 참고하시는 게 더 좋을 것 같아 참고 링크로 대신합니다.

https://www.redhat.com/ko/topics/devops/what-is-ci-cd

 

CI/CD(CI CD, 지속적 통합/지속적 배포): 개념, 툴, 구축, 차이

CI/CD는 애플리케이션의 통합 및 테스트 단계부터 제공 및 배포까지 애플리케이션 라이프사이클 전체에서 지속적인 자동화와 지속적인 모니터링을 제공하는 것을 뜻합니다.

www.redhat.com

 

그러면 먼저 기존에 test용으로 이용했던 레포지토리를 제거하고 다시 새로운 레포지토리를 만들어보도록 하겠습니다.

 

테스트용 레포지토리 제거(이전 블로그를 따라하지 않았다면 생략)

해당 레포지토리 페이지에 들어가서 Settings 탭에 들어갑니다.

General 섹션 맨 밑으로 가게 되면 "Delete this repository"가 있는데 해당 버튼을 누르고 삭제하시면 됩니다.

 

Blog 레포지토리 생성

레포지토리를 생성하는데, 여기서 유의할 것은 github pages 뒤에 들어갈 path와 똑같이 작성해주셔야 합니다.

다음과 같이 레포지토리가 만들어졌다면 이제 기존에 이용했던 프로젝트를 git과 연결해주도록 해봅시다.

 

프로젝트 Git 세팅

우선 해당 포스팅에서는 git 설치에 관해서는 따로 얘기하진 않고 해당 링크로 대신합니다.

https://git-scm.com/book/ko/v2/%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-Git-%EC%84%A4%EC%B9%98

 

Git - Git 설치

이 책은 Git 2.0.0 버전을 기준으로 썼다. 대부분의 명령어는 그 이전 버전에서도 잘 동작하지만, 몇 가지 기능은 아예 없거나 미묘하게 다를 수 있다. Git의 하위 호환성은 정말 훌륭하기 때문에 2.0

git-scm.com

 

git이 설치되어있다는 가정하에 ide나 command로 해당 프로젝트 폴더로 들어가고 command에 다음과 같이 실행합니다.

git init

 

그 다음에 현재 있는 모든 파일을 git에 추가하여 commit해줍니다.

git add .
git commit -m "first init"

 

그리고 이제 Github 프로젝트와 연결해주는데 각자 git 주소가 달라 정확히는 Github 레포의 "or push an existing repository from the command line" 섹션의 코드를 그대로 하시면 됩니다. 

git remote add origin {Github 레포 git주소}
git branch -M main
git push -u origin main

 

그러면 이제 Flutter 프로젝트가 Github 레포에 올라가 있는 것을 확인할 수 있습니다.

 

Github Actions 세팅

이제 자동화를 위해 Github Actions를 세팅해줍시다.

Github Actions에 대한 정보는 해당 링크로 대체합니다.

https://docs.github.com/ko/actions

 

GitHub Actions 설명서 - GitHub Docs

GitHub Actions를 사용하여 리포지토리에서 바로 소프트웨어 개발 워크플로를 자동화, 사용자 지정 및 실행합니다. CI/CD를 포함하여 원하는 작업을 수행하기 위한 작업을 검색, 생성 및 공유하고 완

docs.github.com

 

Github Actions를 더 다양하게 이용하려면 Github Actions 관련 문법과 지원하는 기능들을 이해하고 있어야해서 이번 포스팅의 기능 이외에 더 다양한 경우(branch에 따라 다르게 하고 싶은 경우, flavor에 따라 다르게 하고 싶은 경우, iOS/Android 빌드 등)에는 Github Actions 문서를 참고해서 만들어보시면 됩니다.

추후 iOS/Android 빌드에 대해서는 따로 포스팅해보도록 하겠습니다.

이번 포스팅에서는 Web 빌드와 Github Pages 배포만을 구현해보도록 하겠습니다.

 

우선 조건은 다음과 같습니다.

  1. main 브랜치에 push가 될 때 트리거가 되어야함.
  2. 트리거 될 때 flutter build web 처리를 한다.
  3. 빌드된 web 파일을 web 브랜치로 옮겨서 해당 파일로 Github Pages에 Deploy한다.

 

그런데 여기서 몇 가지가 추가되어야합니다.

Github Actions는 주로 가상 머신에서 동작하는데, 해당 머신에는 Flutter가 설치가 안되어있어 Flutter를 세팅해주는 기능도 추가해주고 혹시나 기존에 있는 cache도 있을수 있으니 관련 cache도 지워주고, pub도 새로 받아야 할겁니다.

또한 Github Repository에 access도 필요합니다.

그래서 몇몇 조건을 추가하면 순서가 다음과 같이 구현되어야합니다.

  1. main 브랜치에 push가 될 때 트리거가 되어야함.
  2. 트리거 될 때 Github Repository에 access를 해준다.
  3. 그다음 flutter 세팅을 해준다.
  4. flutter 세팅이 되면 먼저 기존 flutter pub 관련 캐시를 지워준다.
  5. flutter pub을 다시 가져온다.
  6. flutter pub을 가져왔다면, 이제 flutter web을 빌드한다.
  7. 빌드된 web 파일을 web 브랜치로 옮겨서 해당 파일로 Github Pages에 Deploy한다.

이게 하나의 step이 되면 우리가 원하는 기능을 구현할 수 있게 될겁니다.

그러면 해당 step을 코드로 구현하면 다음과 같습니다.

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  build_web:
    name: Deploy Flutter Web to GitHub Pages
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Github 레포지토리 세팅
        uses: actions/checkout@v4
      - name: Flutter 세팅
        uses: subosito/flutter-action@v2
        with:
          channel: stable
      - name: 캐시된 빌드, Pub 초기화
        run: flutter clean
      - name: Pub 새로 가져오기
        run: flutter pub get
      - name: 플러터 웹 빌드
        run: flutter build web --base-href "/blog/"
      - name: Github Pages 배포
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_branch: web
          publish_dir: ./build/web

 

해당 코드에 대해 하나씩 소개해드리도록 하겠습니다.

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

name: 해당 workflow의 이름

on: 트리거 처리

push: 푸시할 경우

branches: 해당 브랜치에

- main: main 브랜치

정리하면 "main브랜치에 푸시할 경우 트리거 처리한다."를 코드로 구현된 것입니다.

 

jobs:
  build_web:
    name: Deploy Flutter Web to GitHub Pages
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:

jobs: 실행할 job 리스트

build_web: job 아이디

name: job 이름

runs-on: 해당 job을 돌릴 runner. 여기서는 ubuntu 최신 버전 runner를 이용한다.

permissions: 권한 추가

"contents:write" : 컨텐츠 write 권한

steps: step 목록

현재까진 web만 빌드해도 되기에 기본적으로 Github에서 제공해주는 ubuntu-latest를 이용합니다. 하지만 iOS/macOS, Windows 빌드를 하려면 각각 Mac, Windows 러너가 필요합니다.
해당 프로젝트는 Public 레포지토리에서 진행하기 때문에 Runner에 대해 무료이나, Private 레포지토리에서는 유료이기에 돈이 든다는 부담감이 있으면 자기가 갖고 있는 로컬 머신을 이용한 Self-Hosted Runner를 이용하셔야합니다. 관련 정보는 해당 링크를 참고하시면 됩니다.
"contents:write" permission을 추가해야 Github Pages 배포할 때 새로운 branch를 만들고, 해당 branch에 파일을 넣을 수 있어 꼭 필요한 권한입니다.

 

      - name: Github 레포지토리 세팅
        uses: actions/checkout@v4

actions/checkout@v4를 이용하여 레포지토리 세팅을 해준다.

      - name: Flutter 세팅
        uses: subosito/flutter-action@v2
        with:
          channel: stable

subosito/flutter-action@v2를 이용하여 Flutter 세팅을 해주는데, stable 버전으로 설정해준다.

      - name: 캐시된 빌드, Pub 초기화
        run: flutter clean
      - name: Pub 새로 가져오기
        run: flutter pub get
      - name: 플러터 웹 빌드
        run: flutter build web --base-href "/blog/"

Flutter 관련 처리. Flutter 프로젝트를 clean한 후 다시 pub을 가져와 cache, build가 꼬이는 것을 방지해준다.

그리고 Flutter Web을 빌드한다.

      - name: Github Pages 배포
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_branch: web
          publish_dir: ./build/web

peaceiris/actions-gh-pages@v3를 이용하여 Github Pages를 배포한다.

원래 기본 branch가 있지만, 우리는 web으로 설정할 것이기에 publish_branch를 web으로 설정하고, 플러터 web을 빌드하면 ./build/web에 관련 파일들이 있기에 publish_dir로 해당 폴더로 설정해두고, github_token은 Github에서 제공하는 GITHUB_TOKEN을 연결해주면 된다.

 

그래서 프로젝트 파일의 .github/workflows 폴더를 새로 만들어주고, 새로운 yml파일을 만들어 위의 전체 코드를 넣어줍니다.

그리고 해당 변경사항을 commit한다음 push합니다.

git add .github\workflows\deploy_web.yml
git commit -m "add github action"
git push origin main

 

그러면 레포지토리의 Actions탭에 들어가면 해당 작업이 진행중인 것을 확인할 수 있다.

그리고 해당 이미지처럼 Deploy가 완료될 경우, 다시 Github Pages를 세팅합니다.

 

Settings 탭에 들어가 Pages 섹션을 누르고, Branch를 해당 브랜치로 설정해주고 save합니다.

그러면 이제 branch에서 Github Pages deploy가 됩니다.

deploy가 완료되면 해당 링크로 들어가 잘 작동하는지 확인해보면, 이미지처럼 잘 들어가질 것입니다.

Next?

이제 기초적인 세팅을 마췄고, 본격적으로 프로젝트 구조, 디자인, Routing, 상태관리 세팅을 진행하려 합니다. 아마 우선순위에 따라 만들어야할 것 같아서 어떤 걸 먼저 할지 고민입니다. 아마 상태관리 세팅하면서 프로젝트 구조를 세팅하지 않을까 싶습니다.

 

아니면 Github Actions를 Android/iOS에도 빌드하는 방법에 대해 다뤄보는 것도 좋을 것 같습니다. 대신 대부분의 회사들이 Private 레포지토리에 작업을 하는 경우가 많을텐데 기존 Runner를 이용하게 되면 비용이 발생하고, iOS까지 빌드하게 되면 제공해주는 Mac Runner 비용이 비싸 스타트업에서 쓰기 어려울 것입니다.

그래서 Self-Hosted Runner를 이용하여 회사에서 제공해주는 맥북이 있다면 그것을 로컬머신으로 이용하여 Github Actions를 처리하는 방법도 있는데, 이것에 대해 다뤄보는 것도 좋을 것 같습니다. 특히 Android, iOS 앱을 배포할 때는 각각 네이티브 기능과 Fastlane에 대해 다뤄야해서 정리해서 올리면 많은 도움이 되지 않을까 싶습니다.

 

[출처]

https://velog.io/@kdeun1/Github-Actions%EB%A1%9C-gh-pages-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0

 

Github Actions로 gh-pages 자동 배포하기

gh-pages, github actions, 자동 배포 이야기

velog.io

https://joeyhwang.tistory.com/19

 

GitHub : Flutter Web app을 GitHub Pages를 이용해 Deploy하기 1/2

Flutter 에서 만든 App은 Windows, Linux, Mac OS, Android, iOS, Web 등 다양한 방식으로 Build, Release 가능합니다. 타 플랫폼에서 사용하는 App의 경우, 해당 플랫폼용 설치 파일 포맷이나 실행 파일로 배포하면

joeyhwang.tistory.com

 

티스토리를 그동안 쓰질 않아서 블로그 개편 겸, 개인적으로 Flutter로 이것저것 시도하는 놀이터같은 블로그를 만들고 싶어서 이번에 Flutter Web 형태로 Github Pages에 개인 블로그를 만들어보려 합니다.

 

현재는 제작중이라 기본 틀만 만들어뒀는데, 개인 블로그 만들면서 적용했던 기술들을 하나씩 여기에 글 올릴 예정이니 관심 있으시다면 한번씩 찾아보시면 좋을 것 같아요.

 

이번 포스팅에서는 다음과 같은 작업에 대해 얘기드릴 예정입니다.

  1. Github Pages 세팅
  2. Flutter Web 빌드할 때 원하는 주소로 세팅하는 방법

다음 포스팅에서는 다음과 같은 작업에 대해 작성하였고, 보시려면 다음 링크로 가시면 됩니다.

  1. Github Actions를 이용하여 빌드와 배포를 자동으로 처리하는 CI/CD 자동화 구현

https://bluebada.tistory.com/18

 

[Flutter로 개인 블로그 만들기] #2. Github Actions를 이용하여 CI/CD 구현하기

이전 포스트에서 Flutter Web을 Github Pages에 올리는 방법에 대해 작성했는데, 해당 방식의 단점이라고 하면 Web으로 빌드된 파일로만 처리해야해서 프로젝트 코드를 모두 올릴 수가 없어 관리가 어

bluebada.tistory.com

 

 

Github Pages 세팅

Github Pages란?

https://pages.github.com/

 

GitHub Pages

Websites for you and your projects, hosted directly from your GitHub repository. Just edit, push, and your changes are live.

pages.github.com

Github에서 제공하는 호스팅 기능이라고 생각하시면 됩니다.

계정마다 하나를 생성할 수 있고, 만들게 되면 다음과 같은 주소로 사이트가 만들어집니다.

https://{Github 유저명}.github.io/

그래서 이걸 활용해서 자기만의 블로그나 사이트를 만들 수 있는데, 대부분 개인 블로그로 많이 이용합니다.

 

Github Pages 만들기

Github 계정이 있다면 Github 홈페이지에 Create New Repository 들어가서 옆의 Owner이름을 참고하여 "{Owner이름}.github.io"로 Repository name을 만들고 그 외 추가 세팅 없이 Create Repository 버튼을 눌러 새로운 Repository를 만들어줍니다.

 

그러면 이제 새로운 레포지토리가 생성되었다면, 다시 새로운 레포지토리를 만들어줄겁니다.

해당 레포지토리는 Github Pages의 Root를 담당하는 페이지이고,

저의 경우에는 blog이외에 다른 기능을 쓸 수도 있기에 블로그 코드만 분리해서 이용해볼 예정입니다.

다만 다음 포스팅에서 더 맞는 세팅을 위해서 이번엔 테스트용으로 레포를 만들어봅시다.

여기서 다시 New repository를 눌러서 테스트용 레포를 따로 만들어두겠습니다.

 

레포 이름은 원하시는 대로 작성해주셔도 됩니다. 해당 레포는 다음 포스팅때는 다시 삭제할 예정입니다.

 

Flutter 프로젝트 생성

이제 레포가 모두 만들어졌다면 블로그용으로 만들 새로운 Flutter 프로젝트를 만들어봅시다.

각자 이용하시는 IDE나 command를 이용하여 새로운 프로젝트를 만들어주시면 됩니다.

저는 Intellij를 이용하여 만들겠습니다. 프로젝트는 다음과 같이 세팅해뒀습니다.

프로젝트 이름이나 organization 등 자유롭게 하셔도 되는데, Platforms를 선택할 때 무조건 Web에 체크해주셔야합니다!

체크까지 다 하셨다면 프로젝트를 새로 만들어줍니다.

 

Flutter Web 빌드하기

다음과 같이 프로젝트가 만들어졌으면 이거 그대로 Web으로 빌드해서 올려보도록 하겠습니다.

 

command에 다음과 같이 입력해줍니다.

flutter build web

 

그러면 프로젝트에 build라는 폴더가 새로 생기고, web안에 여러 파일이 만들어져있는 걸 볼 수 있을겁니다.

 

빌드된 Flutter Web을 Github 올리기

원래대로라면 build/web 폴더를 git을 이용해 push를 하는 게 맞지만, 다음 포스팅에서 flutter 프로젝트 전체를 git으로 push를 할 예정이라 이번 포스팅에서만 file을 직접 업로드하겠습니다.

 레포지토리 페이지에 들어가서 빨간 박스의 "uploading an existing file"을 눌러줍니다.

해당 페이지가 나왔다면 아까 build/web 폴더 안에 있는 모든 파일들을 drag해서 넣어줍니다.

업로드가 모두 되었다면 commit 이름 넣어주고 "Commit changes"를 눌러서 업로드해줍니다.

그러면 다음과 같이 프로젝트가 생성됩니다.

 

이렇게 하면 자동으로 Github Pages가 세팅되는 건 아니기에 Github Pages 세팅을 해줍니다.

 

위에 Settings 탭을 눌러주고, Pages 섹션을 눌러줍니다.

여기서 Source는 다음과 같이 "Deploy from a branch"를 선택해주고, branch는 main/(root)로 설정해준 후에 Save버튼을 눌러줍니다.

 

그러면 자동으로 Github에서 deploy를 해주는데 Actions탭을 누르면 다음 사진과 같이 진행 과정이 보입니다.

 

Deploy가 완료되면 저 밑에 링크가 만들어지는데 들어가게 되면 다음과 같이 나옵니다.

 

그런데 해당 링크로 실행하게 되면 흰 화면만 보일거에요.

그 이유는 링크와 code가 연결이 되지 않아서 그런거에요.

index.html에 들어가보면 다음과 같은 코드가 보일겁니다.

 

흰 화면만 보이는 이유

<!DOCTYPE html>
<html>
<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <base href="/">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="플러터로 만드는 나만의 블로그">

  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="flutter_blog">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">

  <!-- Favicon -->
  <link rel="icon" type="image/png" href="favicon.png"/>

  <title>flutter_blog</title>
  <link rel="manifest" href="manifest.json">
</head>
<body>
  <script src="flutter_bootstrap.js" async></script>
</body>
</html>

 

윗 쪽의 주석을 보면 관련 내용이 있는데, 요약하면 해당 링크에 맞게 연결하려면 저 base 태그의 href를 해당 링크에 맞게 변경해줘야한다는 뜻입니다.

 

저 값을 직접 변경할 수도 있지만, command로 빌드할 때 뒤에 추가로 해당 값을 입력해서 적용할 수 있습니다.

flutter build web --base-href "/{레포지토리명}/"

 

저의 경우에는 레포지토리명이 flutter-web-test이기에 다음과 같이 입력하시면 됩니다.

flutter build web --base-href "/flutter-web-test/"

 

그러고나서 빌드된 파일을 다시 github에 업로드해줍니다.

그러고 Deploy되고 링크를 다시 열면 다음과 같이 페이지가 열립니다.

 

이렇게 flutter web을 Github Pages에 올릴 수 있게 되었습니다!

 

Next?

Flutter Web을 올렸지만, 단점이라고 하면 Web으로 빌드된 파일로만 처리해야해서 프로젝트 코드를 모두 올릴수가 없어서 관리가 어려운 문제가 있습니다.

그래서 다음 포스팅에서는 branch를 분리해서 web-page 브랜치에는 빌드된 웹페이지 파일들이, main 브랜치에서는 flutter 프로젝트 파일들이 구성되게 할 예정입니다.

 

그리고 이러한 빌드를 일일이 command 입력 없이 Github Actions를 통해 웹 빌드를 자동으로 해주고, Github Pages에 자동 Deploy까지 될 수 있게 하면 매우 편안해질 겁니다.

 

이러한 방식을 지속적인 배포(CD)라고 하는데 이걸 세팅해주면 일일이 command로 빌드하면서 기다리고, pages에 deploy등 작업과 시간이 단축되기에 많은 도움이 될것입니다.

다음 게시글에 다음과 같은 내용에 대해 다뤘습니다.

https://bluebada.tistory.com/18

 

[Flutter로 개인 블로그 만들기] #2. Github Actions를 이용하여 CI/CD 구현하기

이전 포스트에서 Flutter Web을 Github Pages에 올리는 방법에 대해 작성했는데, 해당 방식의 단점이라고 하면 Web으로 빌드된 파일로만 처리해야해서 프로젝트 코드를 모두 올릴 수가 없어 관리가 어

bluebada.tistory.com

 

 

 

[출처]

https://pages.github.com/

 

GitHub Pages

Websites for you and your projects, hosted directly from your GitHub repository. Just edit, push, and your changes are live.

pages.github.com

https://velog.io/@kdeun1/Github-Actions%EB%A1%9C-gh-pages-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0

 

Github Actions로 gh-pages 자동 배포하기

gh-pages, github actions, 자동 배포 이야기

velog.io

https://joeyhwang.tistory.com/19

 

GitHub : Flutter Web app을 GitHub Pages를 이용해 Deploy하기 1/2

Flutter 에서 만든 App은 Windows, Linux, Mac OS, Android, iOS, Web 등 다양한 방식으로 Build, Release 가능합니다. 타 플랫폼에서 사용하는 App의 경우, 해당 플랫폼용 설치 파일 포맷이나 실행 파일로 배포하면

joeyhwang.tistory.com

 

+ Recent posts