
저장소 품질 스캔 중에 .github/workflows 는 있지만 typecheck·lint 를 실제로 검증하는 잡이 없는 걸 발견했고, CI 를 추가하려고 로컬에서 ESLint 를 먼저 돌려보니 오래 잠복해 있던 위반 3건이 튀어나왔다.
레거시 개선 거리를 찾느라 프론트 저장소 여러 개를 훑었다. 의존성 버전, TypeScript strict 설정, CI 워크플로우 파일 수, any 타입 수, @ts-ignore 수, eslint-disable 수, 큰 파일 분포 같은 지표를 뽑고 "어디가 가장 손볼 여지가 많은가" 를 보려던 목적이었다.
결과를 표로 정리하다 한 저장소가 눈에 띄었다. 해당 저장소에는 워크플로우 파일이 2개뿐이었고 그것도 전부 PR 제목 검증과 머지 자동화였다. typecheck, lint, test 를 돌리는 잡이 아예 없었다😇
기능 변경이 잦은 저장소가 아니다 보니 여러모로 방치되었던 것 같은데,
아예 CI 가 빠져 있는 줄은 몰랐다.(내 얼굴에 침뱉기인듯ㅜㅜ)

처음엔 GitHub Actions 에 없어도 Jenkins 배포 파이프라인에서 린트가 돌 거라고 믿었다. 그런데 Jenkins 배포 스크립트를 뜯어보니 순서가 이랬다.
run "Install" "yarn install --immutable --immutable-cache"
run "Build" "yarn env-cmd ... craco build"
run "Check Build" "yarn env-cmd ... kn-check-cra-build"
ssh_deploy_server
명시적 lint 단계가 없었다. "그럼 craco build 안에서 CRA 가 내부적으로 ESLint 를 돌리니까 거기서 걸러지지 않을까?" 싶었는데 이것도 제약이 있었다.
react-app 프리셋만 강제로 사용한다. 프로젝트 package.json 에 깔려있는 @typescript-eslint/* 플러그인 규칙들은 이 경로에서 적용되지 않는다.CI=true 환경변수가 있어야 warning 이 error 로 승격된다. 없으면 warning 으로만 출력되고 빌드는 성공한다.CI=true 를 자동 설정하지만, 승격된다 해도 그물 자체가 좁은 프리셋이라 프로젝트에 있는 더 엄격한 규칙은 빠져나간다.즉 어느 경로에서도 실제로 원하는 수준의 린트가 돌지 않고 있었다.
| 검사 경로 | 돌긴 했나 | 잡았나 |
|---|---|---|
로컬 craco start | O (react-app 프리셋) | X, @typescript-eslint/* 는 프리셋 밖 |
Jenkins craco build | O (동일) | X, 동일 |
| Jenkins 명시 lint 단계 | X | (해당 없음) |
| GitHub Actions CI | X | (해당 없음) |
로컬에서 ./node_modules/.bin/eslint src --ext .ts,.tsx,.js,.jsx 를 직접 돌렸더니 곧바로 3개의 에러가 떴다. 모두 @typescript-eslint/return-await 규칙 위반이었다.
// 수정 전
export default async function getData(params) {
return await fetchApi(params);
}
// 수정 후 (--fix 로 자동 수정)
export default async function getData(params) {
return fetchApi(params);
}
return await 는 대부분의 경우 불필요한 한 단계를 더 거친다. async 함수 안에서 return somePromise 는 Promise 를 그대로 호출자에게 넘겨주는데, return await somePromise 는 한 번 풀어서 값을 받고 다시 Promise 로 감싸서 돌려준다. 호출자 입장에서 결과는 같지만, 안에서 try/catch 로 reject 를 잡고 싶을 때만 await 가 실제로 의미가 있다.
async function f() {
try {
return await apiCall(); // reject 가 여기서 잡힘
} catch (e) {
handle(e);
}
}
try/catch 없는 곳에서 return await 는 마이크로태스크 한 번 추가 + 의도 혼란만 남긴다. 이번 3건 모두 try/catch 밖이었고 --fix 로 안전하게 제거됐다.
다음 순서로 진행했다.
eslint --fix 한 방으로 해결!만약 순서를 뒤집었다면, CI 를 추가한 첫 날부터 3건의 과거 위반이 파이프라인을 막아서 아무도 머지를 못 한다. 규모가 3건이 아니라 수백 건이었다면 autofix 대량 적용, 정말 못 고칠 건 파일 단위 예외 처리 같은 순차적 작업이 필요해진다. 벌써 아찔하다.

ci.yml 은 typecheck 와 lint 를 두 개의 병렬 job 으로 분리했다. 한 job 안 스텝으로 묶지 않은 이유:
on.paths 로 src/**, package.json, yarn.lock, tsconfig*.json, .github/workflows/ci.yml 에만 반응하도록 제한했다. README 같은 문서만 바꾼 PR 에 CI 크레딧을 태우지 않는다.
concurrency.group: ${{ github.workflow }}-${{ github.ref }} 로 같은 브랜치에 연속 커밋이 들어오면 이전 실행을 취소한다. group 을 ${{ github.workflow }} 만으로 두면 A 브랜치 CI 가 돌고 있을 때 B 브랜치에 커밋해도 A 까지 취소되므로 조합이 중요하다.
react-app 프리셋 고정. 프로젝트에 @typescript-eslint/* 같은 추가 규칙을 깔아도 빌드 타임에는 일부만 적용된다. 독립 lint 스텝이 필요하다..github/workflows/ 파일 개수 만 보고 안심하지 말기. 실제로 타입체크·린트·테스트·e2e 중 무엇이 돌고 있는지 내용까지 확인하기.eslint 를 먼저 돌려서 잠복 위반부터 발굴하기. autofix 로 해결되는 건 단일 커밋으로 빠르게 정리.ci.yml 몇 줄로 끝나지만, 도입 순서가 전부다. 기존 위반 청산 → 게이트 설치 → 이후 회귀 차단.