수동 배포에서 CI/CD로: 코드 품질을 유지할 수 있는 개발 흐름 만들기

2026년 10월 11일

개발CICDDevOps

수동 배포에서 CI/CD로: 코드 품질을 유지할 수 있는 개발 흐름 만들기

회사 입사 후 기능을 개발하고 운영하면서 코드 변경뿐 아니라 기존 개발 워크플로우를 개선하는 데도 관심이 커졌습니다.

출시를 위한 기능이 어느 정도 만들어진 시점에 합류했고, 이후 성능과 개발 환경에서 여러 이슈를 찾아 팀원들의 업무를 수월하게 만들고자 했습니다.

특히 서비스 초기에 여러 제약이나 인력 문제로 미뤄졌던 작업들을 살펴봤습니다. 그중 크게 개선이 필요하다고 느낀 부분은 수동 배포였습니다.

빠른 개발과 초기 프로덕션 출시를 위해 로컬에서 빌드한 결과를 서버로 전송하고, 의존성을 설치한 뒤 프로세스를 다시 실행하는 배포 스크립트가 있었습니다. 하지만 사람이 실행해야 했고, 배포 준비와 확인에 별도의 시간이 필요했습니다.

무엇보다 배포 전 검증이 일관된 흐름으로 연결돼 있지 않았습니다.

처음에는 GitHub Actions 같은 도구로 기존 명령을 실행하면 해결될 일이라고 생각하기 쉽습니다. 하지만 실제로 살펴보면서 느낀 것은 배포 전 코드 품질과 테스트의 검증 기준부터 정리해야 한다는 점이었습니다. 기존 문제가 누적된 상태에서는 새로운 변경으로 생긴 문제도 구분하기 어려웠습니다.

따라서 단순히 수동 배포를 자동 배포로 전환하는 것보다 코드 품질을 검증하고 어떤 변경을 배포해도 되는지 판단할 기준을 만드는 작업이 먼저 필요하다고 판단했습니다.

이 글에서는 기존 코드의 lint·타입 문제를 정리하고, 테스트와 빌드를 CI에 연결한 뒤 자동 배포로 전환한 과정을 정리합니다.

1. 배포 스크립트는 있었지만 검증 흐름은 분리돼 있었습니다

기존 배포 스크립트의 흐름은 다음과 같았습니다.

로컬 빌드
→ 코드와 빌드 결과물 전송
→ 서버 의존성 설치 및 DB 스키마 동기화
→ PM2 reload
→ 프로세스 상태 및 HTTP 응답 확인

기존 수동 배포 흐름

이 과정은 배포 명령을 일일이 입력하는 일을 줄여줬습니다. 다만 실행을 사람이 시작해야 했고, 빌드도 개발자의 로컬 환경에서 수행됐습니다. lint, 타입 검사, 테스트, 빌드가 각각 존재하더라도 모든 변경에 같은 기준으로 실행되는 것은 아니었습니다.

따라서 자동 배포를 도입하려면 검증의 시작 조건과 통과 기준부터 정해야 했습니다.

2. 자동 배포보다 검증 기준을 먼저 만들었습니다

기존 프로젝트에서 기능을 개발할 때 Claude Code가 기존 lint·TypeScript 문제를 지속적으로 알려주는 상황을 만났습니다. 현재 변경에서 생긴 문제가 아니라는 이유로 별도로 다루지 않고 넘어가는 흐름도 있었습니다.

이런 상황이 반복되면서 불편함을 느꼈습니다. 코드를 작성하는 것뿐 아니라, 그 코드가 유지되고 관리될 수 있는 기준도 필요하다고 생각했습니다.

테스트 실행 조건에도 정리가 필요했습니다. 단위 테스트 명령 안에 MongoDB, Redis, 로컬 서버가 필요한 테스트가 섞여 있었습니다. 테스트가 없었던 것이 아니라, 실행 조건이 달라 같은 명령으로 안정적인 결과를 얻기 어려웠습니다.

이 상태에서 모든 검사를 한꺼번에 CI에 넣으면 실패 원인이 기존 코드인지 실행 환경인지 구분하기 어렵습니다. 가장 우선해야 할 것은 기존 서비스 기능이 정상적으로 작동하는 것이었습니다.

그래서 초기 CI는 lint와 타입 검사를 중심으로 구성했습니다. 코드 품질을 확인하는 기본적인 검사였고, 실제 코드와 타입을 보면서 수정 범위를 나눌 수 있다고 생각했습니다. 관련 기능의 동작도 함께 확인하며 진행했습니다.

