목차
배경
ESLint에 새로운 룰을 추가하면, 기존의 모든 코드가 해당 룰을 위반하고 있지 않은지 검사됩니다. 대규모 모노레포에서는 새 룰을 추가할 때마다 대량의 기존 코드를 수정해야 하며, 영향 범위가 커지는 문제가 있습니다.
예를 들어, 새로운 룰을 하나 추가한 것만으로 수십~수백 개의 파일에서 에러가 검출되는 경우가 있습니다. 이렇게 되면 새로운 룰의 도입 비용이 너무 높아져서 코드 품질 개선이 진행되기 어렵습니다.
이상적으로는, 수정된 파일만 검사하여 영향 범위를 줄이면서 단계적으로 품질을 향상시키고 싶습니다.
과제
각 앱의 CI 워크플로우에서는 ESLint를 프로젝트 전체에 대해 실행하고 있었습니다.
- name: ESLint
run: yarn lint:app-a
이 방식의 문제점은 다음과 같습니다.
- 새 룰 추가 시 기존의 모든 파일이 검사 대상이 된다
- 기존 코드의 대량 수정이 필요하여 PR의 영향 범위가 커진다
- 결과적으로 새 룰 도입을 주저하게 된다
검토한 접근 방식
방안 A: 설정 파일을 2개로 분리하여 운용
처음에 검토한 방법은 ESLint 설정 파일을 기존 룰용과 신규 룰용 2개로 나누어 운용하는 것이었습니다.
.eslintrc.cjs ← 기존 룰 (전체 파일 검사)
.eslintrc.new-rules.cjs ← 신규 룰 (변경 파일만 검사)
# 기존 룰: 프로젝트 전체에 실행
- name: ESLint (existing rules)
run: yarn lint:app-a
# 신규 룰: 변경된 파일에만 실행
- name: ESLint (new rules)
run: npx eslint -c .eslintrc.new-rules.cjs $CHANGED_FILES
이 방식은 기존 룰의 전체 검사를 유지하면서 신규 룰만 단계적으로 도입할 수 있다는 장점이 있습니다. 하지만 실제로 운용을 고려하면 여러 문제가 있었습니다.
- 설정 파일 관리가 복잡해진다: 2개의 설정 파일을 항상 동기화해야 한다
- 룰 승격 작업이 필요하다: 신규 룰이 전체 코드에 적용 가능해지면
.eslintrc.new-rules.cjs에서.eslintrc.cjs로 옮기는 작업이 발생한다 - 어떤 룰이 어디에 있는지 파악하기 어렵다: 룰이 두 곳에 분산되어 관리 포인트가 늘어난다
방안 B: 모든 룰을 변경된 파일에만 적용 (채택)
다음으로 검토한 방법은, 설정 파일은 하나로 유지하되 검사 대상을 변경된 파일로 한정하는 것이었습니다.
.eslintrc.cjs ← 기존 룰 + 신규 룰 모두 포함 (변경 파일만 검사)
이 방식의 우려는 “기존 룰이 전체 파일에 적용되지 않으면 품질이 떨어지지 않을까?”라는 점이었습니다. 하지만 다음과 같은 이유로 실질적인 문제가 없다고 판단했습니다.
- 기존 코드는 이미 기존 룰을 통과하고 있다: 전체 검사를 하지 않아도, 기존 파일이 갑자기 위반 상태가 되지 않는다
- 변경되지 않은 파일은 검사할 필요가 없다: 위반이 발생하는 것은 코드가 변경될 때뿐이다
- 설정 파일이 하나이므로 관리가 간단하다: 신규 룰을 추가할 때
.eslintrc.cjs에 추가하기만 하면 된다
결과적으로 설정의 단순함과 운용의 용이함을 우선하여 방안 B를 채택했습니다.
해결: PR에서 변경된 파일에만 ESLint 실행
GitHub API를 사용하여 PR의 변경 파일 목록을 가져오고, 변경된 파일에만 ESLint를 실행하도록 수정했습니다.
- name: Get changed files and run ESLint
uses: actions/github-script@v7
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const filtered = files
.filter(f => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
.filter(f => f.filename.startsWith('apps/app-a/src/'))
.filter(f => /\.(ts|tsx|js|jsx)$/.test(f.filename))
.map(f => f.filename);
if (filtered.length === 0) {
core.info('No matching files to lint.');
return;
}
await exec.exec('npx', [
'eslint',
'-c',
'apps/app-a/.eslintrc.cjs',
...filtered,
]);
포인트 해설
1. GitHub API로 변경 파일 취득
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
github.paginate를 사용하여 PR의 변경 파일 목록을 가져옵니다. 페이지네이션에 대응하므로 변경 파일이 많은 경우에도 전체를 취득할 수 있습니다.
2. 대상 파일 필터링
const filtered = files
.filter((f) => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
.filter((f) => f.filename.startsWith('apps/app-a/src/'))
.filter((f) => /\.(ts|tsx|js|jsx)$/.test(f.filename))
.map((f) => f.filename);
3단계로 필터링을 수행합니다.
| 필터 | 목적 |
|---|---|
status 체크 | 추가, 수정, 리네임, 복사된 파일만 대상 (삭제된 파일 제외) |
startsWith 체크 | 해당 앱의 src/ 하위 파일만 대상 |
| 확장자 체크 | .ts, .tsx, .js, .jsx 파일만 대상 |
3. 변경 파일이 없는 경우 스킵
if (filtered.length === 0) {
core.info('No matching files to lint.');
return;
}
대상 파일이 없는 경우 ESLint 실행을 스킵합니다. 예를 들어, 설정 파일이나 문서만 변경된 경우에는 ESLint를 실행하지 않습니다.
4. 변경 파일에만 ESLint 실행
await exec.exec('npx', [
'eslint',
'-c',
'apps/app-a/.eslintrc.cjs',
...filtered,
]);
yarn lint:app-a (프로젝트 전체) 대신, 필터링된 파일만 인수로 전달하여 ESLint를 실행합니다. 각 앱에 적용할 때는 src/ 경로와 ESLint 설정 파일 경로만 변경하면 됩니다.
동작 확인
테스트용 PR에서 실제 동작을 확인했습니다.
새로운 ESLint 룰을 추가한 경우:
- 변경한 파일: ESLint 에러가 검출됨
- 변경하지 않았지만 문제가 있는 파일: ESLint 에러가 검출되지 않음
이로써 새 룰 추가 시 기존 코드에 영향 없이 CI가 통과하는 것을 확인할 수 있었습니다.
정리
| Before | After | |
|---|---|---|
| 검사 대상 | 프로젝트 전체 | PR에서 변경된 파일만 |
| 새 룰 추가 시 영향 | 전체 파일 수정 필요 | 변경 파일만 대응하면 OK |
| 기존 코드 품질 개선 | 일괄 대응 필요 | 수정 시 단계적으로 개선 |
이 방식을 통해 새로운 ESLint 룰을 부담 없이 추가할 수 있게 되었고, 코드 품질을 단계적으로 향상시킬 수 있게 되었습니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.