바탕화면 장난감 개발기 (with Swift)

2023년 02월 03일
제작기간 2023년 01월 08일 ~ 02월 05일
태그 swift Project
GitHub

안녕! 난 너의 가상 친구야!

Overview

바탕화면 장난감은 컴퓨터 바탕화면에서 걸어다니는 캐릭터 프로그램이다. 사용자가 지정한 대사를 내뱉거나 Macro를 실행해준다. 어릴 적에 바탕화면에 캐릭터를 대여섯개 띄워두고 좋아했던 추억이 되살아나서 만들었다. 이 프로그램은 사용자의 바탕화면을 의미있게 채울 수 있다.

이 프로그램은 당연히 MacOS에서만 동작한다. dotnet의 WinForm API를 주로 사용해 만든 Window용 바탕화면 장난감은 회사에서 배포했고, 다시 만들 계획은 없다.

처음 Swift를 사용하며 겪은 우여곡절과 함께 간단한 프로젝트 설명을 하고자 한다.

.

개발 환경 & 빌드 세팅

MacOS 13.0.1에서 Xcode 14.2를 이용해 개발했다.

Xcode 포맷팅은 범위 선택 -> Control + i로 할 수 있다. Xcode -> Settings... -> Text Editing에서 포맷팅 옵션을 수정할 수 있다.
…는 매우 불편하다. 매번 범위 선택 후 포맷팅을 한다는 동작은 믿음직스럽지 못하다.

project-name.xcodeprojBuild Phase에 빌드 액션을 추가할 수 있다. 미리 매 빌드에 실행할 셸 스크립트를 추가하는 방식이다. 이 프로젝트에서는 formatting과 lint 체크를 한다. 사용한 formatter는 apple에서 제공하는 swift-format이다.

빌드 방법 및 사용 방법은 해당 레포에 상세히 나와있다. 별도로 해줄 것은 프로그램 Path를 zsh에 등록하여 Xcode가 swift-format을 사용할 수 있게 해주는 거다. ~/.zshrc에 Path를 추가해주면 된다.

# swift
export PATH="$PATH:$HOME/YOUR_PATH/swift-format/.build/release" # swift-format
# end swift

이제 Xcode에서 정상적으로 프로젝트를 빌드할 수 있다. 아래는 Build Phase 중, format Phase의 스크립트다.

# Import all env vars in zshrc
. ~/.zshrc

if which swift-format >/dev/null; then
    swift-format format --configuration ${SRCROOT}/swift-format-configuration.json --in-place --recursive --parallel ${SRCROOT}
else
    echo "warning: swift-format not installed"
    exit 1
fi

아까 등록한 swift-format을 이용해서 전체 프로젝트를 포맷팅한다.

CI/CD 세팅

당연히 매번 빌드하는 것은 너무 귀찮기 때문에 GitHub Action을 이용한 CI/CD 세팅을 해줬다. 이 과정에서 정말 헤맸다. workflow 파일은 아래와 같다.

// ...
    - name: Install the Apple certificate
      env:
        BUILD_CERTIFICATE_BASE64: $
        P12_PASSWORD: $
        KEYCHAIN_PASSWORD: $
      run: |
        # create variables
        CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
        PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
        KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
        # import certificate from secrets
        echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
        # create temporary keychain
        security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
        security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
        security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
        # import certificate to keychain
        security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
        security list-keychain -d user -s $KEYCHAIN_PATH

    - name: Install the swift-format
      run: |
        brew install swift-format

    - name: Build project
      run: |
        xcodebuild build \
          -project background-toy.xcodeproj \
          -scheme background-toy \
          -destination 'platform=OS X,arch=x86_64' \
          -configuration Release \
          -derivedDataPath build

    - name: Zipping build output
      run: |
        cd build/Build/Products/Release
        zip -r background-toy.zip background-toy.app

    - name: Upload Build
      uses: actions/upload-artifact@v3
      with:
        name: backgroud-toy app
        path: build/Build/Products/Release/background-toy.zip

    - name: Clean up keychain
      if: ${{ always() }}
      run: |
        security delete-keychain $RUNNER_TEMP/app-signing.keychain-db

가장 먼저, Apple certificate 세팅이다. Installing an Apple certificate on macOS runners for Xcode development에 매우 자세히 설명이 나와있다. Secret으로 보안이 필요한 항목들을 뺄 수 있다. 로그를 찍을 때도 알아서 걸러주기 때문에 믿고 사용해도 괜찮다.

Build Phase에 formatting이 등록되어있기 때문에 당연히 CI 세팅에도 swift-format이 필요하다. 처음에는 Local에 설치한 것과 같은 방법으로 clone하려고 했으나…10분 이상 걸리는 어마어마하게 느린 속도에… brew로 설치하기로 했다. brew는 기본으로 있기 때문에 별도의 설치없이 사용할 수 있다.

Build 자체는 Local에서 하는 것과 동일하다. xcodebuild 커맨드를 이용하면 되고, 설정은 위와 같게 해줬다. Building from the Command Line with Xcode FAQ를 참고한다.
-derivedDataPath을 지정해야 원하는 디렉토리에 빌드할 수 있다. 원래 Build 로그에 찍힌 path를 이용했는데 비정상적이라는 이야기를 듣고 수정했다..

