Navigation

8. 도구 설정

자동 검사할 항목은 stylelint로 설정하고, 도구가 판단하지 못하는 항목은 리뷰에서 확인합니다.

8.1 stylelint로 자동 검사할 규칙을 설정합니다

자동 검사 범위를 설정으로 고정하고 의미 판단은 리뷰에 남깁니다.

규칙별 담당

stylelint-config-standard를 확장해 아래 규칙을 적용하고 기계가 확인하지 못하는 의미는 리뷰에서 판단합니다.

Stylelint 규칙담당 컨벤션
selector-class-pattern규칙 1.2
max-nesting-depth규칙 4.1
keyframes-name-pattern규칙 7.2
property-disallowed-list, custom-property-pattern규칙 5.2. 이름 패턴은 토큰 선언 파일에서만 켭니다
selector-attribute-name-disallowed-list규칙 4.5
selector-max-id규칙 1.2. 선택은 클래스로만 합니다
selector-pseudo-class-disallowed-list규칙 4.7
declaration-no-important규칙 7.2의 전역 처리만 예외입니다
media-feature-range-notation규칙 6.2의 범위 표기. 표준 설정에 포함됩니다
no-descending-specificity자손 기본 블록을 조상 규칙보다 앞에 둡니다. 표준 설정에 포함됩니다

selector-disallowed-list는 다음 세 컨벤션을 담당합니다.

no-duplicate-selectors는 규칙 4.4과 규칙 4.3의 단독 재선언을 담당합니다. 폴더별 접두사는 selector-class-pattern과 selector-disallowed-list의 overrides로 나눕니다. selector-max-combinators와 selector-max-type은 넣지 않습니다. 개수만으로 우리 선택자와 라이브러리 경로를 구분할 수 없습니다.

도구 한계와 리뷰

대상도구 한계처리
쉼표 묶음 자체disallowInList는 단독 재선언만 검사합니다규칙 4.3로 판단합니다
최상위 요소 선택자ownMarkupPatterns는 &로 시작하는 형태만 검사합니다selector-max-type은 쓰지 않고 리뷰에서 확인합니다
파일별 소유자 하나, 다른 소유자 클래스의 블록 위치같은 레이어의 다른 식별자와 미등록 외부 클래스는 잡지 못합니다리뷰에서 확인합니다
중첩 @media와 데스크톱 퍼스트 방향at-rule의 최상위 배치를 강제하지 못합니다규칙 6.1으로 판단합니다
우리 마크업의 구조 선택자:first-child, :nth-child()는 클래스에도 붙습니다표기만으로 구분하지 못해 리뷰에서 확인합니다
역할 이름, 승격 판단, 변형 노출, 포커스 대비기계가 뜻을 판단하지 못합니다리뷰에서 확인합니다

공통 규칙에 폴더별 접두사 overrides를 더합니다.

/**
 * 우리 클래스에만 문법 강제
 * 우리 접두사로 시작하지 않는 클래스는 남의 것이라 검사 제외
 */
const ownClassPattern = (scope) =>
	[
		"^(?:",
		// 우리 접두사로 시작하지 않는 클래스는 통과
		`(?!${scope}_).*`,
		"|",
		// pg_scopeSlug__element 또는 pg_scopeSlug__element--modifier만 통과
		`${scope}_[a-z][a-zA-Z0-9]*__[a-z][a-zA-Z0-9]*(?:--[a-z][a-zA-Z0-9]*)?`,
		")$",
	].join("");

/**
 * 우리가 이름을 정하지 않는 라이브러리 클래스
 */
const libraryPrefixes = [/^\.ant-/, /^\.rc-/, /^\.tippy-/, /^\.Mui/];

/**
 * 우리가 마크업을 쓰는 자리에서 금지되는 형태
 */
const ownMarkupPatterns = [
	// 상태 가상 클래스를 최상위 선택자로 다시 여는 것
	/^\.[\w-]+:(hover|focus|focus-visible|focus-within|active|enabled|disabled|checked|visited)/,
	// 중첩 안에서 요소 선택자로 우리 마크업을 잡는 것
	// 우리가 쓰지 않는 마크업은 stylelint-disable 주석으로 예외 표시
	/^&\s*[>+~]?\s*[a-z]/,
];

const disallowed = (foreignScopes) => [
	[...foreignScopes, ...libraryPrefixes, ...ownMarkupPatterns],
	{splitList: true},
];

export default {
	extends: ["stylelint-config-standard"],
	rules: {
		// 최상위 @media 안의 클래스는 깊이 0. 브레이크포인트 안에서 상태를 한 겹 더 쓸 수 있음
		"max-nesting-depth": [1, {ignoreAtRules: ["media", "supports", "container"]}],
		// @keyframes 이름은 전역이라 소유자 접두사 필수. 하이픈은 클래스 --수정자 표기와 섞여 금지
		"keyframes-name-pattern": "^(pg|wg|ui)_[a-z][a-zA-Z0-9]*__[a-z][a-zA-Z0-9]*$",
		// 쉼표 목록에 든 선택자를 아래에서 단독으로 다시 여는 것까지 검출
		"no-duplicate-selectors": [true, {disallowInList: true}],
		// 움직임 감소 전역 처리 외에는 금지
		"declaration-no-important": true,
		// 지역 변수 선언 금지. var() 소비는 허용
		"property-disallowed-list": ["/^--/"],
		// 우리 마크업의 상태는 수정자로 표현
		// 라이브러리가 상태를 data-*로 내는 경우가 있어 우리 접두사만 금지
		"selector-attribute-name-disallowed-list": [/^aria-/, /^data-(pg|wg|ui)-/],
		"selector-max-id": 0,
		// 부정 조건은 기본 블록으로 뒤집음. 남의 마크업만 stylelint-disable로 허용
		"selector-pseudo-class-disallowed-list": ["not"],
	},
	overrides: [
		{
			files: ["src/page/**/*.css"],
			rules: {
				"selector-class-pattern": ownClassPattern("pg"),
				"selector-disallowed-list": disallowed([/^\.(wg|ui)_/]),
			},
		},
		{
			files: ["src/component/widget/**/*.css"],
			rules: {
				"selector-class-pattern": ownClassPattern("wg"),
				"selector-disallowed-list": disallowed([/^\.(pg|ui)_/]),
			},
		},
		{
			files: ["src/component/ui/**/*.css"],
			rules: {
				"selector-class-pattern": ownClassPattern("ui"),
				"selector-disallowed-list": disallowed([/^\.(pg|wg)_/]),
			},
		},
		{
			// 전역 스타일시트는 우리 클래스 문법 대상 아님
			files: ["src/style/**/*.css", "src/*.css"],
			rules: {
				"selector-class-pattern": null,
				"keyframes-name-pattern": null,
				"property-disallowed-list": null,
				// 움직임 감소 전역 처리는 여기서만
				"declaration-no-important": null,
			},
		},
		{
			// 전역 토큰 파일에만 이름 강제
			files: ["src/style/token.css"],
			rules: {
				"selector-class-pattern": null,
				"property-disallowed-list": null,
				// var() 사용까지 검사하므로 외부 변수를 소비하는 파일에는 쓰지 않음
				"custom-property-pattern": "^app-[a-z0-9-]+$",
			},
		},
	],
};

함께 보기