
๋ ์ด์ ์๋ ๋ฐฐํฌ๋ ๊ทธ๋ง! iOS ๋ฐฐํฌ ์๋ํ ๊ตฌ์ถ๊ธฐ

- #iOS
- #CICD
- #Fastlane
- #GitHub Actions
๋ค์ด๊ฐ๋ฉฐ
์๋ ํ์ธ์. ํ๋กฌ iOS ๊ฐ๋ฐ์ ์ด๋์ฐ์ ๋๋ค.
ํ๋กฌ iOS ์ฑ์ ๊ฐ๋ฐํ๋ฉด์ ๊ฐ์ฅ ๋ฒ๊ฑฐ๋ก์ ๋ ์์ ์ค ํ๋๊ฐ ๋ฐ๋ก ๋ฐฐํฌ์์ต๋๋ค. ๋งค๋ฒ ๋ฐฐํฌํ ๋๋ง๋ค ์ ์ ํ ๋ธ๋์น๋ก ์ฒดํฌ์์ํ๊ณ , Xcode๋ฅผ ์ผ์ xcconfig์์ ๋ฒ์ ์ ๋ณ๊ฒฝํ๊ณ , ์์นด์ด๋ธ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ , ipa ํ์ผ์ ์ถ์ถํด์ Firebase Distribution์ ์ ๋ก๋ํ๊ณ , TestFlight๊น์ง ๋ค์ ํ๋ฒ ์ ๋ก๋ํ๋ ๊ณผ์ ์ ๋ฐ๋ณตํด์ผ ํ์ฃ .
๊ฐ ๋จ๊ณ๋ง๋ค ์ผ์ผ์ด ๋๊ธฐํ ๋ค ๋ค์ ์์ ์ ์ด์ด๊ฐ์ผ ํ๋ ๋ฒ๊ฑฐ๋ก์์ ๋ฌผ๋ก , ๋ธ๋์น ์ฒดํฌ์์์ด๋ ๋ฒ์ ๊ด๋ฆฌ์์ ์ค์๊ฐ ๋ฐ์ํ๊ธฐ๋ ํ์ต๋๋ค.
์ ํฌ ํ์ Firebase Distribution๊ณผ TestFlight ๋ ํ๋ซํผ์ ๋ชจ๋ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ฐฐํฌ ๊ณผ์ ์ด ๋์ฑ ๋ณต์กํ์ต๋๋ค.
์ด์ ๋ฐ๋ผ ๋ฐ๋ณต์ ์ธ ์์ ์ ์๋ํํด ํ ๋ฆฌ์์ค๋ฅผ ์ต์ ํํ๊ณ , ์ค์๊ฐ ๋ฐ์ํ์ง ์๋ ์์ ์ ์ธ ๋ฐฐํฌ ํ๊ฒฝ์ ๋ง๋ จํ๊ธฐ ์ํด ๋ฐฐํฌ ์๋ํ ํ๋ก์ ํธ๋ฅผ ์์ํ๊ฒ ๋์์ต๋๋ค.
Fastlane ์ธํฐํ์ด์ค ์ค๊ณ
์ฐ์ ๋ฐฐํฌ ์๋ํ์ ํต์ฌ์ด ๋ Fastlane์ ์ธํฐํ์ด์ค๋ฅผ ์ค๊ณํ์ต๋๋ค. ํ๋กฌ ์๋น์ค๋ ํฌ์ฑ๊ณผ ์ํฐ์ฑ ๋ ๊ฐ์ง ์ฑ์ ์ด์ํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ๋ชจ๋ ์ง์ํด์ผ ํ๊ณ , ๋ํ ๊ฐ๋ฐ/QA/์คํ ์ด์ง/ํ๋ก๋์ ํ๊ฒฝ์ ๊ตฌ๋ถํด ๋ฐฐํฌํ ์ ์๋๋ก ๊ตฌ์ฑํ์ต๋๋ค.
# Fastlane CICD Interface
# Firebase Distribution ์
๋ก๋
lane :upload_firebase do |options|
# Parameters:
# - app_type: fan/arti
# - environment: dev/qa/stg/prd
# - version: ์ฑ ๋ฒ์
# - build_number: ๋น๋ ๋ฒํธ
# - release_notes: ๋ฆด๋ฆฌ์ฆ ๋
ธํธ
end
# TestFlight ์
๋ก๋
lane :upload_testflight do |options|
# Parameters:
# - app_type: fan/arti
# - version: ์ฑ ๋ฒ์
# - build_number: ๋น๋ ๋ฒํธ
# TestFlight๋ ํ๋ก๋์
ํ๊ฒฝ๋ง ์
๋ก๋ํ๋ฉฐ,
# ๋ฆด๋ฆฌ์ฆ ๋
ธํธ๋ ๋ณ๋๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ํ๋ผ๋ฏธํฐ์์ ์ ์ธ
end
Firebase Distribution ์ ๋ก๋์ TestFlight ์ ๋ก๋, ์ด๋ ๊ฒ 2๊ฐ์ public lane์ผ๋ก ์ธํฐํ์ด์ค๋ฅผ ๊ตฌ์ฑํ์ต๋๋ค. TestFlight์ ๊ฒฝ์ฐ ํ๋ก๋์ ํ๊ฒฝ๋ง ์ ๋ก๋ํ๊ณ ๋ฆด๋ฆฌ์ฆ ๋ ธํธ๋ App Store Connect์์ ๋ณ๋๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ต์ํํ์ต๋๋ค.
Fastlane ๊ตฌํ
Fastfile์์๋ ํ๊ฒฝ๋ณ๋ก ์ ์ ํ configuration๊ณผ ์คํค๋ง๋ฅผ ๋งคํํ๊ณ , Firebase Distribution์ ๊ฒฝ์ฐ ํ ์คํฐ ๊ทธ๋ฃน๋ ํ๊ฒฝ๋ณ๋ก ๊ตฌ๋ถํด์ ๊ด๋ฆฌํ๋๋ก ํ์ต๋๋ค.
def environment_to_configuration(environment)
case environment
when "dev"
CONFIGURATION_DEV # SandboxRelease
when "qa"
CONFIGURATION_QA # QARelease
when "stg"
CONFIGURATION_STG # StageRelease
when "prd"
CONFIGURATION_PRD # StoreRelease
else
UI.user_error!("Invalid environment: #{environment}")
end
end
# Firebase ํ
์คํฐ ๊ทธ๋ฃน ํ๊ฒฝ๋ณ ๋งคํ
def tester_group_by_configuration(configuration)
case configuration
when CONFIGURATION_DEV
tester_groups = FIREBASE_TESTER_GROUPS_DEV # "fromm-๊ฐ๋ฐํ, fromm-๊ธฐํํ, ..."
when CONFIGURATION_QA
tester_groups = FIREBASE_TESTER_GROUPS_QA
when CONFIGURATION_STG
tester_groups = FIREBASE_TESTER_GROUPS_STG
when CONFIGURATION_PRD
tester_groups = FIREBASE_TESTER_GROUPS_PRD
end
end
Firebase Distribution๊ณผ TestFlight์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ update_version, build_fromm_app ๋ฑ์ ํจ์๋ฅผ ์ถ๊ฐํ์ฌ ์๋์ ๊ฐ์ด lane์ ์์ฑํ์์ต๋๋ค!
lane :upload_firebase do |options|
puts options
app_type = options[:app_type]
app_type_string = app_type == "fan" ? "ํฌ์ฑ" : "์ํฐ์ฑ"
environment = options[:environment]
version = options[:version]
build_number = options[:build_number]
release_notes = options[:release_notes] || "#{app_type_string} #{version}(#{build_number}) #{environment} ํ๊ฒฝ ๋ฐฐํฌ์
๋๋ค."
is_fan = app_type == "fan"
configuration = environment_to_configuration(environment)
tester_group = tester_group_by_configuration(configuration)
update_version(
isFan: is_fan,
version_number: version,
build_number: build_number,
bump_build_number: false
)
build_fromm_app(
configuration: configuration,
is_fan: is_fan,
distribution_service: DISTRIBUTON_SERVICE_FIREBASE
)
firebase_app_distribution(
app: app_id,
service_credentials_file: "./keystore/fastlane-firebase.json",
ipa_path: ipa_absolute_path,
release_notes: release_notes,
groups: tester_groups,
debug: true
)
end
GitHub Actions์ ๋๊ด๋ค
Fastlane์ ๋ก์ปฌ์์ ์คํํ ์๋ ์์ง๋ง, ๋ ์ผ๊ด๋๊ณ ์์ ์ ์ธ ํ๊ฒฝ์ ๊ตฌ์ฑํ๊ธฐ ์ํด GitHub Actions๋ฅผ ํ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค. ์ฒ์์๋ ๋น์ฐํ GitHub์์ ์ ๊ณตํ๋ ์ต์ macOS Runner๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํ์ฃ .
jobs:
distribute-firebase:
name: Distribute to Firebase
runs-on: macos-latest
์ฌ๊ธฐ์ ์ฒซ ๋ฒ์งธ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ํฌ ํ๋ก์ ํธ๋ Tuist๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, GitHub์์ ํด๋ก ํ ๋ค xcworkspace๋ฅผ ์ง์ generateํด์ผ ํ์ต๋๋ค. ์ด๋ฅผ ์ํด mise์ tuist๊ฐ ์ ์ ๋์ํ๋๋ก ๋ฒ์ ์ ๋ง์ถ๊ณ workflow๋ฅผ ์ค์ ํ๋ ๊ณผ์ ์์ ๊ฝค ๋ง์ ์ด๋ ค์์ ๊ฒช์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ๋ ํฐ ๋๊ด์ด ๊ธฐ๋ค๋ฆฌ๊ณ ์์์ต๋๋ค. ๋ฐ๋ก ์ฝ๋ ์ฌ์ด๋๊ณผ ๋น๋ ์๊ฐ ๋ฌธ์ ์์ต๋๋ค.
GitHub-hosted Runner์ ํ๊ณ
์์นด์ด๋ธ๋ฅผ ํด์ Firebase๋ TestFlight๋ ๋ฐฐํฌ ์ฌ์ดํธ์ ์ฌ๋ฆฌ๋ ค๋ฉด ์ธ์ฆ์์ ํ๋ก๋น์ ๋ ํ์ผ์ด ํ์ํ๋ฐ, ๋ ๋ฆฝ๋ ํ๊ฒฝ์์ ์ด ํ๋ก๋น์ ๋ ํ์ผ์ ์ธํ ํ๋ ๊ฒ์ด ์ด๋ ค์ ์ต๋๋ค.
๊ธฐ์กด์ Auto Signing์ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ App Store Connect์ ์ด๋๋ Apple ๊ณ์ ์ด ํ์ํ ์ํฉ์ด์์ฃ .
ํ ๊ณ์ ์ ์ฌ์ฉํด workflow๋ด์์ ๋ก๊ทธ์ธ์ ํ๊ฑฐ๋ fastlane match๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ ๊ณ ๋ คํด๋ณด์๋๋ฐ์, ๋ค์์ ์ด์ ๋ก ์์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋ชจ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ฒ ๋์์ต๋๋ค.
๋ฐ๋ก ๋น๋์ ์์๋๋ ์๊ฐ์ด์์ต๋๋ค. GitHub Runner์ ์ฑ๋ฅ์ด ์ข์ง ์์๋์ง, ์์นด์ด๋ธ๊ฐ ๋๋ฌด๋๋ฌด ์ค๋ ๊ฑธ๋ ธ์ต๋๋ค.
์ด์ ๋ฐ๋ผ ์ด workflow ์คํ์๊ฐ์ ๊ฑฐ์ 40๋ถ ์ด์์ด ์์๋์์ต๋๋ค. ์๋์ผ๋ก ๋ฐฐํฌํ ๋๋ 10~20๋ถ ์ ๋๊ฐ ๊ฑธ๋ฆฌ๋๋ฐ, ์๋ํ๊ฐ ๋๋ค ํ๋๋ผ๋ ์๋ ๋ฐฐํฌ๋ณด๋ค ํจ์ฌ ์ค๋ ๊ฑธ๋ฆฐ๋ค๋ฉด ๋ฐฐํฌ ํ๋ก์ธ์ค์ ์ ์ฉํ๊ธฐ๊ฐ ๊ณค๋ํ์ต๋๋ค.
Self-hosted Runner๋ก์ ์ ํ
์ด๋ฐ ๋ฌธ์ ๋ค ๋๋ฌธ์ ๋ก์ปฌ ๋จธ์ ํ์ฉํ self-hosted runner๋ฅผ ์ด์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. ๋น๋ํ๊ธฐ ์ํ ๋งฅ๋ถ 2๊ฐ๋ฅผ ํ ๋น๋ฐ์๊ณ , ์ด๋ฅผ GitHub Actions runner์ ๋ฑ๋กํ์ต๋๋ค.
ํด๋น ๋งฅ๋ถ์๋ ๋ฏธ๋ฆฌ ๋ค์๊ณผ ๊ฐ์ ํ๊ฒฝ์ ์ธํ ํด๋์์ต๋๋ค:
- Tuist ์ค์น: ํ๋ก์ ํธ generate๋ฅผ ์ํด
- ์ฌ๋ฐ๋ฅธ ๋ฒ์ ์ Xcode ์ค์น: ์ผ๊ด๋ ๋น๋ ํ๊ฒฝ์ ์ํด
- Xcode์ ํ ๊ณต์ฉ ๊ณ์ ๋ฑ๋ก: ํ ๊ณ์ ์ผ๋ก ์ฌ์ด๋ ์ค์ (Auto Signing ์ฌ์ฉ ๊ฐ๋ฅ)
๊ทธ๋ฆฌ๊ณ workflow์์๋ ํน์ Xcode ๋ฒ์ ์ ์ฌ์ฉํ๋๋ก ๋ช ์์ ์ผ๋ก ์ ํํ๋ ๊ณผ์ ๋ ํฌํจํ์ต๋๋ค:
XCODE_PATH = "/Applications/Xcode-16.4.0.app"
# Fastlane์์ Xcode ๋ฒ์ ์ง์
xcode_select(XCODE_PATH)
jobs:
distribute-firebase:
name: Distribute to Firebase
runs-on: self-hosted # ์ด์ ์ฐ๋ฆฌ ๋งฅ๋ถ์์ ์คํ!
workflow ์คํ ์๊ฐ๋ ๋ํญ ๊ฐ์๋์ด 10๋ถ ๋ด์ธ๊ฐ ๋์์ต๋๋ค.
self-hosted runner๋ฅผ ํตํด ๋ค์ํ ํ๊ฒฝ ์ค์ ๋ฌธ์ ์ ๋น๋ ์๊ฐ ๋ฌธ์ ๋ฅผ ํ๋ฒ์ ํด๊ฒฐํ ์ ์์์ต๋๋ค!
GitHub Secret์ผ๋ก ์ธ์ฆ ๊ด๋ฆฌ
Firebase์ TestFlight์ ํ์ํ ์ฌ๋ฌ ์ธ์ฆ์๋ค์ GitHub Secret์ ์ ์ฅํด๋์์ต๋๋ค. ํนํ JSON ํค ํ์ผ๊ฐ์ ๊ฒฝ์ฐ๋ ๊ฐํ์ด ํฌํจ๋๋ฉด GitHub workflow์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ base64๋ก ์ธ์ฝ๋ฉํด์ ์ฌ์ฉํ์ต๋๋ค.
- name: Generate key file
run: |
mkdir -p keystore
echo '${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }}' | base64 -d > ./keystore/fastlane-firebase.json
Firebase Distribution์ ์ํ ํ๊ฒฝ๋ณ์๋ค๋ workflow์์ ๋์ ์ผ๋ก ์์ฑํ๋๋ก ๊ตฌ์ฑํ์ต๋๋ค:
- name: Make Fastlane Env
run: |
echo FIREBASE_FAN_APP_ID=${{ secrets.FIREBASE_FAN_APP_ID }} >> ./Fastlane/.env
echo FIREBASE_STAR_APP_ID=${{ secrets.FIREBASE_STAR_APP_ID }} >> ./Fastlane/.env
์ค์ ๋ฐฐํฌ ๊ณผ์ ์ Tuist๋ก ํ๋ก์ ํธ๋ฅผ generateํ๊ณ Fastlane์ ์คํํ๋ ๋จ๊ณ๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค:
- name: Make .xcodeproj file
run: |
tuist install
TUIST_ROOT_DIR=${PWD} tuist generate --no-binary-cache --verbose
# Verify project files were generated
echo "Checking generated project files..."
ls -la *.xcworkspace *.xcodeproj 2>/dev/null || echo "No workspace/project files found at root"
find . -name "*.xcworkspace" -o -name "*.xcodeproj" | head -5
- name: Run Fastlane
run: fastlane upload_firebase app_type:${{ inputs.type }} environment:${{ inputs.environment }} version:${{ inputs.version_number }} build_number:${{ inputs.build_number }} release_notes:"${{ inputs.releaseNote }}"
๋น๋ ๋จธ์ ๊ฐ์ฉ์ฑ ํ๋ณด
์ธ์ ๋ ๊ฐ์ฉ์ฑ ์๊ฒ ๋น๋ ๋จธ์ ์ ์ฌ์ฉํ ์ ์๋๋ก Amphetamine ์ฑ์ ์ค์นํด ๋งฅ๋ถ์ด ์ฌ๋ฆฝ ๋ชจ๋๋ก ๋ค์ด๊ฐ์ง ์๊ณ ์จ๋ผ์ธ ์ํ๊ฐ ์ ์ง๋๋๋ก ํ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Chrome ์๊ฒฉ ๋ฐ์คํฌํ ํ์ฅ ํ๋ก๊ทธ๋จ์ ์ด์ฉํด ์ด๋์๋ ๋ฌธ์ ํด๊ฒฐ์ ํ ์ ์๋๋ก ์ธํ ํ์ต๋๋ค.
๋ค๋ง ๊ธฐ๊ธฐ๋ฅผ ์์ ํ ๋ซ์ผ๋ฉด ์ฌ๋ฆฝ ๋ชจ๋๊ฐ ๋๊ณ ๋คํธ์ํฌ๊ฐ ์ ์ค๋๋ฏ๋ก ์ด ๋ถ๋ถ์ ์ฃผ์ํ๋๋ก ์๋ดํ์ต๋๋ค.
์๋ ๋ฒ์ ๊ด๋ฆฌ
์ถ๊ฐ์ ์ผ๋ก Actions์์ ๋ฐฐํฌ๊ฐ ์ฑ๊ณตํ๋ฉด ๋ฒ์ ์ ์ปค๋ฐ์ ์๋์ผ๋ก ์ฌ๋ฆฌ๋๋ก ๊ตฌ์ฑํ์ต๋๋ค.
- name: Auto commit version changes
if: success()
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git add XCConfig/Artist/ArtistBase.xcconfig XCConfig/Fan/FanBase.xcconfig
git commit -m "[version-up] ${{ inputs.version_number }}(${{ inputs.build_number }})"
git push
์ด์ ๋ฐฐํฌ๊ฐ ์ฑ๊ณตํ๋ฉด xcconfig ํ์ผ์ ๋ฒ์ ์ ๋ณด๊ฐ ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋๊ณ , ์ปค๋ฐ๊น์ง ์ฌ๋ผ๊ฐ์ ๋ฒ์ ํ์คํ ๋ฆฌ๋ฅผ ๊น๋ํ๊ฒ ๊ด๋ฆฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
workflow_dispatch๋ก ํธ๋ฆฌํ GUI ๋ฐฐํฌ
workflow_dispatch๋ฅผ ์ด์ฉํด์ GUI ํ๊ฒฝ์์ ํธํ๊ฒ ๋ฐฐํฌํ ์ ์๊ฒ ๋์์ต๋๋ค. GitHub์ Actions ํญ์์ ๋ฒํผ ํด๋ฆญ๋ง์ผ๋ก ๋ฐฐํฌํ ์ ์๊ณ , ๋ชจ๋ฐ์ผ์์๋ ์ฌ์ฉ ๊ฐ๋ฅํด์ ์ ๋ง ํธ๋ฆฌํฉ๋๋ค.
on:
workflow_dispatch:
inputs:
type:
description: App Type (arti, fan)
required: true
environment:
description: Server Environment (dev, qa, stg, prd)
required: true
version_number:
description: Version Number (e.g. 1.2.3)
required: true
build_number:
description: Build Number (e.g. 100)
required: true
releaseNote:
description: Release Note
required: true
GitHub ์ฑ์ ์ค์นํ๋ฉด ๋ชจ๋ฐ์ผ์์๋ Actions๋ฅผ ์คํํ ์ ์์ด์, ์ธ์ ์ด๋์๋ ๋ฐฐํฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
Slack ์๋ฆผ์ผ๋ก ์๋ฒฝ ๋ง๋ฌด๋ฆฌ
๋ฐฐํฌ ์ฑ๊ณต ์ Slack์ผ๋ก ์๋ฆผ์ ๋ณด๋ด๋๋ก ๊ตฌ์ฑํด์ ํ์๋ค์ด ์ค์๊ฐ์ผ๋ก ๋ฐฐํฌ ํํฉ์ ํ์ ํ ์ ์๋๋ก ํ์ต๋๋ค. Steve Jobs์ Tim Cook ์์ด์ฝ์ผ๋ก Firebase Distribution๊ณผ TestFlight๋ฅผ ๊ตฌ๋ถํด์ ์๋ฆผ์ ๋ณด๋ด๋ ๊ฒ๋ ์์ํ ์ฌ๋ฏธ ์์์ ๋๋ค.
# in Fastfile
SLACK_USERNAME = distribution_service == DISTRIBUTON_SERVICE_TESTFLIGHT ? SLACK_USERNAME_TIM : SLACK_USERNAME_STEVE
ICON_URL = distribution_service == DISTRIBUTON_SERVICE_TESTFLIGHT ? ICON_URL_TIM : ICON_URL_STEVE
slack(
message: message,
slack_url: DEPLOY_NOTY_SLACK_URL,
username: SLACK_USERNAME,
icon_url: ICON_URL,
default_payloads: [],
attachment_properties: {
fields: fields
},
fail_on_error: false,
link_names: true
)
๋ง์น๋ฉฐ
์ด๋ ๊ฒ ํด์ ์ ํฌ๋ iOS ๋ฐฐํฌ ์๋ํ์ ์ฑ๊ณตํ์ต๋๋ค! ์ด์ ๋ ์ด์ ๋ฒ๊ฑฐ๋ก์ด ์๋ ๋ฐฐํฌ ๊ณผ์ ์ ๋ฐ๋ณตํ ํ์๊ฐ ์์ด์ก๊ณ , ์ค์ํ ๊ฐ๋ฅ์ฑ๋ ํฌ๊ฒ ์ค์ด๋ค์์ต๋๋ค.
ํนํ GitHub Actions์ workflow_dispatch๋ฅผ ํ์ฉํ GUI ๊ธฐ๋ฐ ๋ฐฐํฌ๋ ์ ๋ง ๋ง์กฑ์ค๋ฌ์ด ๊ฒฐ๊ณผ์์ต๋๋ค. ๋ช ๋ฒ์ ํด๋ฆญ๋ง์ผ๋ก ์ํ๋ ํ๊ฒฝ์ ์ํ๋ ๋ฒ์ ์ ๋ฐฐํฌํ ์ ์๊ณ , ๋ชจ๋ฐ์ผ์์๋ ๊ฐ๋ฅํ๋ค๋ ์ ์ด ํฐ ์ฅ์ ์ด์์ต๋๋ค.
๋ฌผ๋ก self-hosted runner๋ฅผ ์ฌ์ฉํ๋ฉด์ ๋น๋ ๋จธ์ ๊ด๋ฆฌ๋ผ๋ ์๋ก์ด ๊ณผ์ ๊ฐ ์๊ฒผ์ง๋ง, ๊ทธ๋ณด๋ค๋ ์ป์ ๊ฒ์ด ํจ์ฌ ๋ง๋ค๊ณ ์๊ฐํฉ๋๋ค. ๋น๋ ์๋๋ ๋นจ๋ผ์ก๊ณ , ๋ง์ ๋ฌธ์ ๋ค์ด ๊น๋ํ๊ฒ ํด๊ฒฐ๋์์ต๋๋ค.
์์ผ๋ก๋ ์ด ์๋ํ๋ ๋ฐฐํฌ ์์คํ ์ ํตํด ๋ ๋น ๋ฅด๊ณ ์์ ์ ์ผ๋ก ํ๋กฌ ์๋น์ค๋ฅผ ๊ฐ์ ํด ๋๊ฐ๋๋ก ํ๊ฒ ์ต๋๋ค. ๊ฐ์ฌํฉ๋๋ค!