본문 바로가기

개발/React

eslint-lint-sprinkles-rule 제작기 - (2) sprinkles-rule 작성을 위한 배경 지식

반응형

https://www.svgrepo.com/svg/374153/vanilla-extract

목차

 

  1. eslint-lint-sprinkles-rule 제작기 - (1) 문제인지
  2. eslint-lint-sprinkles-rule 제작기 - (2) sprinkles-rule 작성을 위한 배경 지식
  3. eslint-lint-sprinkles-rule 제작기 - (3) sprinkles-rule 내부를 구성하는 코드 설명

 

 

서론

 

 

저번 eslint-lint-sprinkles-rule 제작기 시리즈의 2편, sprinkles-rule 작성을 위한 배경지식을 작성하려고 한다.

 

이전 글에서 어떤 부분이 문제여서 rule 작성을 결심했는지에 대한 배경을 설명했다면 이번에는 실제로 어떤 방식으로 이를 해결하려고 했는지에 대한 설명을 하려고 한다.

 

다음 글에서 실제 코드에 대한 설명을 진행하려고 하는데 그 전에 전반적인 흐름을 잡는 글이라고 보면 된다.

 

기존에 작성했던 흐름에 이어서 그대로 작성해본다.

 

 

 

본론

 

먼저 eslint rule을 만들려고 했기때문에 eslint가 동작하는 방식을 간단하게 보자.

 

eslint는 실제로 코드에 문제가 있는지 없는지를 AST(Abstract Syntax Tree) 라는 추상 구문 트리를 통해 체크한다.

 

각 라인(node)에 대한 값을 받아서 이를 토대로 조건을 체크(report)하고, 한걸음 더 나아가 문제 상황에서 lint를 통해 어떤 식으로 변경시킬지를 정의(fix)할 수 있다.

 

간단하게 console.log('hello world') 를 AST로 변경하면 어떤 방식으로 나오는지 확인해보자.

 

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "hello world",
            "raw": "'hello world'"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

 

 

보면 일반적으로 호출 구문에 대한 표현을 나타내는 트리가 위와 같이 나타나는데 기본적으로 typeExpressionStatement로 나타나고, 표현의 타입이 CallExpression, 호출한 callee의 type이 MemberExpression이라는 점을 볼 수 있다.

 

또한 argument에 대한 값도 Literal type으로 정의되고 이에 대한 Value가 적어둔 hello world로 나타나는 것을 볼 수 있다.

 

자세한 내용은 https://astexplorer.net/ 에서 실제 파서별로 어떤식으로 AST가 나타나는지 확인해볼 수 있기때문에 궁금하다면 해당 홈페이지를 확인해보길 바란다.

 

 

 

이렇게 변환된 AST를 ESLint에서는 직접 접근해서 체크 및 변환이 가능한 것이다.

 

다만 ESLint에서는 이런식으로 depth가 너무 깊은 케이스를 체크하기 용이하도록 rule을 create할 때 특정 Expression을 기준으로 접근이 가능하다.

 