이제 빌드한 파일을 업로드한다. actions/upload-artifact을 사용한다. *.app은 사실상 디렉토리이기 때문에 파일처럼 지정해서 업로드할 수 없다. 이를 피하기 위해서 애플리케이션을 zip한 뒤에 업로드한다.

모든 과정이 끝났으면 keychain을 정리한다.

깃헙 액션은 커밋이 되어야 동작을 확인할 수 있기 때문에, 별도 브랜치를 따서 타겟에 넣어줬다. 수없이 많은 실패 후에 드디어 빌드가 올라갔다. 녹색 동그라미가 정말 반가웠다. 모두가 이 기쁨을 알았으면 좋겠다. 오늘 하루 참 좋다 대신에 오늘 왠지 녹색 동그라미가 더 와닿을 것 같다.

커스텀 가이드

프로젝트 문서에 적어뒀지만 여기에도 첨부한다.

JSON 파일 수정 방법

Finder로 Json 파일을 찾을 수 있다. Json 파일 편집기 추가 예정은 없다. (그래도 언젠가는…?)

맥 OS 애플리케이션은 기본적으로 폴더이다. 애플리케이션을 우클릭하고 패키지 콘텐츠 보기를 선택하면 해당 콘텐츠 내용물에 액세스할 수 있다. ./Contents/Resources/ 경로에 편집할 수 있는 Json 파일이 있다.

커스텀하고 싶다면 아래 가이드를 참고하면 된다.

커스텀 애니메이션

animation.json 파일을 편집하여 애니메이션을 커스텀할 수 있다. spriteFolderPath를 애니메이션 이미지가 저장된 폴더의 경로로 변경한 후 프레임 수나 애니메이션 플레이 방식을 자신의 어셋에 맞게 수정하면 된다.

  • 애니메이션 이미지 이름은 {animation name}_{index}.png 형식이어야 한다.
  • idle, walk, grab, touch 및 playingcursor에 대해 각각 하나 이상의 이미지가 있어야 한다.

다음은 샘플 animation.json 파일이다.

{
    "spriteFolderPath": "default",
    "clips": {
        "idle": {
            "count": 3,
            "playType": "pingpong"
        },
        "walk": {
            "count": 7,
            "playType": "restart"
        },
        "grab": {
            "count": 3,
            "playType": "pingpong"
        },
        "touch": {
            "count": 2,
            "playType": "restart"
        },
        "playingcursor": {
            "count": 6,
            "playType": "restart"
        }
    }
}

playType은 애니메이션이 재생되는 방식을 말하며, “pingpong”(애니메이션이 끝나면 반대로 재생) 또는 “restart”(애니메이션이 끝나면 다시 시작)로 설정한다. count는 애니메이션에 해당하는 이미지 개수다.

커스텀 매크로

macro.json 파일을 편집하여 커스텀 매크로를 설정할 수 있다. 매크로는 프로세스를 실행하고, 웹 URL을 열고, 채팅 메시지를 출력할 수 있다.

다음은 샘플 macro.json 파일이다.

{
    "test1": [
        {
            "type": "process",
            "payload": "/Applications/Discord.app"
        },
        {
            "type": "web",
            "payload": "https://www.google.com/"
        },
        {
            "type": "chat",
            "payload": "Hey! Let's play game."
        }
    ],
    "test2": [
        {
            "type": "web",
            "payload": "https://www.naver.com/"
        }
    ]
}

커스텀 대화

chat.json 파일을 편집하여 커스텀 대화 메시지를 설정할 수 있다. 채팅 메시지는 시간에 따라 오전, 점심, 오후, 저녁, 밤으로 분류하여 지정할 수 있다.

다음은 샘플 chat.json 파일이다.

{
    "morning" : [
        "morning data 1",
        "morning data 2"
    ],
    "lunch" : [
        "launch data 1",
        "launch data 2"
    ],
    "afternoon" : [
        "afternoon data 1",
        "afternoon data 2"
    ],
    "evening" : [
        "evening data 1",
        "evening data 2"
    ],
    "night" : [
        "night data 1",
        "night data 2"
    ]
}

후기

꽤 재밌는 토이 프로젝트였다. 쉬엄쉬엄해서 개발 기간이 꽤 길었던 점이 아쉽지만 이것 저것 많이 배울 수 있었던 것 좋은 시간이었다. Xcode랑 친해지는 것은 조금 힘들었다… (아직 안 친한 듯)

작업에 들인 공은.. 세팅 > 리팩토링 > 코드 디테일 순서다. 작은 규모의 프로젝트다 보니까 디테일은 적었고, 처음 짜보는 스타일의 코드이다보니까 세팅 및 리팩토링 시간이 길었다.

event driven 방식으로 디자인하는 것이 정석이나, State 및 주기적 업데이트 처리로 구현했다. 좋지 않은 디자인일 수도 있으나, 움직임 및 애니메이션 처리에 적당한 선택이었다고 본다. Update 안에 프로그램 흐름이 보이는 편이 익숙하다. 그런 이유로, 이 프로젝트가 좋은 스위프트 예시라고는 생각하지 않는다. 조금 더 전형적인 방식으로 구현할 필요를 느낀다. 하지만 짧고 빠르게 만드는 것도 이 프로젝트에서 바라던 것이니 이 정도로 타협…! 하기로…!

앞으로도 짬짬히 다양한 기능을 추가할 예정이다.