목차
- eslint-lint-sprinkles-rule 제작기 - (1) 문제인지
- eslint-lint-sprinkles-rule 제작기 - (2) sprinkles-rule 작성을 위한 배경 지식
- eslint-lint-sprinkles-rule 제작기 - (3) sprinkles-rule 내부를 구성하는 코드 설명
본론
이번 글은 바로 기존에 정의했던 문제를 바탕으로 작성한 코드를 분석해보려한다.
실제 린트에 대한 정의를 수행하기 전 필요한 값부터 확인해보자.
우리가 해결하고자 하는 문제는 내 프로젝트에 정의되어있는 sprinkles를 바탕으로 lint룰을 통해 sprinkles로 사용해야함에도 불구하고 일반 style을 사용하는 케이스를 잡아내야하기때문에 부득이하게 lint plugin으로 sprinkles에 대한 config 객체를 넘겨주어야한다.
또한 vanilla-extract에서는 sprinkles의 사용 시 이에 대한 shortcut, 즉 축약어를 활용해 여러가지 스타일을 한번에 적용할 수도 있기때문에 shorthands config도 전달받아야한다.
다행히 eslint에서는 어떤 종류의 property를 넘겨 받는지, 받는 값은 어떤 값들이 들어오는 지에 대한 schema를 정의해주면 plugin을 선언할 때 두번째 인자를 통해 lint로 넘겨주고자 하는 데이터를 넘겨줄 수 있게 되어있다.
또한 schema를 정의해줄 때, lint의 속성에 대해 정의할 수 있는데 문제 상황일때 어떤 메시지를 보여줄 것(messages)인지, 문제 상황일때 lint rule에 따라 수정까지 해줄 것(fixable)인지를 정의할 수 있다.
하지만 해당 lint는 우리 프로젝트에서 사용하기 위해 만들었던 것이고, 우리 프로젝트에서는 아직 Eslint Flat 문법으로 마이그레이션할 수 없는 플러그인들을 사용하고있었다. 따라서 내부적으로 ESM이 아니라 CJS를 사용하고있으며 넘겨받는 파일도 CJS를 넘겨받아야한다.
그렇다면 기존에 사용하던 config를 단순히 파일분리하는 것으로는 넘겨줄 수 없는데, 이를 위해 사용처에서 sprinkles config와 shorthands config를 별도 파일에 분리해두기만 하면 이를 통해 plugin에 넘겨줄 수 있는 파일을 만들어주는 스크립트를 작성했다.
내용은 간단하다.
분리해둔 sprinkles config 파일로부터 sprinklesProperties와 shorthands 변수를 가지고오고 이를 CJS 문법으로 된 파일로 만들어주는 것인데, 이 역시 ESM문법으로 작성되어있기때문에 단순히 스크립트 실행을 하면 에러가 발생한다.
이를 수행하기 위해 tsx 라는 라이브러리를 활용할 수 있는데, 이 라이브러리를 활용하면 Typescript 코드를 Node.js에서 실행할 수 있게 해준다.
여기까지 진행한다면 일단 custom lint 에 필요한 환경은 구축이 되었기 때문에 이제는 내부 코드를 이전 글에서 보았던 요구사항에 따라 케이스별로 나누어 분석해보겠다.
먼저 기존에 해결해야하는 상황에 대해 정리한 내용은 다음과 같다.
Style Case
- 단순히 style만 있는 경우
- style 내부 인자가 객체일 때
- style object에 있는 스타일 중 일부 스타일이 sprinkles에 들어있는 경우 : style 배열로 감싸고 sprinkles에 있어야하는 스타일은 sprinkles로 옮기고, 남은 스타일은 style object에 넣어준다.
- style object에 있는 스타일 중 모든 스타일이 sprinkles에 들어있는 경우 : style로 감싸져 있는 객체를 sprinkles로 바꾼다.
- style 내부 인자가 배열일 때
- 배열 내의 style object 내에 있는 스타일 중 일부가 sprinkles에 있는 경우 : 기존의 형태를 유지하면서 style object에 있는 일부 스타일만 sprinkles로 이동한다.
- 배열 내의 style object 내에 있는 스타일 중 모든 스타일이 sprinkles에 있는 경우 : 기존 형태를 유지하면서 style object를 제거하고 style object에 있던 모든 스타일을 sprinkles로 이동한다.
- 변환 후 style 배열 내에 sprinkles만 남은 경우 : 감싸고있던 style 배열을 제거하고 sprinkles만 남겨준다.
- 변수와 같이 사용하고 있는 경우 : 변수는 그대로 두어야하기때문에 style 배열을 유지한다.
- style 내부 인자가 객체일 때
- 실제로는 sprinkles만 사용하는데 style array로 감싸져 있는 경우
- 이런 케이스는 일반적으로는 잘 발생하지 않지만, 특정 케이스에서 스펙이 변경되어 style([])로 감싸져있었지만 특정 스타일이 사라져 sprinkles만 남는 경우가 있기때문에 이를 린트에서 체크해주고 Sprinkles만 남아있는 경우 감싸고있는 style 배열을 제거해준다.
Recipe Case
- recipe의 base가 배열로 감싸져 있는 경우
- 배열 내의 style object 내에 있는 스타일 중 일부가 sprinkles에 있는 경우 : 기존의 형태를 유지하면서 style object에 있는 일부 스타일만 sprinkles로 이동한다.
- 배열 내의 style object 내에 있는 스타일 중 모든 스타일이 sprinkles에 있는 경우 : 기존 형태를 유지하면서 style object를 제거하고 style object에 있던 모든 스타일을 sprinkles로 이동한다.
- 변환 후 style 배열 내에 sprinkles만 남은 경우 : 감싸고있던 대괄호를 제거하고 sprinkles 만 남겨준다.
- 변수와 같이 사용하고 있는 경우 : 변수는 그대로 두어야하기때문에 나머지는 그대로 두고 감싸고있던 형태가 style 배열인지 체크하고 style 배열이라면 일반 배열로 만들어준다.
- 나머지는 모두 정상이지만 recipe의 base에 style로 감싸져 있는 경우
- 일반적으로 이렇게 작성해도 정상적으로 동작하지만, 내가 만든 룰에서는 통일성을 위해 recipe의 base에 2개 이상의 인자가 존재한다면 이는 반드시 배열로만 감싸야한다. 따라서 기존에 여러 사람이 작업하면서 파편화되어있는 케이스를 하나로 모아준다.
굉장히 많은 내용이 있는것처럼 보이지만 핵심 내용은 받아온 property들을 역할에 맞게 분리하고, 이를 상황에 맞게 포맷팅만 해주는 것이다.
따라서 모든 케이스의 코드를 보기 보다는 대부분의 케이스에서 처리하는 방식을 모두 포함하고있는 Style Case에서 Object일때 이를 포맷팅하는 방식만 보겠다.
CallExpression(node) {
// using style
if (node.callee.name === 'style') {
// if style({}), {} is node.arguments[0]
const styleArgument = node.arguments[0];
// Case. style({})
if (isObject(styleArgument)) {
const { sprinklesProps, remainingProps } = separateProps({
sprinklesConfig,
shorthands,
properties: styleArgument.properties,
sourceCode,
});
if (isEmpty(sprinklesProps)) {
return;
}
const targetProperties = Object.keys(sprinklesProps).join(', ');
context.report({
node: styleArgument,
messageId: 'useSprinkles',
data: {
property: targetProperties,
},
fix(fixer) {
if (isEmpty(remainingProps)) {
// return sprinkles only template
return fixer.replaceText(node, `sprinkles(${sourceCode.getText(styleArgument)})`);
}
return fixer.replaceText(
node,
createTransformTemplate({
sourceCode,
sprinklesProps,
remainingProps,
}),
);
},
});
}
먼저 처음에는 AST를 통해 해당 구문이 style 구문인지를 찾아야한다.
이전 글에서 언급했지만 AST에서는 해당하는 성격의 구문을 바로 찾을 수 있는 메서드가 존재하는데 현재 찾고자 하는 값은 함수 호출 표현식이기때문에 CallExpression이라는 표현을 통해 접근이 가능하다.
해당 표현을 통해 각 node에 대한 AST를 받아온다면 이 값은 간단하게는 아래와 같이 나타난다. (아래의 이미지는 style({}) 에 대한 AST이기때문에 대략적인 형태만 보길 바란다.)
AST를 보면 알 수 있지만 해당 node의 callee.name으로 접근하면 이름을 가지고 판단할 수 있게 된다. 따라서 우리가 lint를 통해 체크하고자 하는 값을 이를 통해 확인할 수 있게 된다.
모든 케이스에서 동일하게 수행하겠지만 받아온 node의 인자를 먼저 style 객체인지 배열인지 확인하고, 받아온 값을 토대로 우리가 넘겨준 sprinkles config에 해당하는지, style에 해당하는지를 나누어(separateProps) 상황에 맞게 포맷팅해주는 것이다.
이 상황에서 만약 분리된 props 중 sprinkles가 비어있다면 포맷팅할 필요가 없기때문에 return 시키고, 또한 남은 style props가 없다면 해당 style 구문은 sprinkles 구문으로 대체가 가능하기때문에 이에 대한 예외 처리만 해주면 된다.
그렇다면 핵심이 되는 separateProps와 이 내부에서 각 Props을 나누는 함수를 살펴보자.
먼저 separateProps를 보자.
여기서는 sprinkles와 남은 style인 remainingStyle 을 Map으로 관리하는데, 이는 sprinkles에 선언한 속성을 style에서도 선언하는 경우가 간혹 생길 수 있는데 이런 케이스를 제거하기 위함이다.
이렇게 선언해둔 후 받아온 property들을 체크하면서 상황에 맞게 sprinklesMap 혹은 remainingStyleMap에 넣어주는데, Map을 통해 처리하고 있더라도 이미 해당 Key에 대한 값이 이미 Map에 존재하는 경우에는 하위의 로직을 처리할 필요가 없기때문에 먼저 각 Map에 해당 property가 이미 있는지 확인하고 없을때만 체크를 수행하도록 한다.
처음에는 CSS에서 사용하는 selector (:나 &를 활용하는 방식) 을 체크하고, 이런 케이스는 sprinkles에서 사용할 수 없기 때문에 remainingStyleProps로 보내준다.
또한 이미 선언해둔 변수 역시 동일하기때문에 이런 케이스도 remainingStyleProps로 보내준다.
이렇게 일차적으로 정리한 후 해당 값이 sprinkles config에 정의되어있는지를 확인하는 checkDefinedValueInSprinkles 를 수행하고 이에 따라 각 Map에 할당한 후 이를 객체형태로 다시 포맷팅해 반환해주는 방식인 것이다.
그렇다면 마지막으로 핵심인 checkDefinedValueInSprinkles 를 보겠다.
checkDefinedValueInSprinkles 는 생각보다 간단한데 먼저 받아온 값이 shorthands config에 있는지를 확인한다.
당연하게도 shorthands는 sprinkles에 정의되어있어야만 사용이 가능하기때문에 shorthands에 해당 값이 있다면 sprinkles라고 보는 것이다.
이후에는 shorthands에는 정의되어있지 않은 값이기 때문에 해당 값이 sprinkles config에 있는지 확인해야한다.
이렇게 확인하기 전 확인하고자하는 값이 숫자라면 괜찮지만, 문자열인 경우에는 작은 따옴표인지 큰 따옴표인지에 따라 값을 다르게 인지할 수도 있기때문에 이를 작은 따옴표로 통일시키는 작업을 수행해준다.
이제 대상인 값을 확인하기만 하면 되는데, sprinkles에서는 값을 정의할 때 단순히 배열에 해당 속성이 가질 수 있는 값을 담을 수도 있지만 color같은 값은 객체를 통해 key, value로 정의해서 사용할 수도 있다. (이에 대한 예시는 위의 코드에 있다.)
따라서 sprinkles의 config가 가진 value가 배열인지 객체인지 케이스를 나누고 이에 따라 해당 값이 있는지 여부를 확인해준 뒤 config에 있다면 true를 없으면 false를 반환해주면 되는 것이다.
이렇게 각 케이스를 처리하고 나면 케이스별로 아래와 같이 변환될 수 있다.
결론
처음에는 Lint를 통해 문제를 해결한다는 생각을 못했지만 좋은 레포를 보고 겪고 있는 문제를 Custom Lint를 통해서도 해결할 수 있다는 점을 알게되고 실제로 만들어 팀원분들의 생산성을 향상시키는데 기여해서 좋았다.
다만 Vanilla Extract를 사용하는 다른 사람들도 비슷한 문제를 겪고있을텐데 기존에 이런 lint가 없다는 것이 의아했고 실제로 이 lint를 만들면서 다른 사람들도 사용해주었으면 하는 바람에 Vanilla Extract의 Github에도 이에 대한 discussion을 남겼는데 생각보다 커뮤니티가 활발하지는 않아 아무 답변이 없는 것이 아쉬웠다.
아무래도 국내에서 VE의 유즈케이스는 많지 않은 것도 아쉽기도 했고, 커뮤니티가 활발하지 않아 만약 Vanilla Extract의 사용을 다시한번 고민해야하는 순간이 온다면 그때는 굳이 이를 사용하지 않아도 괜찮겠다는 생각을 하게 되었다.
'개발 > React' 카테고리의 다른 글
eslint-lint-sprinkles-rule 제작기 - (2) sprinkles-rule 작성을 위한 배경 지식 (0) | 2025.02.16 |
---|---|
eslint-lint-sprinkles-rule 제작기 - (1) 문제인지 (0) | 2025.02.02 |
React 19 긍정 검토하기 (0) | 2025.01.19 |
Polymorphic Component를 정의하는 여러가지 방법 해체분석 (0) | 2025.01.05 |
(짧) Tree Shaking과 이를 사용하는 곳에서 해야할 일 (1) | 2024.12.03 |