안녕! 난 너의 가상 친구야!
Overview
바탕화면 장난감은 컴퓨터 바탕화면에서 걸어다니는 캐릭터 프로그램이다. 사용자가 지정한 대사를 내뱉거나 Macro를 실행해준다. 어릴 적에 바탕화면에 캐릭터를 대여섯개 띄워두고 좋아했던 추억이 되살아나서 만들었다. 이 프로그램은 사용자의 바탕화면을 의미있게 채울 수 있다.
이 프로그램은 당연히 MacOS에서만 동작한다. dotnet의 WinForm API를 주로 사용해 만든 Window용 바탕화면 장난감은 회사에서 배포했고, 다시 만들 계획은 없다.
처음 Swift를 사용하며 겪은 우여곡절과 함께 간단한 프로젝트 설명을 하고자 한다.

개발 환경 & 빌드 세팅
MacOS 13.0.1에서 Xcode 14.2를 이용해 개발했다.
Xcode 포맷팅은 범위 선택 -> Control + i로 할 수 있다. Xcode -> Settings... -> Text Editing에서 포맷팅 옵션을 수정할 수 있다. 
…는 매우 불편하다. 매번 범위 선택 후 포맷팅을 한다는 동작은 믿음직스럽지 못하다.
project-name.xcodeproj의 Build 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 안에 프로그램 흐름이 보이는 편이 익숙하다. 그런 이유로, 이 프로젝트가 좋은 스위프트 예시라고는 생각하지 않는다. 조금 더 전형적인 방식으로 구현할 필요를 느낀다. 하지만 짧고 빠르게 만드는 것도 이 프로젝트에서 바라던 것이니 이 정도로 타협…! 하기로…!
앞으로도 짬짬히 다양한 기능을 추가할 예정이다.