처음부터 검사 항목을 많이 넣기보다, 실패했을 때 개발자가 원인을 이해할 수 있는 기준을 만드는 데 집중했습니다.

3. ESLint 경고는 종류와 위험도를 나눠 정리했습니다

처음 확인한 ESLint 기준은 다음과 같습니다.

Errors: 0
Warnings: 1,614

이 중 상당 부분은 no-explicit-any였고, React Hook 의존성과 상태 처리 관련 경고도 있었습니다. 경고 수가 실제 기능 버그 수를 의미하는 것은 아닙니다.

모든 경고를 같은 방식으로 수정할 수는 없었습니다. 전체 자동 수정보다 규칙과 도메인을 나눠 정리했고, AI를 활용할 때도 수정 범위와 검증 기준을 함께 전달했습니다.

대상수정 시 고려한 점
재할당되지 않는 변수선언 변경의 영향이 비교적 작습니다.
타입 예외 주석실제 타입 오류와 예외 이유를 확인해야 합니다.
anyAPI 응답, DB 문서, 함수 반환 형태를 확인해야 합니다.
React HookEffect 실행 시점과 화면 동작의 변화를 확인해야 합니다.
CommonJS 파일모듈 문법 변경이 실행 방식에 영향을 주는지 확인해야 합니다.

특히 Hook 의존성은 배열에 값을 추가하는 것만으로 해결하기 어려웠습니다. Effect가 다시 실행되는 시점이 달라질 수 있어 상태 동기화와 화면 흐름을 함께 확인해야 했습니다.

4. 타입을 정리하면서 실제 데이터 구조를 확인했습니다

타입 문제를 해결할 때는 컴파일러를 통과시키는 것과 실제 데이터 구조를 설명하는 것을 구분하려고 했습니다.

한 서비스 함수에서는 선언한 반환 타입과 실제 반환 데이터가 달랐습니다. 이중 타입 단언이 그 차이를 가리고 있었습니다. 아래는 설명을 위해 이름과 구조를 축약한 예시입니다.

// 실제 데이터와 다른 타입으로 강제 변환합니다.
return {
  items: result.transactions as unknown as LedgerEntry[],
};

해당 함수의 반환 계약을 실제 데이터 형태에 맞춰 정리하고 단언을 제거했습니다.

type HistoryResult = {
  items: FormattedTransaction[];
};

// 반환 계약과 실제 데이터 형태를 맞춥니다.
return {
  items: result.transactions,
};

이 사례는 당시 호출자가 없어 런타임 동작이 바뀌는 수정은 아니었습니다. 하지만 선언이 실제 반환 형태를 설명하게 됐고, 향후 잘못 사용하는 경우 타입 검사로 확인할 수 있는 상태가 됐습니다.

타입 정리는 오류 메시지를 없애는 작업이면서 코드 사이의 계약을 확인하는 작업이기도 했습니다.

5. 정리한 경고가 다시 누적되지 않도록 했습니다

ESLint 경고를 정리한 뒤에는 다음 조건을 추가했습니다.

eslint . --max-warnings 0

경고가 하나라도 생기면 lint가 실패하도록 기준을 고정했습니다. TypeScript 오류는 별도의 타입 검사로 확인했습니다.

문제를 한 번 정리하는 것뿐 아니라, 새로운 문제가 들어왔을 때 확인할 수 있는 기준도 필요했습니다.

ESLint 경고 정리와 품질 기준 고정

6. 로컬에서 만든 검사 기준을 CI에서도 실행했습니다

lint와 타입 문제를 정리하고 나니 이 기준을 모든 변경에 적용할 방법이 필요했습니다. 로컬에서 검사 명령을 실행할 수 있어도 작업자가 실행하지 않으면 문제가 다시 들어올 수 있기 때문입니다.

사내 Claude Code 플러그인에도 품질 검사 훅을 연결해 지원하는 작업 경로에서 검사하도록 했습니다. 다만 도구의 훅만으로 모든 변경을 확인할 수는 없으므로, GitHub Actions에서 PR의 코드를 받아 같은 기준을 검사하도록 구성했습니다.

처음에는 lint와 타입 검사부터 연결했습니다. 이후 테스트와 빌드를 추가하려고 하니 앞서 발견했던 실행 환경 문제가 다시 드러났습니다. 단위 테스트 명령에는 DB나 Redis가 필요한 테스트가 섞여 있었고, 빌드에는 별도의 환경 설정이 필요했습니다.

