Ver3.0 ๐ ์ฑ ๋ฐฐํฌ์ CI/CD ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฆด๋ฆฌ์ฆ ์๋ํ ์์ ์ ๋ณต

๐ ์ฑ ๋ฐฐํฌ์ CI/CD ํ์ดํ๋ผ์ธ์ผ๋ก ๋ฆด๋ฆฌ์ฆ ์๋ํ ์์ ์ ๋ณต
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ์๋ ค์ฃผ๋ ์๋ํ๋ ์ฑ ๋ฐฐํฌ์ ๋ชจ๋ ๊ฒ ๐ฑโจ
๐ฏ ์ CI/CD๊ฐ ํ์ํ ๊น?
์ฌ๋ฌ๋ถ, ์ฑ ๊ฐ๋ฐํ๋ค๊ฐ ์ด๋ฐ ๊ฒฝํ ์์ผ์์ฃ ? ๐"์ ์ด๋ฒ ๋ฒ์ ๋ฐฐํฌํ๋ ค๋ฉด ๋ ๋ช ์๊ฐ ๊ฑธ๋ฆฌ๊ฒ ๋ค..."
"ํ ์คํธ๋ ๋ค ํ๋? ๋น๋ ์ค์ ์? ์ธ์ฆ์๋?"
"์ด์ ๋ฐฐํฌํ ๋ฒ์ ์ ๋ฒ๊ทธ๊ฐ ์๋... ๋กค๋ฐฑ ์ด๋ป๊ฒ ํ์ง?"
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด CI/CD๊ฐ ํ์ํ์ต๋๋ค! CI/CD๋ Continuous Integration(์ง์์ ํตํฉ)๊ณผ Continuous Delivery/Deployment(์ง์์ ์ ๋ฌ/๋ฐฐํฌ)์ ์ฝ์์ธ๋ฐ์, ์ฝ๊ฒ ๋งํ๋ฉด ์ฝ๋๋ฅผ ์์ฑํ๊ณ ๋ฐฐํฌํ๋ ์ ๊ณผ์ ์ ์๋ํํ๋ ์์คํ ์ด์์. ๐ช
์ ํต์ ์ธ ๋ฐฉ์์์๋ ๊ฐ๋ฐ์๊ฐ ์ฝ๋๋ฅผ ์์ฑํ๊ณ , ์๋์ผ๋ก ๋น๋ํ๊ณ , ํ ์คํธํ๊ณ , ๋ฐฐํฌ ํ์ผ์ ๋ง๋ค์ด์ ์คํ ์ด์ ์ ๋ก๋ํ๋ ๋ชจ๋ ๊ณผ์ ์ ์ผ์ผ์ด ์์ผ๋ก ํด์ผ ํ์ด์. ํ์ง๋ง CI/CD ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํ๋ฉด? ์ฝ๋๋ฅผ ํธ์ํ๋ ์๊ฐ๋ถํฐ ์๋์ผ๋ก ๋น๋, ํ ์คํธ, ๋ฐฐํฌ๊น์ง ์งํ๋ฉ๋๋ค! ๐
์ด๋ค ์คํํธ์ ์ CI/CD ๋์ ์ ์๋ ์ฑ ๋ฐฐํฌ์ ํ๊ท 4์๊ฐ์ด ๊ฑธ๋ ธ๋์. ํ์ง๋ง ํ์ดํ๋ผ์ธ ๊ตฌ์ถ ํ์๋? ๋จ 20๋ถ์ผ๋ก ๋จ์ถ๋์๊ณ , ์ธ์ ์ค๋ฅ๋ 90% ์ด์ ๊ฐ์ํ๋ค๊ณ ํฉ๋๋ค. ๊ฐ๋ฐ์๋ค์ ๋ฐ๋ณต ์์ ์์ ํด๋ฐฉ๋์ด ๋ ์ฐฝ์์ ์ธ ๊ธฐ๋ฅ ๊ฐ๋ฐ์ ์ง์คํ ์ ์๊ฒ ๋์์ฃ ! ๐
๐ CI/CD์ ํต์ฌ ๊ฐ๋ ํํค์น๊ธฐ
๐ฆ CI (Continuous Integration) - ์ง์์ ํตํฉ
CI๋ ์ฌ๋ฌ ๊ฐ๋ฐ์๊ฐ ์์ฑํ ์ฝ๋๋ฅผ ์์ฃผ, ์ ๊ธฐ์ ์ผ๋ก ํตํฉํ๋ ํ๋ก์ธ์ค์์. ์์ ์๋ ๊ฐ๋ฐ์๋ค์ด ๊ฐ์ ๋ช ์ฃผ์ฉ ์ฝ๋๋ฅผ ์์ฑํ๋ค๊ฐ ํ ๋ฒ์ ํฉ์น๋ ค๊ณ ํ๋ฉด ์์ฒญ๋ ์ถฉ๋(conflict)์ด ๋ฐ์ํ์ฃ . ๐ฑCI์ ์ฃผ์ ํ๋ก์ธ์ค:
1๏ธโฃ ์ฝ๋ ์ปค๋ฐ: ๊ฐ๋ฐ์๊ฐ Git ๊ฐ์ ๋ฒ์ ๊ด๋ฆฌ ์์คํ ์ ์ฝ๋๋ฅผ ํธ์
2๏ธโฃ ์๋ ๋น๋: CI ์๋ฒ๊ฐ ์๋์ผ๋ก ์ฝ๋๋ฅผ ๊ฐ์ ธ์์ ๋น๋ ์์
3๏ธโฃ ์๋ ํ ์คํธ: ์ ๋ ํ ์คํธ, ํตํฉ ํ ์คํธ ๋ฑ์ด ์๋์ผ๋ก ์คํ
4๏ธโฃ ํผ๋๋ฐฑ: ๋น๋๋ ํ ์คํธ ์คํจ ์ ์ฆ์ ๊ฐ๋ฐ์์๊ฒ ์๋ฆผ
CI์ ํต์ฌ์ "์๊ณ ์์ฃผ"์ ๋๋ค! ํฐ ๋ณ๊ฒฝ์ฌํญ์ ํ ๋ฒ์ ํธ์ํ๋ ๊ฒ๋ณด๋ค, ์์ ๋ณ๊ฒฝ์ฌํญ์ ์์ฃผ ํธ์ํ๋ ๊ฒ์ด ํจ์ฌ ์์ ํ๊ณ ๋ฌธ์ ๋ฐ๊ฒฌ๋ ๋น ๋ฆ ๋๋ค. ํ๋ฃจ์ ์ฌ๋ฌ ๋ฒ ์ปค๋ฐํ๋ ์ต๊ด์ ๋ค์ด์ธ์! ๐ช
๐ข CD (Continuous Delivery/Deployment) - ์ง์์ ์ ๋ฌ/๋ฐฐํฌ
CD๋ ๋ ๊ฐ์ง ์๋ฏธ๋ก ์ฌ์ฉ๋ผ์:Continuous Delivery (์ง์์ ์ ๋ฌ)
๋น๋์ ํ ์คํธ๋ฅผ ์๋ํํ๊ณ , ๋ฐฐํฌ ์ค๋น๊ฐ ์๋ฃ๋ ์ํ๊น์ง ์๋์ผ๋ก ๋ง๋ค์ด์ฃผ๋ ๊ฒ์ด์์. ์ค์ ๋ฐฐํฌ๋ ์ฌ๋์ด ๋ฒํผ์ ๋๋ฌ์ ์งํํฉ๋๋ค. ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ์ต์ข ๊ฒํ ๊ฐ ํ์ํ ๊ฒฝ์ฐ์ ์ ํฉํ์ฃ . ๐
Continuous Deployment (์ง์์ ๋ฐฐํฌ)
ํ ๋จ๊ณ ๋ ๋์๊ฐ์ ๋ฐฐํฌ๊น์ง ์์ ์๋ํํ๋ ๊ฑฐ์์! ํ ์คํธ๋ฅผ ํต๊ณผํ ์ฝ๋๋ ์ฌ๋์ ๊ฐ์ ์์ด ์๋์ผ๋ก ํ๋ก๋์ ํ๊ฒฝ์ ๋ฐฐํฌ๋ฉ๋๋ค. ๋ฌผ๋ก ์ด๊ฑด ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ์ถฉ๋ถํ ๋๊ณ , ๋กค๋ฐฑ ์์คํ ์ด ์ ๊ฐ์ถฐ์ง ๊ฒฝ์ฐ์๋ง ๊ถ์ฅ๋ฉ๋๋ค. ๐ฏ
๐ ๏ธ ๋ชจ๋ฐ์ผ ์ฑ ๋ฐฐํฌ๋ฅผ ์ํ CI/CD ๋๊ตฌ๋ค
๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์์ ์ฌ์ฉํ ์ ์๋ CI/CD ๋๊ตฌ๋ ์ ๋ง ๋ค์ํด์! ๊ฐ๊ฐ์ ํน์ง์ ์ดํด๋ณผ๊น์? ๐ง1. GitHub Actions โก
โข GitHub์ ์๋ฒฝํ๊ฒ ํตํฉ๋์ด ๋ณ๋ ์ค์ ์ด ๊ฑฐ์ ํ์ ์์
โข YAML ํ์ผ๋ก ์ํฌํ๋ก์ฐ ์ ์
โข ํผ๋ธ๋ฆญ ๋ ํฌ์งํ ๋ฆฌ๋ ๋ฌด๋ฃ, ํ๋ผ์ด๋น์ ์ 2,000๋ถ ๋ฌด๋ฃ
โข Marketplace์ ์์ฒ ๊ฐ์ ์ก์ (Action) ์ ๊ณต
์ฅ์ :
โ ์ค์ ์ด ๊ฐ๋จํ๊ณ ์ง๊ด์
โ GitHub ์ํ๊ณ์ ์๋ฒฝํ ํธํ
โ ์ปค๋ฎค๋ํฐ๊ฐ ํ๋ฐํ๊ณ ๋ ํผ๋ฐ์ค๊ฐ ๋ง์
โ ๋งคํธ๋ฆญ์ค ๋น๋๋ก ์ฌ๋ฌ ํ๊ฒฝ ๋์ ํ ์คํธ ๊ฐ๋ฅ
๋จ์ :
โ ๋ณต์กํ ํ์ดํ๋ผ์ธ์ YAML์ด ๊ธธ์ด์ง ์ ์์
โ ๋น๋ ์๊ฐ์ด ๋ค๋ฅธ ๋๊ตฌ๋ณด๋ค ์ฝ๊ฐ ๋๋ฆด ์ ์์
์ถ์ฒ ๋์: GitHub๋ฅผ ๋ฉ์ธ์ผ๋ก ์ฌ์ฉํ๋ ํ, ์ค์๊ท๋ชจ ํ๋ก์ ํธ
2. Fastlane ๐
โข iOS์ Android ์ฑ ๋ฐฐํฌ์ ํนํ๋ ์คํ์์ค ๋๊ตฌ
โข Ruby ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋์ด ์์
โข ์คํฌ๋ฆฐ์ท ์๋ ์์ฑ, ๋ฉํ๋ฐ์ดํฐ ๊ด๋ฆฌ ๋ฑ ๋ถ๊ฐ ๊ธฐ๋ฅ ํ๋ถ
โข ๋ค๋ฅธ CI ๋๊ตฌ์ ์กฐํฉํด์ ์ฌ์ฉ ๊ฐ๋ฅ
์ฃผ์ ๊ธฐ๋ฅ:
๐ฑ match: iOS ์ธ์ฆ์์ ํ๋ก๋น์ ๋ ํ๋กํ์ผ ๊ด๋ฆฌ
๐ธ snapshot: ์๋์ผ๋ก ์ฑ ์คํฌ๋ฆฐ์ท ์์ฑ
๐ deliver: App Store์ ์๋ ์ ๋ก๋
๐ค supply: Google Play์ ์๋ ์ ๋ก๋
๐งช scan: ์๋ํ๋ ํ ์คํธ ์คํ
์ถ์ฒ ๋์: ๋ชจ๋ฐ์ผ ์ฑ ๋ฐฐํฌ์ ์ง์คํ๋ ํ, iOS/Android ๋์ ๊ฐ๋ฐ
3. Jenkins ๐๏ธ
โข ๊ฐ์ฅ ์ค๋๋๊ณ ์ฑ์ํ CI/CD ๋๊ตฌ
โข ์์ฒด ํธ์คํ ๋ฐฉ์ (์๋ฒ ์ง์ ๊ด๋ฆฌ)
โข 1,800๊ฐ ์ด์์ ํ๋ฌ๊ทธ์ธ์ผ๋ก ๋ฌดํ ํ์ฅ ๊ฐ๋ฅ
โข ์์ ํ ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅ
์ฅ์ :
โ ๋งค์ฐ ์ ์ฐํ๊ณ ๊ฐ๋ ฅํจ
โ ์จํ๋ ๋ฏธ์ค ํ๊ฒฝ์์ ์์ ํ ์ ์ด ๊ฐ๋ฅ
โ ๋ ๊ฑฐ์ ์์คํ ๊ณผ์ ํตํฉ์ด ์ฌ์
โ ๋๊ท๋ชจ ์ํฐํ๋ผ์ด์ฆ ํ๊ฒฝ์ ์ ํฉ
๋จ์ :
โ ์ด๊ธฐ ์ค์ ๊ณผ ์ ์ง๋ณด์๊ฐ ๋ณต์ก
โ ์๋ฒ ๊ด๋ฆฌ ํ์ (๋น์ฉ๊ณผ ์ธ๋ ฅ)
โ UI๊ฐ ๋ค์ ๊ตฌ์
์ถ์ฒ ๋์: ๋๊ธฐ์ , ๋ณต์กํ ์๊ตฌ์ฌํญ์ด ์๋ ํ๋ก์ ํธ, ์จํ๋ ๋ฏธ์ค ํ๊ฒฝ
4. GitLab CI/CD ๐ฆ
โข GitLab์ ๋ด์ฅ๋ CI/CD ์์คํ
โข DevOps ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด์ ํ๋์ ํ๋ซํผ์์ ๊ด๋ฆฌ
โข Auto DevOps ๊ธฐ๋ฅ์ผ๋ก ์๋ ์ค์ ๊ฐ๋ฅ
โข Kubernetes์์ ํตํฉ์ด ๋ฐ์ด๋จ
์ฅ์ :
โ ์์ค ๊ด๋ฆฌ๋ถํฐ ๋ฐฐํฌ๊น์ง ํตํฉ ํ๊ฒฝ
โ ๊ฐ๋ ฅํ ๋ณด์ ์ค์บ๋ ๊ธฐ๋ฅ
โ ์ ํ ํธ์คํ ๊ณผ ํด๋ผ์ฐ๋ ๋ชจ๋ ์ง์
โ ๋ฌด๋ฃ ํฐ์ด๋ ๊ธฐ๋ฅ์ด ํ๋ถํจ
์ถ์ฒ ๋์: GitLab ์ฌ์ฉ์, DevSecOps๋ฅผ ์ค์์ํ๋ ํ
5. Bitrise ๐ฒ
โข ๋ชจ๋ฐ์ผ ์ฑ ์ ์ฉ CI/CD ํ๋ซํผ
โข ๋๋๊ทธ ์ค ๋๋กญ ๋ฐฉ์์ ์ํฌํ๋ก์ฐ ๋น๋
โข iOS, Android, React Native, Flutter ๋ฑ ๋ชจ๋ ๋ชจ๋ฐ์ผ ํ๋ ์์ํฌ ์ง์
โข ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ์ผ๋ก ์๋ฒ ๊ด๋ฆฌ ๋ถํ์
์ฅ์ :
โ ๋ชจ๋ฐ์ผ์ ์ต์ ํ๋ ๊ธฐ๋ฅ๋ค
โ ์ค์ ์ด ๋งค์ฐ ์ฝ๊ณ ์ง๊ด์
โ 300๊ฐ ์ด์์ ํตํฉ ์คํ ์ ๊ณต
โ ์ค์ ๋๋ฐ์ด์ค์์ ํ ์คํธ ๊ฐ๋ฅ
๋จ์ :
โ ์ ๋ฃ ํ๋์ด ๋ค์ ๋น์
โ ๋ชจ๋ฐ์ผ ์ธ ํ๋ก์ ํธ์๋ ๋ถ์ ํฉ
์ถ์ฒ ๋์: ๋ชจ๋ฐ์ผ ์ ๋ฌธ ๊ฐ๋ฐํ, ๋น ๋ฅธ ์ค์ ์ ์ํ๋ ์คํํธ์
6. CircleCI โญ
โข ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ CI/CD ํ๋ซํผ
โข Docker ์ปจํ ์ด๋ ๊ธฐ๋ฐ ๋น๋
โข ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ๋น ๋ฅธ ๋น๋ ์๋
โข ์ 6,000๋ถ ๋ฌด๋ฃ (Linux ๊ธฐ์ค)
์ฅ์ :
โ ๋น๋ ์๋๊ฐ ๋งค์ฐ ๋น ๋ฆ
โ ์ค์ ํ์ผ์ด ๊ฐ๊ฒฐํจ
โ SSH ๋๋ฒ๊น ๊ธฐ๋ฅ ์ ๊ณต
โ ์ํฌํ๋ก์ฐ ์๊ฐํ๊ฐ ํ๋ฅญํจ
์ถ์ฒ ๋์: ๋น๋ ์๋๊ฐ ์ค์ํ ํ๋ก์ ํธ, Docker ์ฌ์ฉ ํ
์ด๋ณด์๋ผ๋ฉด? โ GitHub Actions ๋๋ Bitrise
๋ชจ๋ฐ์ผ ์ ๋ฌธ์ด๋ผ๋ฉด? โ Fastlane + Bitrise ์กฐํฉ
๋๊ธฐ์ ์ด๋ผ๋ฉด? โ Jenkins ๋๋ GitLab CI/CD
๋น ๋ฅธ ๋น๋๊ฐ ํ์ํ๋ค๋ฉด? โ CircleCI
๋น์ฉ์ ์ต์ํํ๋ ค๋ฉด? โ GitHub Actions (ํผ๋ธ๋ฆญ ๋ ํฌ) ๋๋ GitLab CI/CD
์ฌ์ค ์ ๋ต์ ์์ด์! ํ์ ์ํฉ, ์์ฐ, ๊ธฐ์ ์คํ์ ๋ง์ถฐ ์ ํํ๋ฉด ๋ฉ๋๋ค. ๐
๐ฑ iOS ์ฑ ๋ฐฐํฌ ์๋ํ ์ค์ ๊ฐ์ด๋
iOS ์ฑ ๋ฐฐํฌ๋ Android๋ณด๋ค ๋ณต์กํ ํธ์ด์์. ์ธ์ฆ์, ํ๋ก๋น์ ๋ ํ๋กํ์ผ, App Store Connect ๋ฑ ์ ๊ฒฝ ์จ์ผ ํ ๊ฒ ๋ง์ฃ . ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์์๋ณผ๊น์? ๐๐ iOS ์ธ์ฆ์์ ํ๋ก๋น์ ๋ ํ๋กํ์ผ ์ดํดํ๊ธฐ
iOS ์ฑ์ ๋ฐฐํฌํ๋ ค๋ฉด ๋ฐ๋์ ํ์ํ ๊ฒ๋ค์ด ์์ด์:โข ์ฐ๊ฐ $99 (์ฝ 13๋ง์) ๋น์ฉ
โข ๊ฐ์ธ ๋๋ ์กฐ์ง ๊ณ์ ์ ํ ๊ฐ๋ฅ
โข App Store Connect ์ ๊ทผ ๊ถํ ํฌํจ
2. ์ธ์ฆ์ (Certificate)
โข Development Certificate: ๊ฐ๋ฐ ์ค ํ ์คํธ์ฉ
โข Distribution Certificate: ๋ฐฐํฌ์ฉ (App Store, Ad Hoc, Enterprise)
โข ๊ฐ์ธ๋น ๊ฐ ํ์ ๋ณ๋ก ์ต๋ 2๊ฐ๊น์ง ์์ฑ ๊ฐ๋ฅ
โข .p12 ํ์ผ๋ก ๋ด๋ณด๋ด๊ธฐ ๊ฐ๋ฅ
3. ํ๋ก๋น์ ๋ ํ๋กํ์ผ (Provisioning Profile)
โข ์ฑ, ์ธ์ฆ์, ๋๋ฐ์ด์ค๋ฅผ ์ฐ๊ฒฐํ๋ ํ์ผ
โข Development: ๊ฐ๋ฐ ์ค ํ ์คํธ
โข Ad Hoc: ํน์ ๋๋ฐ์ด์ค์ ๋ฐฐํฌ (์ต๋ 100๋)
โข App Store: ์ฑ์คํ ์ด ๋ฐฐํฌ์ฉ
โข Enterprise: ๊ธฐ์ ๋ด๋ถ ๋ฐฐํฌ์ฉ
4. App ID
โข ์ฑ์ ๊ณ ์ ํ๊ฒ ์๋ณํ๋ ID (์: com.company.appname)
โข Explicit App ID: ํน์ ์ฑ ํ๋๋ง
โข Wildcard App ID: ์ฌ๋ฌ ์ฑ์ ์ฌ์ฉ ๊ฐ๋ฅ (์ ํ์ ๊ธฐ๋ฅ)
ํ์์ ์ธ์ฆ์๋ฅผ ๊ด๋ฆฌํ๋ ๊ฑด ์ ๋ง ๊ณจ์น ์ํ ์ผ์ด์์. A ๊ฐ๋ฐ์๊ฐ ๋ง๋ ์ธ์ฆ์๋ฅผ B ๊ฐ๋ฐ์๊ฐ ์ฌ์ฉํ๋ ค๋ฉด .p12 ํ์ผ์ ๊ณต์ ํด์ผ ํ๊ณ , ๋น๋ฐ๋ฒํธ๋ ์์์ผ ํ์ฃ . ๐ฐ
Fastlane Match๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ๋ชจ๋ ๊ฒ ํด๊ฒฐ๋ฉ๋๋ค! Git ๋ ํฌ์งํ ๋ฆฌ์ ์ํธํ๋ ์ธ์ฆ์๋ฅผ ์ ์ฅํ๊ณ , ํ์๋ค์ด ์๋์ผ๋ก ๋๊ธฐํํ ์ ์์ด์. CI/CD ํ๊ฒฝ์์๋ ์๋ฒฝํ๊ฒ ์๋ํฉ๋๋ค! ๐
๐ GitHub Actions๋ก iOS ์ฑ ์๋ ๋ฐฐํฌํ๊ธฐ
์ค์ ๋ก ์ฌ์ฉํ ์ ์๋ GitHub Actions ์ํฌํ๋ก์ฐ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์! ๐ชStep 1: ํ๋ก์ ํธ ์ค๋น
๋จผ์ Xcode ํ๋ก์ ํธ๊ฐ ์ค๋น๋์ด ์์ด์ผ ํด์. ๊ทธ๋ฆฌ๊ณ Fastlane์ ์ค์นํฉ๋๋ค:
sudo gem install fastlane -NV
cd your-ios-project
fastlane initFastlane ์ด๊ธฐํ ์ 4๊ฐ์ง ์ต์ ์ด ๋์ค๋๋ฐ, "๐ธ Automate App Store distribution"์ ์ ํํ๋ฉด ๋ฉ๋๋ค.
Step 2: Fastlane ์ค์
fastlane/Fastfile์ ์ด์ด์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํด์:
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
# ์ธ์ฆ์ ๋๊ธฐํ
match(type: "appstore")
# ๋น๋ ๋ฒํธ ์๋ ์ฆ๊ฐ
increment_build_number(xcodeproj: "YourApp.xcodeproj")
# ์ฑ ๋น๋
build_app(
scheme: "YourApp",
export_method: "app-store"
)
# TestFlight์ ์
๋ก๋
upload_to_testflight(
skip_waiting_for_build_processing: true
)
# Slack ์๋ฆผ (์ ํ์ฌํญ)
slack(
message: "์๋ก์ด ๋น๋๊ฐ TestFlight์ ์
๋ก๋๋์์ต๋๋ค! ๐",
success: true
)
end
desc "Deploy to App Store"
lane :release do
match(type: "appstore")
# ๋ฒ์ ๋ฒํธ ์
๋ ฅ๋ฐ๊ธฐ
version = prompt(text: "๋ฒ์ ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์: ")
increment_version_number(version_number: version)
build_app(
scheme: "YourApp",
export_method: "app-store"
)
# App Store์ ์
๋ก๋
upload_to_app_store(
submit_for_review: false, # ์๋ ์ฌ์ฌ ์ ์ถ ์ฌ๋ถ
automatic_release: false, # ์น์ธ ํ ์๋ ์ถ์ ์ฌ๋ถ
force: true
)
slack(
message: "์ฑ์ด App Store์ ์ ์ถ๋์์ต๋๋ค! ๐",
success: true
)
end
endStep 3: GitHub Secrets ์ค์
GitHub ๋ ํฌ์งํ ๋ฆฌ์ Settings โ Secrets and variables โ Actions์์ ๋ค์ ์ํฌ๋ฆฟ์ ์ถ๊ฐํด์:
Step 4: GitHub Actions ์ํฌํ๋ก์ฐ ์์ฑ
.github/workflows/ios-deploy.yml ํ์ผ์ ์์ฑํฉ๋๋ค:
name: iOS CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
workflow_dispatch: # ์๋ ์คํ ๊ฐ๋ฅ
jobs:
build-and-deploy:
runs-on: macos-latest
steps:
- name: ์ฝ๋ ์ฒดํฌ์์
uses: actions/checkout@v3
- name: Xcode ๋ฒ์ ์ ํ
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.0'
- name: Ruby ์ค์
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Fastlane ์ค์น
run: |
gem install fastlane
fastlane --version
- name: ์์กด์ฑ ์ค์น
run: |
cd ios
pod install
- name: ์บ์ ์ค์
uses: actions/cache@v3
with:
path: |
~/Library/Caches/CocoaPods
Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
- name: ํ
์คํธ ์คํ
run: |
xcodebuild test \
-workspace YourApp.xcworkspace \
-scheme YourApp \
-destination 'platform=iOS Simulator,name=iPhone 14'
- name: TestFlight ๋ฐฐํฌ
if: github.ref == 'refs/heads/main'
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
run: |
cd ios
fastlane beta
- name: ๋น๋ ์ํฐํฉํธ ์
๋ก๋
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-logs
path: |
~/Library/Logs/gym
fastlane/report.xml1๏ธโฃ ํธ๋ฆฌ๊ฑฐ: main, develop ๋ธ๋์น์ ํธ์ํ๊ฑฐ๋ PR ์์ฑ ์ ์คํ
2๏ธโฃ ํ๊ฒฝ: macOS ์ต์ ๋ฒ์ (iOS ๋น๋๋ macOS์์๋ง ๊ฐ๋ฅ)
3๏ธโฃ ํ ์คํธ: ์๋ฎฌ๋ ์ดํฐ์์ ์๋ ํ ์คํธ ์คํ
4๏ธโฃ ๋ฐฐํฌ: main ๋ธ๋์น์ผ ๋๋ง TestFlight์ ์๋ ๋ฐฐํฌ
5๏ธโฃ ์บ์: CocoaPods ์์กด์ฑ ์บ์ฑ์ผ๋ก ๋น๋ ์๊ฐ ๋จ์ถ
6๏ธโฃ ๋ก๊ทธ: ์คํจ ์ ๋น๋ ๋ก๊ทธ ์๋ ์ ๋ก๋
๐จ ๊ณ ๊ธ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ
1. ์๋ ์คํฌ๋ฆฐ์ท ์์ฑFastlane Snapshot์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๋๋ฐ์ด์ค์ ์คํฌ๋ฆฐ์ท์ ์๋์ผ๋ก ์์ฑํ ์ ์์ด์:
fastlane snapshot init
# Snapfile ์ค์
devices([
"iPhone 14 Pro Max",
"iPhone 14",
"iPhone SE (3rd generation)",
"iPad Pro (12.9-inch)"
])
languages([
"ko",
"en-US",
"ja"
])
scheme("YourAppUITests")2. ์๋ ๋ฒ์ ๊ด๋ฆฌ
Git ํ๊ทธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฒ์ ์ ์๋์ผ๋ก ๊ด๋ฆฌํ ์ ์์ด์:
lane :bump_version do
# ํ์ฌ ๋ฒ์ ๊ฐ์ ธ์ค๊ธฐ
version = get_version_number(xcodeproj: "YourApp.xcodeproj")
# ๋น๋ ๋ฒํธ๋ฅผ Git ์ปค๋ฐ ์๋ก ์ค์
build_number = number_of_commits()
increment_build_number(build_number: build_number)
# Git ํ๊ทธ ์์ฑ
add_git_tag(tag: "v#{version}(#{build_number})")
push_git_tags()
end3. ์กฐ๊ฑด๋ถ ๋ฐฐํฌ
์ปค๋ฐ ๋ฉ์์ง์ ๋ฐ๋ผ ๋ฐฐํฌ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ ์ ์์ด์:
lane :smart_deploy do
# ๋ง์ง๋ง ์ปค๋ฐ ๋ฉ์์ง ํ์ธ
commit_message = last_git_commit[:message]
if commit_message.include?("[deploy]")
UI.success("๐ ๋ฐฐํฌ ํ๊ทธ ๋ฐ๊ฒฌ! TestFlight์ ์
๋ก๋ํฉ๋๋ค.")
beta
elsif commit_message.include?("[release]")
UI.success("๐ ๋ฆด๋ฆฌ์ฆ ํ๊ทธ ๋ฐ๊ฒฌ! App Store์ ์ ์ถํฉ๋๋ค.")
release
else
UI.message("๋ฐฐํฌ ํ๊ทธ๊ฐ ์์ต๋๋ค. ๋น๋๋ง ์ํํฉ๋๋ค.")
build_app(scheme: "YourApp")
end
end
๐ค Android ์ฑ ๋ฐฐํฌ ์๋ํ ์ค์ ๊ฐ์ด๋
Android ์ฑ ๋ฐฐํฌ๋ iOS๋ณด๋ค ์๋์ ์ผ๋ก ๊ฐ๋จํ ํธ์ด์์. ํ์ง๋ง ๊ทธ๋งํผ ์ ๊ฒฝ ์จ์ผ ํ ๋ถ๋ถ๋ ์์ฃ ! ๐ฏ๐ Android ์๋ช ํค ๊ด๋ฆฌ
Android ์ฑ์ ๋ฐฐํฌํ๋ ค๋ฉด ์๋ช ํค(Signing Key)๊ฐ ํ์ํด์. ์ด๊ฑด ์ฑ์ ์ ์์ ์ฆ๋ช ํ๋ ๋์งํธ ์ธ์ฆ์์์.keytool -genkey -v -keystore my-release-key.keystore \
-alias my-key-alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000์ด ๋ช ๋ น์ด๋ฅผ ์คํํ๋ฉด ์ฌ๋ฌ ์ ๋ณด๋ฅผ ์ ๋ ฅํ๋ผ๊ณ ๋์์:
โข ๋น๋ฐ๋ฒํธ (์ ๋ ์์ผ๋ฉด ์ ๋จ! ๐จ)
โข ์ด๋ฆ, ์กฐ์ง, ๋์, ๊ตญ๊ฐ ๋ฑ
โ ๏ธ ์ค์ํ ์ฃผ์์ฌํญ:
โข ํค์คํ ์ด ํ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ ์ ๋ Git์ ์ปค๋ฐํ๋ฉด ์ ๋ฉ๋๋ค!
โข ํค์คํ ์ด๋ฅผ ์์ด๋ฒ๋ฆฌ๋ฉด ์ฑ ์ ๋ฐ์ดํธ๊ฐ ๋ถ๊ฐ๋ฅํด์
โข ์์ ํ ๊ณณ์ ๋ฐฑ์ ํ์! (ํด๋ผ์ฐ๋ ์ ์ฅ์, ๋น๋ฐ๋ฒํธ ๊ด๋ฆฌ์ ๋ฑ)
2. Gradle ์ค์
android/app/build.gradle์ ์๋ช ์ค์ ์ ์ถ๊ฐํด์:
android {
...
signingConfigs {
release {
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}3. gradle.properties ์ค์
android/gradle.properties (Git์ ์ปค๋ฐํ์ง ์์!):
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=****
MYAPP_RELEASE_KEY_PASSWORD=****
Google Play Console์์ App Signing ๊ธฐ๋ฅ์ ํ์ฑํํ๋ฉด, Google์ด ์ต์ข ์๋ช ํค๋ฅผ ๊ด๋ฆฌํด์ค์. ์ฌ๋ฌ๋ถ์ ์ ๋ก๋ ํค๋ง ๊ด๋ฆฌํ๋ฉด ๋๊ณ , ๋ง์ฝ ์ ๋ก๋ ํค๋ฅผ ์์ด๋ฒ๋ ค๋ Google ์ง์ํ์ ์์ฒญํด์ ์ฌ๋ฐ๊ธ๋ฐ์ ์ ์์ด์! ํจ์ฌ ์์ ํ์ฃ ? ๐
๐ GitHub Actions๋ก Android ์ฑ ์๋ ๋ฐฐํฌํ๊ธฐ
Step 1: Fastlane ์ค์ (Android)cd android
fastlane initandroid/fastlane/Fastfile:
default_platform(:android)
platform :android do
desc "Deploy to Google Play Internal Testing"
lane :internal do
gradle(
task: "clean bundleRelease",
project_dir: "android/"
)
upload_to_play_store(
track: 'internal',
aab: 'app/build/outputs/bundle/release/app-release.aab',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
slack(
message: "Android ์ฑ์ด Internal Testing์ ์
๋ก๋๋์์ต๋๋ค! ๐"
)
end
desc "Deploy to Google Play Beta"
lane :beta do
gradle(
task: "clean bundleRelease",
project_dir: "android/"
)
upload_to_play_store(
track: 'beta',
aab: 'app/build/outputs/bundle/release/app-release.aab'
)
end
desc "Deploy to Google Play Production"
lane :production do
gradle(
task: "clean bundleRelease",
project_dir: "android/"
)
upload_to_play_store(
track: 'production',
aab: 'app/build/outputs/bundle/release/app-release.aab',
rollout: '0.1' # 10%์๊ฒ๋ง ๋จผ์ ๋ฐฐํฌ (๋จ๊ณ์ ์ถ์)
)
slack(
message: "๐ Android ์ฑ์ด ํ๋ก๋์
์ ๋ฐฐํฌ๋์์ต๋๋ค! (10% ๋กค์์)"
)
end
endStep 2: Google Play Console API ์ค์
1๏ธโฃ Google Cloud Console์์ ํ๋ก์ ํธ ์์ฑ
2๏ธโฃ Google Play Android Developer API ํ์ฑํ
3๏ธโฃ ์๋น์ค ๊ณ์ ์์ฑ ๋ฐ JSON ํค ๋ค์ด๋ก๋
4๏ธโฃ Google Play Console์์ ์๋น์ค ๊ณ์ ์ ๊ถํ ๋ถ์ฌ
Step 3: GitHub Secrets ์ค์
ํค์คํ ์ด๋ฅผ Base64๋ก ์ธ์ฝ๋ฉํ๋ ๋ฐฉ๋ฒ:
base64 -i my-release-key.keystore | pbcopy # macOS
base64 my-release-key.keystore | clip # WindowsStep 4: GitHub Actions ์ํฌํ๋ก์ฐ
.github/workflows/android-deploy.yml:
name: Android CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: ์ฝ๋ ์ฒดํฌ์์
uses: actions/checkout@v3
- name: JDK 17 ์ค์
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Ruby ์ค์
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Fastlane ์ค์น
run: |
gem install fastlane
cd android
fastlane --version
- name: Gradle ์บ์
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: ํค์คํ ์ด ๋์ฝ๋ฉ
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/my-release-key.keystore
- name: gradle.properties ์์ฑ
run: |
echo "MYAPP_RELEASE_STORE_FILE=my-release-key.keystore" >> android/gradle.properties
echo "MYAPP_RELEASE_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> android/gradle.properties
echo "MYAPP_RELEASE_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> android/gradle.properties
echo "MYAPP_RELEASE_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> android/gradle.properties
- name: ์ ๋ ํ
์คํธ ์คํ
run: |
cd android
./gradlew test
- name: ๋ฆฐํธ ์ฒดํฌ
run: |
cd android
./gradlew lint
- name: APK ๋น๋ (develop ๋ธ๋์น)
if: github.ref == 'refs/heads/develop'
run: |
cd android
./gradlew assembleRelease
- name: Google Play Service Account ์ค์
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}" > android/fastlane/google-play-service-account.json
- name: Internal Testing ๋ฐฐํฌ
if: github.ref == 'refs/heads/main'
run: |
cd android
fastlane internal
- name: APK ์ํฐํฉํธ ์
๋ก๋
uses: actions/upload-artifact@v3
with:
name: app-release
path: android/app/build/outputs/apk/release/app-release.apk
- name: ํ
์คํธ ๋ฆฌํฌํธ ์
๋ก๋
if: always()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: android/app/build/reports/
๐ ๋ฒ์ ๊ด๋ฆฌ ์๋ํ
Android ์ฑ์ ๋ฒ์ ์ฝ๋์ ๋ฒ์ ๋ค์์ ์๋์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด์์:android/app/build.gradle:
def getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', '--count', 'HEAD'
standardOutput = stdout
}
return Integer.parseInt(stdout.toString().trim())
} catch (Exception e) {
return 1
}
}
def getVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--tags', '--always'
standardOutput = stdout
}
return stdout.toString().trim()
} catch (Exception e) {
return "1.0.0"
}
}
android {
defaultConfig {
versionCode getVersionCode()
versionName getVersionName()
}
}์ด๋ ๊ฒ ํ๋ฉด Git ์ปค๋ฐ ์๊ฐ ์๋์ผ๋ก ๋ฒ์ ์ฝ๋๊ฐ ๋๊ณ , Git ํ๊ทธ๊ฐ ๋ฒ์ ๋ค์์ด ๋ฉ๋๋ค! ๐ฏ
๐ ๋ฉํฐ ํ๋ซํผ ์ฑ์ CI/CD ์ ๋ต
React Native, Flutter ๊ฐ์ ํฌ๋ก์ค ํ๋ซํผ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด? iOS์ Android๋ฅผ ๋์์ ๋น๋ํ๊ณ ๋ฐฐํฌํด์ผ ํ์ฃ ! ๐ช๐ฏ React Native ์ฑ ๋ฐฐํฌ ์๋ํ
React Native๋ JavaScript๋ก ์์ฑํ์ง๋ง, ๋ค์ดํฐ๋ธ ๋น๋๊ฐ ํ์ํด์. ์์ชฝ ํ๋ซํผ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊น์? ๐ฑํ๋ก์ ํธ ๊ตฌ์กฐ ์ต์ ํ
my-rn-app/
โโโ .github/
โ โโโ workflows/
โ โโโ ios-deploy.yml
โ โโโ android-deploy.yml
โ โโโ test.yml
โโโ android/
โ โโโ fastlane/
โโโ ios/
โ โโโ fastlane/
โโโ src/
โโโ __tests__/
โโโ package.jsonํตํฉ ์ํฌํ๋ก์ฐ ์์
.github/workflows/deploy-all.yml:
name: Deploy to Both Platforms
on:
push:
tags:
- 'v*' # v1.0.0 ๊ฐ์ ํ๊ทธ๊ฐ ํธ์๋๋ฉด ์คํ
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Node.js ์ค์
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: ์์กด์ฑ ์ค์น
run: npm ci
- name: ๋ฆฐํธ ์ฒดํฌ
run: npm run lint
- name: ํ์
์ฒดํฌ
run: npm run type-check
- name: ์ ๋ ํ
์คํธ
run: npm test -- --coverage
- name: E2E ํ
์คํธ (Detox)
run: |
npm run build:e2e
npm run test:e2e
deploy-ios:
needs: test
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Node.js ์ค์
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: ์์กด์ฑ ์ค์น
run: |
npm ci
cd ios && pod install
- name: Fastlane ์ค์
run: |
gem install fastlane
cd ios
fastlane beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
deploy-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Node.js ์ค์
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: JDK ์ค์
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: ์์กด์ฑ ์ค์น
run: npm ci
- name: ํค์คํ ์ด ์ค์
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/my-release-key.keystore
- name: Fastlane ๋ฐฐํฌ
run: |
gem install fastlane
cd android
fastlane internal
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
notify:
needs: [deploy-ios, deploy-android]
runs-on: ubuntu-latest
steps:
- name: Slack ์๋ฆผ
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: '๐ ์ ๋ฒ์ ์ด iOS์ Android ๋ชจ๋์ ๋ฐฐํฌ๋์์ต๋๋ค!'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
1๏ธโฃ CodePush ํ์ฉ: Microsoft์ CodePush๋ฅผ ์ฌ์ฉํ๋ฉด JavaScript ๋ฒ๋ค๋ง ์ ๋ฐ์ดํธํ ์ ์์ด์. ์คํ ์ด ์ฌ์ฌ ์์ด ์ฆ์ ๋ฐฐํฌ ๊ฐ๋ฅ! ๐
2๏ธโฃ OTA ์ ๋ฐ์ดํธ: Expo๋ฅผ ์ฌ์ฉํ๋ค๋ฉด Over-The-Air ์ ๋ฐ์ดํธ๋ก ๋ ๋น ๋ฅธ ๋ฐฐํฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
3๏ธโฃ ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ: react-native-config๋ก ๊ฐ๋ฐ/์คํ ์ด์ง/ํ๋ก๋์ ํ๊ฒฝ์ ๋ถ๋ฆฌํ์ธ์.
4๏ธโฃ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ: Hermes ์์ง ํ์ฑํ๋ก ์ฑ ํฌ๊ธฐ์ ์์ ์๊ฐ์ ์ค์ผ ์ ์์ด์!
๐ฆ Flutter ์ฑ ๋ฐฐํฌ ์๋ํ
Flutter๋ Dart๋ก ์์ฑํ๊ณ , ๋ค์ดํฐ๋ธ ์ฝ๋๋ก ์ปดํ์ผ๋ผ์. ๋น๋ ํ๋ก์ธ์ค๊ฐ React Native์ ์ฝ๊ฐ ๋ค๋ฅด์ฃ ! ๐จFlutter์ฉ GitHub Actions ์ํฌํ๋ก์ฐ
name: Flutter CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Flutter ์ค์
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
channel: 'stable'
- name: ์์กด์ฑ ์ค์น
run: flutter pub get
- name: ์ฝ๋ ๋ถ์
run: flutter analyze
- name: ํ
์คํธ ์คํ
run: flutter test --coverage
- name: ์ปค๋ฒ๋ฆฌ์ง ์
๋ก๋
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build-ios:
needs: test
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Flutter ์ค์
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: ์์กด์ฑ ์ค์น
run: |
flutter pub get
cd ios && pod install
- name: iOS ๋น๋
run: |
flutter build ios --release --no-codesign
- name: Fastlane ๋ฐฐํฌ
run: |
cd ios
fastlane beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Flutter ์ค์
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: JDK ์ค์
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: ์์กด์ฑ ์ค์น
run: flutter pub get
- name: ํค์คํ ์ด ์ค์
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/upload-keystore.jks
echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
echo "storeFile=upload-keystore.jks" >> android/key.properties
- name: AAB ๋น๋
run: flutter build appbundle --release
- name: Google Play ์
๋ก๋
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.yourcompany.yourapp
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: internal
1. Flavors ํ์ฉํ๊ธฐ
Flutter์ Flavor ๊ธฐ๋ฅ์ผ๋ก ๊ฐ๋ฐ/์คํ ์ด์ง/ํ๋ก๋์ ํ๊ฒฝ์ ๋ถ๋ฆฌํ ์ ์์ด์:
flutter build apk --flavor production
flutter build ios --flavor development
2. ๋น๋ ๋ฒํธ ์๋ํ
pubspec.yaml์ ๋ฒ์ ์ Git ํ๊ทธ์ ๋๊ธฐํํ์ธ์!
3. ์ฑ ํฌ๊ธฐ ์ต์ ํ
--split-per-abi ํ๋๊ทธ๋ก ABI๋ณ APK๋ฅผ ์์ฑํ๋ฉด ์ฑ ํฌ๊ธฐ๊ฐ ์ค์ด๋ค์ด์.
๐งช ํ ์คํธ ์๋ํ์ ํ์ง ๊ด๋ฆฌ
CI/CD์ ํต์ฌ์ ์๋ํ๋ ํ ์คํธ์์! ์ฝ๋ ํ์ง์ ๋ณด์ฅํ๊ณ ๋ฒ๊ทธ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ๋ ๊ฒ ์ค์ํ์ฃ . ๐ฏ๐ ํ ์คํธ ํผ๋ผ๋ฏธ๋
โข ๊ฐ๋ณ ํจ์๋ ํด๋์ค๋ฅผ ํ ์คํธ
โข ์คํ ์๋๊ฐ ๋งค์ฐ ๋น ๋ฆ (๋ฐ๋ฆฌ์ด ๋จ์)
โข ์ ์ฒด ํ ์คํธ์ 70% ์ ๋๋ฅผ ์ฐจ์งํด์ผ ํจ
โข Jest, XCTest, JUnit ๋ฑ ์ฌ์ฉ
์์ (React Native + Jest):
describe('Calculator', () => {
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
test('handles negative numbers', () => {
expect(add(-1, -1)).toBe(-2);
});
});2. ํตํฉ ํ ์คํธ (Integration Tests) - ์ ๋นํ ๐ง
โข ์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ํจ๊ป ์๋ํ๋์ง ํ ์คํธ
โข API ํธ์ถ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ๋ฑ
โข ์ ์ฒด ํ ์คํธ์ 20% ์ ๋
โข React Native Testing Library, Espresso ๋ฑ ์ฌ์ฉ
์์ (React Native Testing Library):
test('user can login successfully', async () => {
const { getByPlaceholderText, getByText } = render(<LoginScreen />);
fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com');
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
fireEvent.press(getByText('Login'));
await waitFor(() => {
expect(getByText('Welcome!')).toBeTruthy();
});
});3. E2E ํ ์คํธ (End-to-End Tests) - ์์ ์ ์ ๐ฌ
โข ์ค์ ์ฌ์ฉ์ ์๋๋ฆฌ์ค๋ฅผ ์๋ฎฌ๋ ์ด์
โข ์ค์ ๋๋ฐ์ด์ค๋ ์๋ฎฌ๋ ์ดํฐ์์ ์คํ
โข ์ ์ฒด ํ ์คํธ์ 10% ์ ๋
โข Detox, Appium, XCUITest ๋ฑ ์ฌ์ฉ
์์ (Detox):
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should login successfully', async () => {
await element(by.id('emailInput')).typeText('test@example.com');
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
await expect(element(by.text('Home Screen'))).toBeVisible();
});
});
๐ ์ฝ๋ ํ์ง ๋๊ตฌ๋ค
1. ์ ์ ๋ถ์ ๋๊ตฌGitHub Actions์ ํตํฉํ๊ธฐ:
- name: ๋ฆฐํธ ์ฒดํฌ
run: |
npm run lint
npm run prettier:check
npm run type-check
- name: iOS ๋ฆฐํธ
run: |
cd ios
swiftlint lint --strict
- name: Android ๋ฆฐํธ
run: |
cd android
./gradlew ktlintCheck2. ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง
ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ธก์ ํ๊ณ ๋ฆฌํฌํธ๋ฅผ ์์ฑํด์:
- name: ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง
run: npm test -- --coverage
- name: Codecov ์
๋ก๋
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: trueโข 80% ์ด์: ํ๋ฅญํจ! ๋๋ถ๋ถ์ ์ฝ๋๊ฐ ํ ์คํธ๋จ โ
โข 60-80%: ์ํธํจ. ํต์ฌ ๋ก์ง์ ์ปค๋ฒ๋จ ๐
โข 60% ๋ฏธ๋ง: ๊ฐ์ ํ์. ๋ ๋ง์ ํ ์คํธ ์์ฑ ํ์ โ ๏ธ
ํ์ง๋ง ์ซ์์ ์ง์ฐฉํ์ง ๋ง์ธ์! ์ค์ํ ๊ฑด ์๋ฏธ ์๋ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฑฐ์์. ์ปค๋ฒ๋ฆฌ์ง 100%๋ผ๋ ๋ฒ๊ทธ๊ฐ ์์ ์ ์๊ณ , 70%์ฌ๋ ํต์ฌ ๊ธฐ๋ฅ์ด ์ ํ ์คํธ๋๋ฉด ์ถฉ๋ถํฉ๋๋ค! ๐
์ฑ ์ฑ๋ฅ์ ์๋์ผ๋ก ์ธก์ ํ๊ณ ๋ฆฌํฌํธํ๋ ๋๊ตฌ๋ค:
โข Lighthouse CI: ์น ์ฑ๋ฅ ์ธก์
โข Firebase Performance Monitoring: ์ฑ ์ฑ๋ฅ ์ถ์
โข Sentry: ์๋ฌ ์ถ์ ๋ฐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
โข New Relic: ์ข ํฉ APM ์๋ฃจ์
- name: Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
- name: ์ฑ๋ฅ ๋ฆฌํฌํธ ์
๋ก๋
uses: actions/upload-artifact@v3
with:
name: lighthouse-report
path: .lighthouseci/
๐จ ๋ฐฐํฌ ์ ๋ต๊ณผ ๋กค๋ฐฑ ๊ด๋ฆฌ
๋ฐฐํฌ๋ ๋จ์ํ "์ ๋ก๋"๋ง ํ๋ ๊ฒ ์๋์์. ์์ ํ๊ฒ ๋ฐฐํฌํ๊ณ , ๋ฌธ์ ๋ฐ์ ์ ๋น ๋ฅด๊ฒ ๋์ํ๋ ๊ฒ ์ค์ํ์ฃ ! ๐ก๏ธ๐ฏ ๋ฐฐํฌ ์ ๋ต๋ค
๋ ๊ฐ์ ๋์ผํ ํ๋ก๋์ ํ๊ฒฝ์ ์ ์งํ๊ณ , ํธ๋ํฝ์ ํ ๋ฒ์ ์ ํํ๋ ๋ฐฉ์์ด์์.
์ฅ์ :
โ ์ฆ์ ๋กค๋ฐฑ ๊ฐ๋ฅ (ํธ๋ํฝ๋ง ๋ค์ ์ ํ)
โ ๋ค์ดํ์ ์ ๋ก
โ ํ๋ก๋์ ํ๊ฒฝ์์ ์ต์ข ํ ์คํธ ๊ฐ๋ฅ
๋จ์ :
โ ๋ ๋ฐฐ์ ์ธํ๋ผ ๋น์ฉ
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ์ด ๋ณต์กํ ์ ์์
์ ํฉํ ๊ฒฝ์ฐ: ๋๊ท๋ชจ ์๋น์ค, ๋ค์ดํ์์ด ์ ๋ ์ ๋๋ ๊ฒฝ์ฐ
์ ๋ฒ์ ์ ์์์ ์ฌ์ฉ์์๊ฒ๋ง ๋จผ์ ๋ฐฐํฌํ๊ณ , ๋ฌธ์ ๊ฐ ์์ผ๋ฉด ์ ์ง์ ์ผ๋ก ํ๋ํ๋ ๋ฐฉ์์ด์์.
์ฅ์ :
โ ๋ฆฌ์คํฌ ์ต์ํ
โ ์ค์ ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ๋น ๋ฅด๊ฒ ๋ฐ์ ์ ์์
โ ๋ฌธ์ ๋ฐ์ ์ ์ํฅ ๋ฒ์๊ฐ ์์
๋จ์ :
โ ๋ฐฐํฌ ์๊ฐ์ด ๊ธธ์ด์ง
โ ๋ชจ๋ํฐ๋ง์ด ๋ณต์กํจ
์ ํฉํ ๊ฒฝ์ฐ: ๋๊ท๋ชจ ์ฌ์ฉ์ ๊ธฐ๋ฐ, ์ ์คํ ๋ฐฐํฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ
Google Play์์ Canary ๋ฐฐํฌ ์ค์ :
lane :canary do
upload_to_play_store(
track: 'production',
rollout: '0.05' # 5%์๊ฒ๋ง ๋ฐฐํฌ
)
end
# ๋ฌธ์ ์์ผ๋ฉด ์ ์ง์ ์ผ๋ก ํ๋
lane :expand_rollout do
upload_to_play_store(
track: 'production',
rollout: '0.25', # 25%๋ก ํ๋
skip_upload_apk: true
)
end
์ฝ๋๋ ๋ฐฐํฌํ๋, ํน์ ๊ธฐ๋ฅ์ ์ผ๊ณ ๋ ์ ์๊ฒ ํ๋ ๋ฐฉ์์ด์์.
์ฅ์ :
โ ๋ฐฐํฌ์ ์ถ์๋ฅผ ๋ถ๋ฆฌํ ์ ์์
โ A/B ํ ์คํธ ๊ฐ๋ฅ
โ ํน์ ์ฌ์ฉ์ ๊ทธ๋ฃน์๊ฒ๋ง ๊ธฐ๋ฅ ์ ๊ณต ๊ฐ๋ฅ
โ ๊ธด๊ธ ์ํฉ ์ ์ฆ์ ๊ธฐ๋ฅ ๋นํ์ฑํ
๋จ์ :
โ ์ฝ๋ ๋ณต์ก๋ ์ฆ๊ฐ
โ ํ๋๊ทธ ๊ด๋ฆฌ๊ฐ ํ์ํจ
์ ํฉํ ๊ฒฝ์ฐ: ์ง์์ ๋ฐฐํฌ, A/B ํ ์คํธ, ์ ์ง์ ๊ธฐ๋ฅ ์ถ์
Feature Flag ๊ตฌํ ์์:
// LaunchDarkly, Firebase Remote Config ๋ฑ ์ฌ์ฉ
import { useFeatureFlag } from '@launchdarkly/react-native-client-sdk';
function MyComponent() {
const showNewFeature = useFeatureFlag('new-feature-enabled', false);
return (
<View>
{showNewFeature ? (
<NewFeature />
) : (
<OldFeature />
)}
</View>
);
}
โฎ๏ธ ๋กค๋ฐฑ ์ ๋ต
๋ฌธ์ ๊ฐ ๋ฐ์ํ์ ๋ ๋น ๋ฅด๊ฒ ์ด์ ๋ฒ์ ์ผ๋ก ๋์๊ฐ๋ ๋ฐฉ๋ฒ๋ค์ด์์:1. Git ํ๊ทธ ๊ธฐ๋ฐ ๋กค๋ฐฑ
# ํน์ ๋ฒ์ ์ผ๋ก ๋กค๋ฐฑ
git checkout v1.2.3
git push origin v1.2.3
# GitHub Actions์์ ์๋ ๋ฐฐํฌ
# ์ํฌํ๋ก์ฐ๊ฐ ํด๋น ํ๊ทธ๋ฅผ ๊ฐ์งํ๊ณ ์ฌ๋ฐฐํฌ2. Fastlane์ ํตํ ๋กค๋ฐฑ
lane :rollback do |options|
version = options[:version]
# App Store Connect์์ ์ด์ ๋ฒ์ ํ์ฑํ
deliver(
app_version: version,
submit_for_review: true,
automatic_release: true,
force: true
)
slack(
message: "์ฑ์ด ๋ฒ์ #{version}์ผ๋ก ๋กค๋ฐฑ๋์์ต๋๋ค! โ ๏ธ"
)
end
# ์ฌ์ฉ๋ฒ: fastlane rollback version:1.2.33. Google Play ๋จ๊ณ์ ๋กค์์ ์ค๋จ
lane :halt_rollout do
upload_to_play_store(
track: 'production',
rollout: '0.0', # ๋กค์์ ์ค๋จ
skip_upload_apk: true
)
# ์ด์ ๋ฒ์ ์ผ๋ก ๋กค๋ฐฑ
upload_to_play_store(
track: 'production',
version_code: previous_version_code,
skip_upload_apk: true
)
end
1๏ธโฃ ๋ชจ๋ํฐ๋ง ์๋ฆผ ํ์ธ: ์๋ฌ์จ, ํฌ๋์์จ ๊ธ์ฆ ๊ฐ์ง
2๏ธโฃ ์ํฅ ๋ฒ์ ํ์ : ๋ช %์ ์ฌ์ฉ์๊ฐ ์ํฅ์ ๋ฐ๋์ง ํ์ธ
3๏ธโฃ ์ฌ๊ฐ๋ ํ๋จ: ์ฆ์ ๋กค๋ฐฑ์ด ํ์ํ์ง, ํซํฝ์ค๋ก ํด๊ฒฐ ๊ฐ๋ฅํ์ง
4๏ธโฃ ๋กค๋ฐฑ ์คํ: ํ์ํ๋ค๋ฉด ์ฃผ์ ํ์ง ๋ง๊ณ ๋กค๋ฐฑ!
5๏ธโฃ ์์ธ ๋ถ์: ๋ก๊ทธ ๋ถ์, ์ฌํ ์๋
6๏ธโฃ ํซํฝ์ค ๋ฐฐํฌ: ๋ฌธ์ ํด๊ฒฐ ํ ๊ธด๊ธ ํจ์น
7๏ธโฃ ํฌ์คํธ๋ชจํ : ์ฌ๋ฐ ๋ฐฉ์ง๋ฅผ ์ํ ํ๊ณ
๊ธฐ์ตํ์ธ์: ๋กค๋ฐฑ์ ์คํจ๊ฐ ์๋๋ผ ํ๋ช ํ ์ ํ์ ๋๋ค! ๐ฏ
๐ ๋ชจ๋ํฐ๋ง๊ณผ ์๋ฆผ ์์คํ
๋ฐฐํฌ ํ์๋ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง์ด ํ์์์! ๋ฌธ์ ๋ฅผ ๋น ๋ฅด๊ฒ ๋ฐ๊ฒฌํ๊ณ ๋์ํด์ผ ํ์ฃ . ๐๐ ์๋ฆผ ์ฑ๋ ์ค์
1. Slack ํตํฉSlack์ ํ ์ปค๋ฎค๋์ผ์ด์ ์ ์ค์ฌ์ด์ฃ ! CI/CD ์๋ฆผ์ Slack์ผ๋ก ๋ฐ์๋ณด์ธ์.
- name: Slack ์๋ฆผ - ์ฑ๊ณต
if: success()
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
"text": "โ
๋ฐฐํฌ ์ฑ๊ณต!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*๋ฐฐํฌ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค!* ๐\n\n*๋ฒ์ :* ${{ github.ref }}\n*์ปค๋ฐ:* ${{ github.sha }}\n*์์ฑ์:* ${{ github.actor }}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "GitHub์์ ๋ณด๊ธฐ"
},
"url": "${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: Slack ์๋ฆผ - ์คํจ
if: failure()
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
"text": "โ ๋ฐฐํฌ ์คํจ!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*๋ฐฐํฌ๊ฐ ์คํจํ์ต๋๋ค!* ๐ฑ\n\n*๋ธ๋์น:* ${{ github.ref }}\n*์ปค๋ฐ:* ${{ github.sha }}\n*์์ฑ์:* ${{ github.actor }}\n\n์ฆ์ ํ์ธ์ด ํ์ํฉ๋๋ค!"
}
}
]
}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}2. ์ด๋ฉ์ผ ์๋ฆผ
- name: ์ด๋ฉ์ผ ์๋ฆผ
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: '๐จ ๋ฐฐํฌ ์คํจ: ${{ github.repository }}'
body: |
๋ฐฐํฌ๊ฐ ์คํจํ์ต๋๋ค.
๋ ํฌ์งํ ๋ฆฌ: ${{ github.repository }}
๋ธ๋์น: ${{ github.ref }}
์ปค๋ฐ: ${{ github.sha }}
์์ฑ์: ${{ github.actor }}
์์ธํ ๋ด์ฉ: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}
to: dev-team@company.com
from: CI/CD Bot3. Discord ํตํฉ
- name: Discord ์๋ฆผ
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: "๋ฐฐํฌ ์ํ"
description: |
๋ฒ์ : ${{ github.ref }}
์ปค๋ฐ: ${{ github.sha }}
์์ฑ์: ${{ github.actor }}
color: 0x00ff00 # ์ฑ๊ณต ์ ์ด๋ก์
username: CI/CD Bot
๐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
1. Firebase Crashlytics์ค์๊ฐ ํฌ๋์ ๋ฆฌํฌํ ๊ณผ ๋ถ์:
// React Native
import crashlytics from '@react-native-firebase/crashlytics';
// ์ฌ์ฉ์ ์ ๋ณด ์ค์
crashlytics().setUserId(userId);
crashlytics().setAttribute('role', 'premium');
// ์ปค์คํ
๋ก๊ทธ
crashlytics().log('User clicked checkout button');
// ์๋ฌ ๊ธฐ๋ก
try {
await processPayment();
} catch (error) {
crashlytics().recordError(error);
}2. Sentry
์๋ฌ ์ถ์ ๊ณผ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง:
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'YOUR_DSN',
environment: __DEV__ ? 'development' : 'production',
tracesSampleRate: 1.0,
enableAutoSessionTracking: true,
sessionTrackingIntervalMillis: 10000,
});
// ์ฑ๋ฅ ์ถ์
const transaction = Sentry.startTransaction({
name: 'User Checkout',
op: 'checkout',
});
try {
await processCheckout();
transaction.setStatus('ok');
} catch (error) {
transaction.setStatus('internal_error');
Sentry.captureException(error);
} finally {
transaction.finish();
}3. ์ปค์คํ ๋ฉํธ๋ฆญ ์์ง
// ์ฑ ์์ ์๊ฐ ์ธก์
const startTime = Date.now();
AppRegistry.registerComponent(appName, () => {
const loadTime = Date.now() - startTime;
// Analytics์ ๊ธฐ๋ก
analytics().logEvent('app_load_time', {
duration: loadTime,
platform: Platform.OS,
version: DeviceInfo.getVersion(),
});
return App;
});
1๏ธโฃ ์๋ฆผ ํผ๋ก๋ ๊ด๋ฆฌ: ๋๋ฌด ๋ง์ ์๋ฆผ์ ์คํ๋ ค ์ญํจ๊ณผ! ์ค์ํ ๊ฒ๋ง ์๋ฆผ ์ค์
2๏ธโฃ ์๊ณ๊ฐ ์ค์ : ์๋ฌ์จ 5% ์ด์, ํฌ๋์์จ 1% ์ด์ ๋ฑ ๋ช ํํ ๊ธฐ์ค
3๏ธโฃ ๋์๋ณด๋ ๊ตฌ์ถ: Grafana, DataDog ๋ฑ์ผ๋ก ์๊ฐํ
4๏ธโฃ ์ ๊ธฐ ๋ฆฌ๋ทฐ: ์ฃผ๊ฐ ๋ฉํธ๋ฆญ ๋ฆฌ๋ทฐ๋ก ํธ๋ ๋ ํ์
5๏ธโฃ ์ฌ์ฉ์ ํผ๋๋ฐฑ ํตํฉ: ์ฑ ๋ด ํผ๋๋ฐฑ ๊ธฐ๋ฅ์ผ๋ก ์ ์ฑ์ ๋ฐ์ดํฐ ์์ง
๐ฐ ๋น์ฉ ์ต์ ํ ์ ๋ต
CI/CD๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ๋น์ฉ์ด ๋ง๋ง์น ์์ ์ ์์ด์. ํนํ ๋น๋ ์๊ฐ์ด ๊ธธ๊ฑฐ๋ ๋น๋ฒํ ๋ฐฐํฌ๋ฅผ ํ๋ ๊ฒฝ์ฐ์๋ ๋์ฑ ๊ทธ๋ ์ฃ ! ๐ธโก ๋น๋ ์๊ฐ ๋จ์ถํ๊ธฐ
์์กด์ฑ์ ๋งค๋ฒ ๋ค์ด๋ก๋ํ๋ฉด ์๊ฐ๊ณผ ๋น์ฉ์ด ๋ญ๋น๋ผ์. ์บ์ฑ์ ์ ๊ทน ํ์ฉํ์ธ์!
- name: ์์กด์ฑ ์บ์ฑ
uses: actions/cache@v3
with:
path: |
~/.npm
~/.gradle/caches
~/.gradle/wrapper
~/Library/Caches/CocoaPods
Pods
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/*.gradle*', '**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-deps-์บ์ฑ ํจ๊ณผ:
โข Node modules: 5๋ถ โ 30์ด โก
โข Gradle ์์กด์ฑ: 3๋ถ โ 20์ด โก
โข CocoaPods: 4๋ถ โ 40์ด โก
2. ๋ณ๋ ฌ ์ฒ๋ฆฌ
์ฌ๋ฌ ์์ ์ ๋์์ ์คํํ๋ฉด ์ ์ฒด ์๊ฐ์ด ํฌ๊ฒ ์ค์ด๋ค์ด์:
jobs:
test:
strategy:
matrix:
platform: [ios, android]
node-version: [16, 18]
runs-on: ${{ matrix.platform == 'ios' && 'macos-latest' || 'ubuntu-latest' }}
steps:
- name: ํ
์คํธ ์คํ
run: npm test3. ์กฐ๊ฑด๋ถ ์คํ
๋ณ๊ฒฝ๋ ํ์ผ์ ๋ฐ๋ผ ํ์ํ ์์ ๋ง ์คํํ์ธ์:
- name: iOS ๋ณ๊ฒฝ์ฌํญ ํ์ธ
id: ios-changes
uses: dorny/paths-filter@v2
with:
filters: |
ios:
- 'ios/**'
- '*.podspec'
- name: iOS ๋น๋
if: steps.ios-changes.outputs.ios == 'true'
run: cd ios && fastlane beta
๐ต ํ๋ซํผ๋ณ ๋น์ฉ ๋น๊ต
| ํ๋ซํผ | ๋ฌด๋ฃ ํฐ์ด | ์ ๋ฃ ํ๋ | ํน์ง |
|---|---|---|---|
| GitHub Actions | ํผ๋ธ๋ฆญ: ๋ฌด์ ํ ํ๋ผ์ด๋น: 2,000๋ถ/์ |
$0.008/๋ถ (Linux) $0.016/๋ถ (Windows) $0.08/๋ถ (macOS) |
macOS๊ฐ ๋น์ ์บ์ฑ ํจ๊ณผ ์ข์ |
| GitLab CI/CD | 400๋ถ/์ (๋ฌด๋ฃ ํฐ์ด) | $19/์ (10,000๋ถ) | ์
ํ ํธ์คํ
๊ฐ๋ฅ ๋น์ฉ ํจ์จ์ |
| CircleCI | 6,000๋ถ/์ (Linux) | $15/์ (์ถ๊ฐ 25,000ํฌ๋ ๋ง) | ๋น๋ ์๋ ๋น ๋ฆ ํฌ๋ ๋ง ์์คํ |
| Bitrise | 200๋น๋/์ (Hobby) | $90/์ (Developer) $270/์ (Team) |
๋ชจ๋ฐ์ผ ํนํ ๋น๊ต์ ๋น์ |
| Jenkins | ๋ฌด๋ฃ (์คํ์์ค) | ์๋ฒ ๋น์ฉ๋ง | ์ด๊ธฐ ์ค์ ๋ณต์ก ์ฅ๊ธฐ์ ์ผ๋ก ์ ๋ ด |
1๏ธโฃ macOS ๋น๋ ์ต์ํ: iOS ๋น๋๋ macOS์์๋ง ๊ฐ๋ฅํ์ง๋ง ๋น์ฉ์ด 10๋ฐฐ! ํ ์คํธ๋ Linux์์ ํ๊ณ , ์ต์ข ๋น๋๋ง macOS์์ ํ์ธ์.
2๏ธโฃ ์ ํ ํธ์คํ ๊ณ ๋ ค: ๋น๋๊ฐ ๋ง๋ค๋ฉด ์์ฒด ์๋ฒ๊ฐ ๋ ์ ๋ ดํ ์ ์์ด์. AWS EC2, DigitalOcean ๋ฑ ํ์ฉ
3๏ธโฃ ๋น๋ ๋น๋ ์กฐ์ : ๋ชจ๋ ์ปค๋ฐ๋ง๋ค ๋น๋ํ ํ์๋ ์์ด์. PR๊ณผ main ๋ธ๋์น๋ง ๋น๋ํ๋ ๊ฒ๋ ๋ฐฉ๋ฒ
4๏ธโฃ ํ์์์ ์ค์ : ๋ฌดํ ๋ฃจํ๋ ๋ฉ์ถ ๋น๋๋ก ๋น์ฉ ๋ญ๋น ๋ฐฉ์ง
5๏ธโฃ ์คํ ์ธ์คํด์ค ํ์ฉ: AWS CodeBuild ๋ฑ์์ ์คํ ์ธ์คํด์ค ์ฌ์ฉ ์ 70% ์ ๊ฐ ๊ฐ๋ฅ!
๐ ์ค์ ์ฌ๋ก ์ฐ๊ตฌ
์ด๋ก ๋ง์ผ๋ก๋ ๋ถ์กฑํ์ฃ ! ์ค์ ํ๋ก์ ํธ์์ CI/CD๋ฅผ ์ด๋ป๊ฒ ๊ตฌ์ถํ๋์ง ์ดํด๋ณผ๊น์? ๐๐ข ์ฌ๋ก 1: ์คํํธ์ ์ React Native ์ฑ
โข ํ ๊ท๋ชจ: ๊ฐ๋ฐ์ 5๋ช
โข ์ฑ: React Native (iOS + Android)
โข ๋ฐฐํฌ ๋น๋: ์ฃผ 2-3ํ
โข ์์ฐ: ์ $100 ์ดํ
์ ํํ ์คํ:
โข GitHub Actions (๋ฌด๋ฃ ํฐ์ด)
โข Fastlane
โข Firebase App Distribution (๋ฒ ํ ํ ์คํธ)
โข Sentry (์๋ฌ ์ถ์ )
์ํฌํ๋ก์ฐ:
1๏ธโฃ develop ๋ธ๋์น: PR ์์ฑ ์ ์๋ ํ ์คํธ
2๏ธโฃ develop ๋จธ์ง: Firebase App Distribution์ ๋ฒ ํ ๋ฐฐํฌ
3๏ธโฃ main ๋ธ๋์น: ์ฃผ๊ฐ ๋ฆด๋ฆฌ์ฆ, TestFlight/Internal Testing ๋ฐฐํฌ
4๏ธโฃ Git ํ๊ทธ: ํ๋ก๋์ ๋ฐฐํฌ (์๋ ์น์ธ ํ)
๊ฒฐ๊ณผ:
โ ๋ฐฐํฌ ์๊ฐ: 4์๊ฐ โ 20๋ถ (92% ๊ฐ์)
โ ๋ฐฐํฌ ์คํจ์จ: 30% โ 5% (ํ์ง ํฅ์)
โ ๊ฐ๋ฐ์ ๋ง์กฑ๋: ํฌ๊ฒ ํฅ์
โ ์ ๋น์ฉ: $0 (๋ฌด๋ฃ ํฐ์ด ๋ด)
๊ตํ:
"์ฒ์๋ถํฐ ์๋ฒฝํ ํ์๋ ์์ด์. ์๊ฒ ์์ํด์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํ๋ ๊ฒ ์ค์ํฉ๋๋ค!" ๐ช
๐ญ ์ฌ๋ก 2: ์ค๊ฒฌ ๊ธฐ์ ์ ๋ค์ดํฐ๋ธ ์ฑ
โข ํ ๊ท๋ชจ: iOS ํ 8๋ช , Android ํ 6๋ช
โข ์ฑ: ๋ค์ดํฐ๋ธ (Swift + Kotlin)
โข ๋ฐฐํฌ ๋น๋: ๊ฒฉ์ฃผ ๋ฆด๋ฆฌ์ฆ
โข ์์ฐ: ์ $500
์ ํํ ์คํ:
โข GitLab CI/CD (์ ํ ํธ์คํ )
โข Fastlane
โข TestFlight + Google Play Beta
โข Firebase Crashlytics
โข Slack ํตํฉ
์ํฌํ๋ก์ฐ:
1๏ธโฃ Feature ๋ธ๋์น: ์๋ ํ ์คํธ + ์ฝ๋ ๋ฆฌ๋ทฐ
2๏ธโฃ develop ๋ธ๋์น: ๋์ดํ๋ฆฌ ๋น๋, ๋ด๋ถ QA ํ ๋ฐฐํฌ
3๏ธโฃ release ๋ธ๋์น: ๋ฒ ํ ํ ์คํฐ ๋ฐฐํฌ (1,000๋ช )
4๏ธโฃ main ๋ธ๋์น: ํ๋ก๋์ ๋ฐฐํฌ (๋จ๊ณ์ ๋กค์์)
๊ณ ๊ธ ๊ธฐ๋ฅ:
โข ์๋ ์คํฌ๋ฆฐ์ท ์์ฑ (8๊ฐ ์ธ์ด)
โข ์๋ ๋ฆด๋ฆฌ์ฆ ๋ ธํธ ์์ฑ
โข ์ฑ๋ฅ ๋ฒค์น๋งํฌ ์๋ ์คํ
โข A/B ํ ์คํธ ํตํฉ
๊ฒฐ๊ณผ:
โ QA ์๊ฐ: 3์ผ โ 1์ผ (์๋ํ๋ ํ ์คํธ)
โ ํฌ๋์์จ: 2.5% โ 0.3% (์กฐ๊ธฐ ๋ฐ๊ฒฌ)
โ ๋ฐฐํฌ ์ ๋ขฐ๋: ํฌ๊ฒ ํฅ์
โ ์ ๋น์ฉ: $350 (์๋ฒ + ๋๊ตฌ)
๊ตํ:
"์ ํ ํธ์คํ ์ ์ด๊ธฐ ์ค์ ์ด ๋ณต์กํ์ง๋ง, ์ฅ๊ธฐ์ ์ผ๋ก๋ ๋น์ฉ ํจ์จ์ ์ด๊ณ ์ปค์คํฐ๋ง์ด์ง์ด ์์ ๋ก์์!" ๐ฏ
๐ ์ฌ๋ก 3: ๋๊ธฐ์ ์ Flutter ์ฑ
โข ํ ๊ท๋ชจ: ๊ฐ๋ฐ์ 20๋ช +
โข ์ฑ: Flutter (ํฌ๋ก์ค ํ๋ซํผ)
โข ๋ฐฐํฌ ๋น๋: ๋งค์ผ (Continuous Deployment)
โข ์์ฐ: ์ ํ ์์
์ ํํ ์คํ:
โข Jenkins (์์ฒด ์๋ฒ ํ)
โข Fastlane
โข Firebase (์ ์ฒด ์ค์ํธ)
โข Datadog (๋ชจ๋ํฐ๋ง)
โข PagerDuty (์จ์ฝ ์์คํ )
์ํฌํ๋ก์ฐ:
1๏ธโฃ ๋ชจ๋ ์ปค๋ฐ: ์๋ ํ ์คํธ + ์ ์ ๋ถ์
2๏ธโฃ main ๋ธ๋์น: ์๋์ผ๋ก Internal Testing ๋ฐฐํฌ
3๏ธโฃ Feature Flags: ํ๋ก๋์ ์ ๋ฐฐํฌํ๋ ๊ธฐ๋ฅ์ ๋นํ์ฑํ
4๏ธโฃ Canary ๋ฐฐํฌ: 1% โ 10% โ 50% โ 100% ๋จ๊ณ์ ํ๋
5๏ธโฃ ์๋ ๋กค๋ฐฑ: ์๋ฌ์จ ์๊ณ๊ฐ ์ด๊ณผ ์ ์๋ ๋กค๋ฐฑ
๊ณ ๊ธ ์ธํ๋ผ:
โข ์ ์ฉ ๋น๋ ์๋ฒ ํด๋ฌ์คํฐ (10๋+)
โข ์๋ ์ค์ผ์ผ๋ง
โข ๋ฉํฐ ๋ฆฌ์ ๋ฐฐํฌ
โข ์ค์๊ฐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
โข AI ๊ธฐ๋ฐ ์ด์ ํ์ง
๊ฒฐ๊ณผ:
โ ๋ฐฐํฌ ๋น๋: ์ฃผ 1ํ โ ํ๋ฃจ 5ํ
โ ํ๊ท ๋ณต๊ตฌ ์๊ฐ: 2์๊ฐ โ 10๋ถ
โ ๊ฐ๋ฐ ์๋: 2๋ฐฐ ํฅ์
โ ์ฌ์ฉ์ ๋ง์กฑ๋: 15% ํฅ์
๊ตํ:
"์์ ์๋ํ๋ ๊ฐ๋ ฅํ์ง๋ง, ์ฒ ์ ํ ํ ์คํธ์ ๋ชจ๋ํฐ๋ง์ด ์ ์ ๋์ด์ผ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ํ์ DevOps ๋ฌธํ๊ฐ ๊ฐ์ฅ ์ค์ํด์!" ๐
๐ฎ ๋ฏธ๋ ํธ๋ ๋์ ๋ฐ์ ๋ฐฉํฅ
CI/CD ๋ถ์ผ๋ ๊ณ์ ์งํํ๊ณ ์์ด์! ์์ผ๋ก ์ด๋ค ๋ณํ๊ฐ ์ฌ์ง ์ดํด๋ณผ๊น์? ๐๐ค AI/ML ํตํฉ
AI๊ฐ ์ฝ๋ ๋ณ๊ฒฝ์ฌํญ์ ๋ถ์ํด์ ์คํํ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์ ํํด์. ๋ชจ๋ ํ ์คํธ๋ฅผ ๋๋ฆด ํ์ ์์ด ๊ด๋ จ๋ ํ ์คํธ๋ง ์คํํ๋ ์๊ฐ์ด ํฌ๊ฒ ๋จ์ถ๋์ฃ ! โก
2. ์๋ ๋ฒ๊ทธ ์์ธก
๋จธ์ ๋ฌ๋ ๋ชจ๋ธ์ด ์ฝ๋๋ฅผ ๋ถ์ํด์ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋์ ๋ถ๋ถ์ ๋ฏธ๋ฆฌ ์๋ ค์ค์. "์ด ์ฝ๋๋ ํ๋ก๋์ ์์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ํ๋ฅ ์ด 85%์ ๋๋ค" ๊ฐ์ ๊ฒฝ๊ณ ๋ฅผ ๋ฐ์ ์ ์์ด์! ๐ฎ
3. ์ค๋งํธ ๋กค๋ฐฑ
AI๊ฐ ์ค์๊ฐ์ผ๋ก ๋ฉํธ๋ฆญ์ ๋ถ์ํด์ ๋ฌธ์ ๋ฅผ ๊ฐ์งํ๋ฉด ์๋์ผ๋ก ๋กค๋ฐฑํด์. ์ฌ๋์ ํ๋จ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ฒ ๋์ํ ์ ์์ฃ ! ๐ก๏ธ
4. ์ฝ๋ ๋ฆฌ๋ทฐ ์๋ํ
GitHub Copilot์ฒ๋ผ AI๊ฐ ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋์์ค์. ๋ณด์ ์ทจ์ฝ์ , ์ฑ๋ฅ ์ด์, ๋ฒ ์คํธ ํ๋ํฐ์ค ์๋ฐ ๋ฑ์ ์๋์ผ๋ก ์ง์ ํด์ค๋๋ค! ๐จโ๐ป
โ๏ธ ํด๋ผ์ฐ๋ ๋ค์ดํฐ๋ธ CI/CD
AWS Lambda, Google Cloud Functions ๊ฐ์ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ์์ ๋น๋๊ฐ ์คํ๋ผ์. ์ฌ์ฉํ ๋งํผ๋ง ๋น์ฉ์ ๋ด๋ ๊ฒฝ์ ์ ์ด์ฃ ! ๐ฐ
2. ์ปจํ ์ด๋ ๊ธฐ๋ฐ ๋น๋
Docker, Kubernetes๋ฅผ ํ์ฉํ ๋น๋ ํ๊ฒฝ์ด ํ์ค์ด ๋๊ณ ์์ด์. ํ๊ฒฝ ์ผ๊ด์ฑ์ด ๋ณด์ฅ๋๊ณ , ์ค์ผ์ผ๋ง์ด ์ฌ์์! ๐ณ
3. ์ฃ์ง ๋ฐฐํฌ
CDN ์ฃ์ง ๋ก์ผ์ด์ ์์ ์ง์ ์ฑ์ ๋ฐฐํฌํ๊ณ ์ ๋ฐ์ดํธํ๋ ๋ฐฉ์์ด ๋ฑ์ฅํ๊ณ ์์ด์. ์ฌ์ฉ์์ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ณณ์์ ์ฑ์ด ์คํ๋๋ ์๋๊ฐ ๋น ๋ฅด์ฃ ! ๐
๐ ๋ณด์ ๊ฐํ
๋ณด์์ด CI/CD ํ์ดํ๋ผ์ธ์ ์์ ํ ํตํฉ๋ผ์. ์ฝ๋ ์์ฑ ๋จ๊ณ๋ถํฐ ๋ฐฐํฌ๊น์ง ๋ชจ๋ ๊ณผ์ ์์ ์๋ ๋ณด์ ๊ฒ์ฌ๊ฐ ์ด๋ฃจ์ด์ ธ์! ๐ก๏ธ
2. ์ ๋ก ํธ๋ฌ์คํธ ์ํคํ ์ฒ
"์ ๋ขฐํ๋ ๊ฒ์ฆํ๋ผ"์์ "์ ๋ ์ ๋ขฐํ์ง ๋ง๊ณ ํญ์ ๊ฒ์ฆํ๋ผ"๋ก ํจ๋ฌ๋ค์์ด ๋ฐ๋๊ณ ์์ด์. ๋ชจ๋ ์ ๊ทผ์ ๋ํด ์ธ์ฆ๊ณผ ๊ถํ ๊ฒ์ฌ๊ฐ ํ์ํด์! ๐
3. ๊ณต๊ธ๋ง ๋ณด์
์์กด์ฑ ํจํค์ง์ ๋ณด์ ์ทจ์ฝ์ ์ ์๋์ผ๋ก ๊ฒ์ฌํ๊ณ , ์์ ํ ๋ฒ์ ์ผ๋ก ์ ๋ฐ์ดํธํด์ค์. npm, CocoaPods ๋ฑ์ ํจํค์ง ๋ณด์์ด ๋์ฑ ์ค์ํด์ง๊ณ ์์ด์! ๐ฆ
๐ฏ GitOps
์ฅ์ :
โ ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ด Git ํ์คํ ๋ฆฌ์ ๊ธฐ๋ก๋จ
โ ๋กค๋ฐฑ์ด ๊ฐ๋จํจ (Git revert๋ง ํ๋ฉด ๋จ)
โ ์ฝ๋ ๋ฆฌ๋ทฐ ํ๋ก์ธ์ค ์ ์ฉ ๊ฐ๋ฅ
โ ์ฌํ ๊ฐ๋ฅํ ๋ฐฐํฌ
๋ํ ๋๊ตฌ:
โข ArgoCD
โข Flux
โข Jenkins X
โข Tekton
1๏ธโฃ ์์ ์๋ํ: ์ฌ๋์ ๊ฐ์ ์์ด ์ฝ๋ ์์ฑ๋ถํฐ ๋ฐฐํฌ๊น์ง ์๋ํ
2๏ธโฃ AI ํ์ด ํ๋ก๊ทธ๋๋ฐ: AI๊ฐ ์ค์๊ฐ์ผ๋ก ์ฝ๋๋ฅผ ๊ฒํ ํ๊ณ ๊ฐ์ ์ ์
3๏ธโฃ ์์ ์ปดํจํ : ๋ณต์กํ ํ ์คํธ์ ์๋ฎฌ๋ ์ด์ ์ ์์๊ฐ์ ์ฒ๋ฆฌ
4๏ธโฃ ๋ธ๋ก์ฒด์ธ ํตํฉ: ๋ฐฐํฌ ์ด๋ ฅ์ ๋ธ๋ก์ฒด์ธ์ ๊ธฐ๋กํด ๋ณ์กฐ ๋ถ๊ฐ๋ฅํ๊ฒ
5๏ธโฃ ๋ฉํ๋ฒ์ค ๋ฐฐํฌ: VR/AR ์ฑ์ ๋ฐฐํฌ์ ์ ๋ฐ์ดํธ ์๋ํ
๋ฏธ๋๋ ์ ๋ง ํฅ๋ฏธ์ง์งํ์ฃ ? ํ์ง๋ง ๊ธฐ๋ณธ ์์น์ ๋ณํ์ง ์์์: ๋น ๋ฅด๊ณ , ์์ ํ๊ณ , ์ ๋ขฐํ ์ ์๋ ๋ฐฐํฌ! ๐
๐ ๋ณด๋์ค: ์ ์ฉํ ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
CI/CD๋ฅผ ๋ฐฐ์ฐ๊ณ ์ถ๊ฑฐ๋ ๋์์ด ํ์ํ ๋ ์ฐธ๊ณ ํ ์ ์๋ ์๋ฃ๋ค์ด์์! ๐๐ ํ์ต ์๋ฃ
โข Fastlane ๋ฌธ์: docs.fastlane.tools
โข GitLab CI/CD ๋ฌธ์: docs.gitlab.com/ee/ci
โข CircleCI ๋ฌธ์: circleci.com/docs
โข Coursera: "Continuous Integration and Continuous Delivery (CI/CD)"
โข YouTube: Fireship, Traversy Media, freeCodeCamp ์ฑ๋
โข "The DevOps Handbook" by Gene Kim
โข "Accelerate" by Nicole Forsgren
๐ฅ ์ปค๋ฎค๋ํฐ
โข Reddit: r/devops, r/androiddev, r/iOSProgramming
โข Dev.to: DevOps ํ๊ทธ
โข React Native Community
โข Flutter Korea
โข iOS Developers Korea
โข Android Developers Korea
โข React Native: github.com/facebook/react-native
โข Flutter: github.com/flutter/flutter
โข Fastlane: github.com/fastlane/fastlane
๐ ๏ธ ์ ์ฉํ ๋๊ตฌ๋ค
- GitHub Actions๋ฅผ ๋ก์ปฌ์์ ํ ์คํธ- Docker๋ก Actions ์คํ
- ์ฝ๋ ๋ฆฌ๋ทฐ ์๋ํ
- ์๋ ๋ฒ์ ๊ด๋ฆฌ
- ์ปค๋ฐ ๋ฉ์์ง ํ์คํ
- Git hooks ๊ด๋ฆฌ
- ์คํ ์ด์ง๋ ํ์ผ๋ง ๋ฆฐํธ
CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ถ์ด ์ด๋ ต๊ฒ ๋๊ปด์ง์๋์? ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ CI/CD ๊ตฌ์ถ ๊ฒฝํ์ด ํ๋ถํ ์ ๋ฌธ๊ฐ๋ค์ ๋ง๋๋ณด์ธ์! ๐ค
โข ํ๋ก์ ํธ์ ๋ง๋ CI/CD ์ ๋ต ์ปจ์คํ
โข GitHub Actions, Fastlane ์ค์ ๋ํ
โข ๊ธฐ์กด ํ์ดํ๋ผ์ธ ์ต์ ํ
โข ํ ๊ต์ก ๋ฐ ๋ฉํ ๋ง
ํผ์ ๊ณ ๋ฏผํ์ง ๋ง์๊ณ , ๊ฒฝํ ๋ง์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ณด์ธ์! ์๊ฐ๊ณผ ๋น์ฉ์ ํฌ๊ฒ ์ ์ฝํ ์ ์๋ต๋๋ค. ๐
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ค์๋๋ผ ์ ๋ง ์๊ณ ๋ง์ผ์ จ์ด์! ๐CI/CD๋ ์ฒ์์๋ ๋ณต์กํ๊ณ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด์. ํ์ง๋ง ํ ๋ฒ ์ ๋๋ก ๊ตฌ์ถํด๋๋ฉด, ๊ฐ๋ฐ ์์ฐ์ฑ์ด ์์ฒญ๋๊ฒ ํฅ์๋๊ณ , ๋ฐฐํฌ์ ๋ํ ์คํธ๋ ์ค๊ฐ ํ ์ค์ด๋ค์ด์! ๐
1๏ธโฃ CI/CD๋ ์ ํ์ด ์๋ ํ์: ํ๋ ์ฑ ๊ฐ๋ฐ์์ CI/CD ์์ด๋ ๊ฒฝ์๋ ฅ์ ์ ์งํ๊ธฐ ์ด๋ ค์์
2๏ธโฃ ์๊ฒ ์์ํ์ธ์: ์ฒ์๋ถํฐ ์๋ฒฝํ ํ์ ์์ด์. ์๋ ํ ์คํธ๋ถํฐ ์์ํด์ ์ ์ง์ ์ผ๋ก ํ์ฅํ์ธ์
3๏ธโฃ ๋๊ตฌ๋ณด๋ค ํ๋ก์ธ์ค: ์ด๋ค ๋๊ตฌ๋ฅผ ์ฐ๋๋๋ณด๋ค ํ์ ์ํฌํ๋ก์ฐ์ ๋ฌธํ๊ฐ ๋ ์ค์ํด์
4๏ธโฃ ๋ชจ๋ํฐ๋ง์ ํ์: ๋ฐฐํฌ ํ ๋ชจ๋ํฐ๋ง ์์ด๋ ๋ฌธ์ ๋ฅผ ๋น ๋ฅด๊ฒ ๋ฐ๊ฒฌํ ์ ์์ด์
5๏ธโฃ ๋ณด์์ ์์ง ๋ง์ธ์: ์๋ํ๋ ํธ๋ฆฌํ์ง๋ง, ๋ณด์ ์ค์ ์ ์ํํ ํ๋ฉด ํฐ ๋ฌธ์ ๊ฐ ๋ ์ ์์ด์
6๏ธโฃ ์ง์์ ์ธ ๊ฐ์ : CI/CD ํ์ดํ๋ผ์ธ๋ ๊ณ์ ์งํํด์ผ ํด์. ์ ๊ธฐ์ ์ผ๋ก ๋ฆฌ๋ทฐํ๊ณ ๊ฐ์ ํ์ธ์
7๏ธโฃ ํ๊ณผ ํจ๊ป: ํผ์์ ๋ชจ๋ ๊ฑธ ๋ค ํ ํ์ ์์ด์. ํ์๋ค๊ณผ ํ๋ ฅํ๊ณ , ํ์ํ๋ฉด ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์ผ์ธ์!
CI/CD ๊ตฌ์ถ์ ๋ง๋ผํค์ด์ง ๋จ๊ฑฐ๋ฆฌ ๋ฌ๋ฆฌ๊ธฐ๊ฐ ์๋์์. ์ฒ์์๋ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ณ ์ํ์ฐฉ์ค๋ ๋ง์ ๊ฑฐ์์. ํ์ง๋ง ํฌ๊ธฐํ์ง ๋ง์ธ์! ๐ช
ํ ๋ฒ ์ ๋๋ก ๊ตฌ์ถํด๋๋ฉด, ์์ผ๋ก ์๋ ๊ฐ ์ฌ๋ฌ๋ถ์ ๊ฐ๋ฐ ์ํ์ ํจ์ฌ ํธํ๊ฒ ๋ง๋ค์ด์ค ๊ฑฐ์์. ๊ทธ๋ฆฌ๊ณ ๊ทธ ๊ณผ์ ์์ ๋ฐฐ์ฐ๋ DevOps ์ง์์ ์ฌ๋ฌ๋ถ์ ์ปค๋ฆฌ์ด์ ํฐ ์์ฐ์ด ๋ ๊ฑฐ์์! ๐
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์. ๋๋ก๋ ๊ฒฝํ ๋ง์ ์ฌ๋์ ํ ๋ง๋ ์กฐ์ธ์ด ๋ฉฐ์น ๊ฐ์ ์ฝ์ง์ ์ค์ฌ์ค ์ ์๊ฑฐ๋ ์! ๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.