
개발자 100명이 있다면 100가지의 코딩 스타일이 있다는 말이 있다는 말이 있습니다.
그만큼 코드를 짤 때 자유도가 높다는 건데 이게 좋으면서도 큰 단점이 있습니다.

??? : 이거 누가 짰냐
남(또는 옛날의 자신)이 짠 코드를 볼 때 이해하기 힘들다는 것입니다.
누구는 변수명을 bookList라 하고, 누구는 books라 하고, Class에 Books라 썻는데, Class에는 List를 쓰는 등 아주 난리가 납니다.
내가 짠 것도 나중에 보면 이해가 안되는데 하물며 인원이 많은 회사나 팀이면 더욱 보기 어려워질테죠.
이는 코드 품질에도 영향이 있으며, 점진적으로 유지보수를 어렵게 하기도 합니다.
이렇듯 100명의 개발자를 1명이 작업하는 것처럼 만들기 위해 우리는 많은 시도를 합니다.
제일 많은 시도가 컨벤션 문서를 작성하여 공유하고, 코드 리뷰라 생각합니다.
하지만, 문서는 팀원에게 하나부터 열까지 학습시켜야하는 단점이 있고, 코드 리뷰는 코드 + 컨벤션 리뷰까지 해야하니 일이 많아집니다.
사실 우리는 답을 알고 있습니다.
IDE에서 띄워주는 빨간줄과 노란줄이 얼마나 편리한지.
이 글에서는 custom_lint 패키지를 사용한 정적코드분석도구(Lint)의 사용법과 커스텀 규칙을 만드는 방법을 알아보려합니다.
배럴(Barrel) 파일을 사용하지 말라는 warning입니다.
Quick fix 기능을 사용해 import를 제거할 수 있습니다.
린트(Lint)는 코드 품질 문제나 잠재적 버그, 가이드 위반 등을 찾아내는 정적 코드 분석 도구입니다.
옷이나 천에 붙은 보푸라기(lint)를 제거하는 것처럼, 코드에서 작은 결함이나 오류를 제거한다는 의미에서 이름이 붙여졌습니다.
린트를 사용하는 이유?
Custom Lint는 Dart 애널라이저 플러그인을 기반으로 하여 개발자가 자신만의 린트 규칙을 정의할 수 있게 해주는 패키지입니다.
기본 Dart 린터로는 검출할 수 없는 프로젝트 특화된 코딩 규칙이나 아키텍처 패턴을 강제할 수 있습니다.
특징
아래 내용들은
Flutter 3.32.6Dart 3.8.1기준으로 작성되었습니다.
커스텀 린트를 만들고 적용하기 위해선 린트를 위한 프로젝트를 생성해야합니다.
린트만 작성할 것이기에 android, ios 등 플랫폼 파일은 필요없으니 package 아규먼트를 사용해 프로젝트를 생성합니다.
flutter create example_lint -t package
커스텀 린트를 만들기 위한 의존성을 추가합니다.
flutter pub add analyzer analyzer_plugin custom_lint_builder
또는 pubspec.yaml을 직접 수정합니다.
dependencies:
# Dart 파일 분석을 위한 애널라이저
analyzer:
analyzer_plugin:
# 린트 작성 도구 제공
custom_lint_builder:
프로젝트의 lib 디렉토리 안에 <package_name>.dart 파일을 생성합니다.
// lib/<package_name>.dart
/// 🚀 플러그인의 메인 진입점
PluginBase createPlugin() => ExampleLinter();
/// 🎯 커스텀 린트 플러그인
///
/// 이 클래스는 custom_lint와 커스텀한 린트 규칙들 사이의 다리 역할을 합니다.
class ExampleLinter extends PluginBase {
/// 📋 활성화할 린트 규칙들의 목록을 반환합니다.
List<LintRule> getLintRules(CustomLintConfigs configs) => [
// 📝 새로운 린트 규칙을 추가할 때는 여기에 추가하세요:
// SomeOtherLintRule(),
// AnotherCustomRule(),
];
}
엔트리포인트는 반드시
createPlugin()를 정의해야하고<package_name>.dart이름이어야합니다.
createPlugin()custom_lint 패키지가 플러그인을 로드할 때 가장 먼저 호출되는 함수이며 이 함수는 반드시 'createPlugin'이라는 이름으로 정의되어야합니다.
PluginBasecustom_lint에 커스텀 린트를 제공하기 위한 클래스입니다.
getLintRules를 통해 커스텀 린트 목록을 제공합니다.getAssists를 통해 변환 등의 보조 도구도 제공합니다.lib/rules에 avoid_use_barrel_file.dart 파일을 생성합니다.
린트 규칙 작성을 위한 DartLintRule 클래스를 상속받아 규칙 클래스를 만들어줍니다.
/// 🚫 배럴 파일 사용을 방지하는 커스텀 린트 규칙
class AvoidUseBarrelFileLintCode extends DartLintRule {
const AvoidUseBarrelFileLintCode() : super(code: _code);
/// 📝 린트 규칙 메타데이터
static const LintCode _code = LintCode(
name: 'avoid_use_barrel_file',
problemMessage:
'Avoid using barrel files as they can lead to circular dependencies and make code harder to maintain.',
);
/// 🎯 금지할 배럴 파일명 목록
static const List<String> _files = ['importer.dart'];
/// 🔍 린트 실행 로직
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
...
}
/// 🔧 자동 수정 기능 제공
List<Fix> getFixes() => [
...
];
}
LintCode린트 규칙의 메타데이터 클래스입니다.
규칙들을 구분 짓는 name 필드와 IDE에서 보여지는 message 필드가 있습니다.
DartLintRule규칙 작성을 위한 매소드를 제공해주는 클래스입니다.
더 많은 매소드가 있지만 run과 getFixes만 사용했으며 자세한 내용은 아래에서 알아보겠습니다.
규칙은 run 매소드를 오버라이드하여 작성합니다.
/// 🔍 린트 규칙의 핵심 실행 로직
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
// 🎯 레지스트리에 등록
// addImportDirective는 import 문을 만날 때마다 콜백을 실행합니다.
context.registry.addImportDirective((node) {
// 📄 Import URI 문자열 추출
final uri = node.uri.stringValue;
// ✅ 유효성 검사: null 체크 및 Dart 파일 확인
if (uri == null || !uri.endsWith('.dart')) {
return;
}
// 📁 파일명 추출
final file = uri.split('/').last;
// 🚨 규칙 검사 및 보고
if (_files.contains(file)) {
// 📢 에러 보고: IDE와 커맨드라인에 표시
reporter.atNode(node, _code);
}
});
}
단순히 import.dart를 import했는지 확인하는 로직입니다.
변수 선언을 검사하는 addVariableDeclaration, 클래스 선언을 검사하는 addClassDeclaration 등 다양한 검사 노드를 제공합니다.
에러 보고는 reporter 객체를 사용해 보고합니다. atNode는 해당 노드에서 보고를 표시하기 위해 사용합니다.
외에도 atEntity, atToken 등 다양한 보고 매소드를 제공합니다.
엔트리포인트의 getLintRules에 만든 규칙을 추가합니다.
class GoldspoonLinter extends PluginBase {
List<LintRule> getLintRules(CustomLintConfigs configs) => [
AvoidUseBarrelFileLintCode(),
];
}
여기까지 린트 규칙 추가는 끝났고 아래는 선택사항으로 IDE에서 빠른 수정을 활성화 하는 과정입니다.
lib/fixes 디렉토리에 remove_barrel_import.dart 파일을 생성합니다.
수정 로직 작성을 위한 DartFix 클래스를 상속받아 수정 클래스를 만들어줍니다.
/// 🔧 배럴 파일 import를 자동으로 제거하는 Fix 클래스
class RemoveBarrelImport extends DartFix {
RemoveBarrelImport(this.files);
final List<String> files;
/// 🚀 자동 수정 메서드
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
...
}
}
run 매소드를 오버라이드해 수정 로직을 작성합니다.
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
// 린트 규칙과 동일한 방식으로 import 문을 순회합니다.
context.registry.addImportDirective((node) {
// 📍 위치 기반 필터링
if (!analysisError.sourceRange.intersects(node.sourceRange)) {
return;
}
// 🛠️ 코드 변경 객체 생성
final changeBuilder = reporter.createChangeBuilder(
message: 'Remove barrel import',
priority: 10,
);
// 📝 Dart 파일 편집 작업 등록
changeBuilder.addDartFileEdit((builder) {
// 🔍 URI 추출 및 검증 (린트 규칙과 동일)
final uri = node.uri.stringValue;
if (uri == null || !uri.endsWith('.dart')) {
return;
}
/// 📁 파일명 추출 및 배럴 파일 여부 확인
final file = uri.split('/').last;
if (!files.contains(file)) {
return;
}
// ✂️ 실제 삭제 작업 수행
builder.addDeletion(node.sourceRange);
});
});
}
DartLintRule과의 차이점만 빠르게 알아보겠습니다.
intersects를 사용해 실제 에러 범위와 현재 노드의 범위를 비교합니다. 즉, 수정하고자 하는 범위만 수정합니다.createChangeBuilder는 빠른 수정에 보여질 항목을 생성합니다.addDeletion는 빠른 수정을 실행했을 때 어떤 처리를 할 지입니다. addSimpleReplacement, addInsertion 등 다양한 수정 동작을 제공합니다.AvoidUseBarrelFile 클래스의 getFixes 매소드에 추가합니다.
class AvoidUseBarrelFileLintCode extends DartLintRule {
...
/// 🔧 자동 수정 기능 제공
List<Fix> getFixes() => [
RemoveBarrelImport(_files),
];
}
만든 린트를 적용하려면 우리의 린트 프로젝트와 함께 custom_lint 패키지도 추가해줘야합니다.
경로는 본인의 프로젝트 구조에 맞게 변경합니다.
dev_dependencies:
...
custom_lint: ^0.7.6
example_lint:
path: ../../example_lint
analysis_options.yaml에서 custom_lint를 활성화합니다.
analyzer:
plugins:
- custom_lint
errors:
avoid_use_barrel_file: warning
avoid_use_barrel_file를warning으로 설정하면 IDE에서 노란줄로 표시됩니다. (기본은info, 초록색)
Dart에서 기본으로 제공하는 dart analyze로는 우리가 만든 린트가 동작하지 않습니다.
아래 명령어를 입력해 애널라이저를 실행합니다.
dart run custom_lint
짠! 린트가 정상 동작하는 모습입니다.
빠른 수정도 지원하고 빠른 수정 실행 시 import문이 삭제됩니다.
사실 "barrel 파일은 사용하지 마시오"라고 문서에 써놓는 것보다 훨 복잡하긴 합니다. 하지만, 이러한 규칙이 몇 개, 몇 십 개가 된다하면 이러한 린트로 컨벤션을 만들어 놓는 것이 빛을 발할 것이라 생각합니다.
컨벤션 문서를 달달 외우고 개발하고 싶어하는 사람은 없을 테니까요..
https://pub.dev/packages/custom_lint
https://github.com/invertase/dart_custom_lint/tree/main/packages/custom_lint/example/example_lint