이를 정리하지 않고 검사만 추가하면 CI가 실패해도 무엇을 수정해야 하는지 알기 어려웠습니다. 따라서 테스트는 필요한 자원에 따라 구분하고, 빌드와 스모크는 별도 워크플로에서 실행 결과를 확인한 뒤 통합했습니다.

또한 변경된 파일을 기준으로 빌드와 스모크 테스트 실행 여부를 판단하도록 했습니다.

검사 기준을 로컬에서 PR 검증으로 연결한 과정

GitHub Actions PR CI 워크플로우 실행 화면

이렇게 CI를 구성하면서 검증 결과가 개인의 로컬 환경에만 남지 않고 PR에서 확인할 수 있는 정보가 됐습니다. 특정 개발자에게만 의존하기보다 모두가 같은 기준으로 코드를 확인할 수 있는 기반을 만들고자 했습니다.

7. 검증한 코드를 자동으로 배포하도록 연결했습니다

CI에서 코드의 기본 상태를 확인할 수 있게 된 뒤 서버에 반영하는 과정도 연결했습니다.

기존에는 개발자의 로컬에서 빌드한 결과를 전송했습니다. CD에서는 지정된 커밋을 기준으로 GitHub Actions에서 프로덕션 빌드를 만들고 서버에 전달하도록 변경했습니다. 산출물에는 커밋 정보를 남겨 배포 버전을 확인할 수 있게 했습니다.

환경변수도 별도로 만든 관리 도구와 연결했습니다. 빌드에 필요한 값은 암호화 금고에서 읽고, 서버 실행에 필요한 값은 서버 반영 경로에서 관리했습니다. 빌드 시점과 실행 시점에 필요한 설정을 구분한 것입니다.

CI에서 자동 배포까지 연결한 흐름

GitHub Actions CD 파이프라인 실행 화면

main 브랜치 머지 시 동작하는 web-cd.yml 워크플로우(CD Quality Gate, CD Production Build, CD Deploy Production) 실행 화면입니다.

배포 실패에 대한 처리도 함께 구성했습니다. 서버 반영 중 실패하면 이전 파일 스냅샷을 복원하고 프로세스와 응답 상태를 다시 확인하도록 했습니다. 단, 파일 롤백이 DB 스키마나 데이터 변경까지 되돌리는 것은 아닙니다.

배포 실패 시 복구할 수 있는 범위

실행 방식은 단계적으로 바꿨습니다. 처음에는 검사와 빌드를 자동화하고 배포는 직접 실행했습니다. 이후에는 머지 후 승인 여부를 지정할 수 있는 이슈를 생성하도록 했으며, 배포 흐름을 확인한 뒤 별도 승인 단계 없이 main의 배포 대상 변경이 검사와 빌드를 통과하면 배포하도록 전환했습니다.

이제 개발자가 별도로 로컬 배포 스크립트를 실행하지 않아도 배포가 진행되고, 팀원들도 Actions에서 과정과 결과를 확인할 수 있게 됐습니다. 대신 main 머지가 운영 반영으로 이어지는 만큼, 머지 전에 배포 가능한 상태인지 판단하는 일이 중요해졌습니다.

8. 결론

수동 배포를 자동 배포 파이프라인으로 개선했지만, 더 중요했던 것은 코드 품질을 검증하고 그 기준을 유지할 수 있는 틀을 마련한 일이라고 생각합니다.

AI를 활용하면서 코드를 작성하는 속도는 빨라졌습니다. 그만큼 생성된 코드를 어떤 기준으로 확인하고 서비스에 반영할지 고민하는 일도 중요해졌습니다.

개발자라는 직업이 앞으로 어떻게 달라질지는 알 수 없습니다. 다만 지금 제가 할 수 있는 일에 집중하려고 합니다. 서비스를 만드는 데서 끝나지 않고, 유지하고 개선하며 사용자의 문제를 기술로 해결하는 역할은 계속 필요하다고 생각합니다.

이번 작업도 혼자 도구를 만드는 것으로 끝나지는 않았습니다. 저와 팀원들이 불편하게 느낀 점을 이야기하고, 개선 방향에 동의를 구하고, 함께 의논하며 진행했습니다. 어떤 문제를 해결할지 이해하고 팀과 소통하는 과정이 AI 도구를 사용하는 것보다 먼저라고 생각합니다.

앞으로도 함께 일하는 사람들이 겪는 어려움을 찾고, 기술과 소통으로 해결해나가는 마음가짐으로 일하고 싶습니다.