위의 예시를 바탕으로 보자면 ExpressionStatement로 접근하면 1뎁스를 기준으로 정리해주고, CallExpression으로 접근하면 2뎁스를 기준으로 정리해주며, MemberExpression으로 접근하면 3뎁스로 접근한 트리를 가지고 체크를 할 수 있게 해준다.

 

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement", // 1 depth
      "expression": {
        "type": "CallExpression", // 2 depth
        "callee": {
          "type": "MemberExpression", // 3 depth
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Literal",
            "value": "hello world",
            "raw": "'hello world'"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

 

 

// ex. 2 depth로 접근하려고 한다면 아래와 같이 작성하면 된다.

create(context) {
  return {
    CallExpression(node) {
      // ...
    }
  }
}


// 위와 같이 접근하면 node는 CallExpression부터 체크한다.


// node에 넘어오는 값
{
  "type": "CallExpression",
  "callee": {
    "type": "MemberExpression",
    "object": {
      "type": "Identifier",
      "name": "console"
    },
    "property": {
      "type": "Identifier",
      "name": "log"
    },
    "computed": false
  },
  "arguments": [
    {
      "type": "Literal",
      "value": "hello world",
      "raw": "'hello world'"
    }
  ]
}

 

 

이 글에서 중요하게 다룰만한 부분은 아니기때문에 AST에 대한 설명은 여기까지 하겠다.

 

 

AST를 다루는 방법은 간단하게 알아보았으니 실제 Sprinkles라는 것을 어떤 룰로 정리하려고 했는지를 먼저 케이스별로 나눠보겠다.

 

 

문제 정리

 

 

Vanilla Extract에 대한 배경지식이 없는 사람도 있을 수 있기에 간단하게만 설명하고 진행해보려고한다.

 

Vanilla Extract에서는 기본적으로 스타일을 선언할 때 VE에서 제공하는 style을 import하여 사용한다.

 

그리고 이전 글에서 설명한 바와 같이 sprinkles라는 문법적 설탕에 정의해두는 경우에는 sprinkles를 import하여 이 안에 선언해둔 style을 사용하면 된다.

 

또한 특정 style set을 만들어두고 이를 import해서 사용할 수 있으며, utility style과 sprinkles, style을 결합해서 사용하는 경우 style 의 인자를 배열로 하여 넣어주는 방식을 사용하면 된다.

 

// 일반 style 선언 시
const test = style({
  display: 'flex'
})

// 이런식으로 utility style을 미리 선언해두고 이를 그대로 활용할 수 있다.
export const flexCenter = sprinkles({
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
});

// sprinkles와 같이 사용하거나 미리 선언한 변수를 함께 사용하는 경우 배열로 감싼다.
const mergeTest = style([
  flexCenter,
  sprinkles({
    display: 'flex'
  }),
  {
    lineHeight: 1.3
  }
])

 

 

또한 Package로 제공하는 Recipe라는 유틸리티 함수가 존재하는데 이는 특정 케이스를 미리 정의해서 해당 값일 경우 정의된 스타일을 보여주는 역할을 하는데 이는 기존 styled-components와 같은 css-in-js에서 prop으로 값을 받아 조건부 스타일링을 수행하는 상황에서 많이 사용한다.

 

 

// 공식문서 출처 : https://vanilla-extract.style/documentation/packages/recipes/#recipe

import { recipe } from '@vanilla-extract/recipes';
import { reset } from './reset.css.ts';
import { sprinkles } from './sprinkles.css.ts';

export const button = recipe({
  base: [reset, sprinkles({ borderRadius: 'round' })],

  variants: {
    color: {
      neutral: sprinkles({ background: 'neutral' }),
      brand: sprinkles({ background: 'brand' }),
      accent: sprinkles({ background: 'accent' })
    },
    size: {
      small: sprinkles({ padding: 'small' }),
      medium: sprinkles({ padding: 'medium' }),
      large: sprinkles({ padding: 'large' })
    }
  },

  defaultVariants: {
    color: 'accent',
    size: 'medium'
  }
});

 

 

 

그렇다면 체크해야하는 상황은 style을 사용할 때와 recipe를 사용할 때, 이 두가지 케이스라고 볼 수 있다.

 

저 두가지 케이스에서 최초 목적이던 sprinkles에 정의되어있는데 style에서 사용하고있는 케이스를 체크하고, 이를 원하는 포맷으로 재정리해주는 것이다.

 

 

 

먼저 Style을 사용하고있는 케이스라면 체크해야할 것과 그에 따라 해야할 액션은 아래와 같다.

 

 

  1. 단순히 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 배열을 유지한다. 
  2. 실제로는 sprinkles만 사용하는데 style array로 감싸져 있는 경우
    • 이런 케이스는 일반적으로는 잘 발생하지 않지만, 특정 케이스에서 스펙이 변경되어 style([])로 감싸져있었지만 특정 스타일이 사라져 sprinkles만 남는 경우가 있기때문에 이를 린트에서 체크해주고 Sprinkles만 남아있는 경우 감싸고있는 style 배열을 제거해준다.

 

 

또한 이와 유사한 맥락으로 recipe를 사용하는 경우에도 체크해주어야한다.

 

다만 recipe에서 기본 스타일을 정의하는 base 에서는 2개 이상의 인자가 들어오는 경우 style로 감싸지 않고 단순한 배열로 감싸주어야한다.

 

 

 

위에서 설명한 recipe의 예시 케이스를 자세히 보면 알 수 있다.

 

 

그렇다면 recipe를 처리할 때에는 어떻게 해야할지 보자

 

  1. recipe의 base가 배열로 감싸져 있는 경우
    • 배열 내의 style object 내에 있는 스타일 중 일부가 sprinkles에 있는 경우 : 기존의 형태를 유지하면서 style object에 있는 일부 스타일만 sprinkles로 이동한다.
    • 배열 내의 style object 내에 있는 스타일 중 모든 스타일이 sprinkles에 있는 경우 : 기존 형태를 유지하면서 style object를 제거하고 style object에 있던 모든 스타일을 sprinkles로 이동한다.
      • 변환 후 style 배열 내에 sprinkles만 남은 경우 : 감싸고있던 대괄호를 제거하고 sprinkles 만 남겨준다.
      • 변수와 같이 사용하고 있는 경우 : 변수는 그대로 두어야하기때문에 나머지는 그대로 두고 감싸고있던 형태가 style 배열인지 체크하고 style 배열이라면 일반 배열로 만들어준다.
  2. 나머지는 모두 정상이지만 recipe의 base에 style로 감싸져 있는 경우
    • 일반적으로 이렇게 작성해도 정상적으로 동작하지만, 내가 만든 룰에서는 통일성을 위해 recipe의 base에 2개 이상의 인자가 존재한다면 이는 반드시 배열로만 감싸야한다. 따라서 기존에 여러 사람이 작업하면서 파편화되어있는 케이스를 하나로 모아준다.

 

 

 

또한 recipe에는 조건부로 스타일을 정의하는 variants가 존재하는데 전반적인 변환 방식은 style 방식과 동일하다.

 

하지만 이는 위에서도 볼 수 있지만 variants의 value로 바로 스타일이 나오는 것이 아니라 몇번의 뎁스를 거쳐 실제 스타일이 정의되기때문에 이에 대한 처리가 필요하다.

 

 

 

 

 

마치며

 

 

이번 글에서 원래 남은 내용을 모두 설명하려고 했지만 배경 설명도 어느정도 필요했기 때문에 다시한번 글을 나누게 되었다.

 

다음 글에서는 실제 이를 어떻게 코드로 구현했는지 설명하도록 하겠다.

반응형