최근 프로젝트에 Storybook을 도입하면서 덩달아 고민하게 되었던 것이 SCSS 변수를 JS에서 공유하는 문제였다.

혹자는 굳이 그런짓(?)을 왜 하려고 하느냐 할 수도 있겠으나, 현재 올려둔 Storybook Sampleutils 항목처럼 디자이너가 정의해 둔 색상, 글꼴 등을 일목요연하게 볼 수 있도록 정리해 두고 싶어서였다. (물론, 일이 그렇게 커질지는 알지 못했다 ㅋㅋ)

실패, 또 실패...

처음에는 SCSS에서 :export { ... }를 이용하는 방법도 생각해봤지만 변수를 일일이 두 번 작성하는 것도 불편할 뿐더러, map은 도무지 방법을 모르겠더라.

구글링에서 발견한 건 map을 수동으로 풀어서 작성하는 방법이었는데, nested map까지 있는 상황에서 이건 너무 비효율적인 방법이라 생각되어서 과감히 버리기로 했다.

그리고 다시 시도한 방법이 SCSS를 JS로 변환시키는 방법이었다. (이미 SCSS를 먼저 작성했기 때문에 이 방법을 우선으로 선택했다.) sass-to-js, sass-to-json, node-sass-json-impoter등 온갖 패키지들을 찾아보고 적용해보고 했으나... nested map이 계속 발목을 잡았다.
왜 때문에 다들 nested map은 고려가 안되어있는것인지... ㅠ

계속 된 실패에 다시 눈을 돌려 이번에는 js에 먼저 변수를 작성하고 SCSS로 변환시키는 방법을 찾아보기 시작했다. js-to-scss, json-to-scss, json-sass, json-to-sass-map 등을 찾아보고 적용해보고 했지만, 역시 내가 원하는 형태의 결과물을 만들어주는 패키지는 존재하지 않았다. 하아...

배열을 list로, object를 map으로, nested object를 nested map으로 변환시켜야 하는데 이 모든 조건을 만족하는 패키지를 찾기 어려웠고 그마나 지원하는 걸 겨우 찾으면 이번에는 무조건 root key가 만들어지는 그런...

그냥 만들자... 내가 만들자... ㅠ

처음에는 그나마 가능한 패키지에 기이한 형태(?)로 JSON 파일을 만들어 적용했다. (nested map이 정상적으로 출력되게 하기 위해 JSON key값을 이중 따옴표로 묶어버리는...)

당시에는 프로젝트 일정 상 고민할 시간이 없어서 그걸로 만족했지만, 이제는 시간이 많으니까(?) storybook 관련 포스팅하는 김에 결국 그냥 내가 만들기로 했다. 내 입맛에 맞는 패키지가 없으면 방법이 없다. 목마른 놈이 우물을 팔 수 밖에.

그나마 내가 생각하는 가장 근접한 형태의 패키지를 이눔아는 어떻게 만들었나 뜯어보고 참고해서 한 땀 한 땀 만들기 시작했다. 대략 4시간 정도(?)를 투자하니 당장 돌아가는 결과물은 나오더라. (이럴거면 처음부터 그냥 만들걸... 왜 패키지 찾고 설치하고 적용해보고 지우고 반복하느라 며칠을 날려먹은 것인지... ㅠ)

만들어놓고 storybook에 scss 변수로 정의한 값들을 불러다가 controls option으로 적용시키니 잘 된다. 이제야 만족스럽다.

Storybook controls에 SCSS map으로부터 만들어 낸 값을 선택상자 옵션으로 적용해냈다.
Storybook controls에 SCSS 변수 선택상자 옵션 적용

이걸 위해 그 많은 시간을 보냈구나 흑흑...

만들고 나니 사실 별거 없더라. 1depth key에 '$'를 접두사로 붙이고, 값들은 배열은 list로, 객체는 map으로, 그 외의 것들은 리터럴 값 그대로 처리하고 nested 된 것들을 처리하기 위해 재귀적으로 처리하는 방법이면 충분했다.

예를 들어, 다음과 같은 값을 변환시키면

const white = '#fff';
const text = {
  size: {
    base: '16px',
    large: '24px',
  },
  weight: {
    regular: 400,
    bold: 600,
  },
};

다음 코드를 가진 scss 파일이 생성된다.

$white: #fff;
$text: ('size': ('base': 16px,'large': 24px),'weight': ('regular': 400,'bold': 600));

물론, 별다른 옵션을 부여하지 않고 단순히 변환만 시키는 것이고 변수 값에 대한 정의만 처리하는 거라 굳이 들여쓰기 등은 고려하지 않아도 괜찮다고 생각했기 때문에 내 입맛에는 충분히 맞다 ㅋㅋ

코드 전체를 공개하기는 창피하니... 핵심 코드만 올려보면

const getSCSS = (entry) => {
  if (Array.isArray(entry)) {
    return `(${entry.join(', ')})`;
  } else if (entry instanceof Object) {
    const temp = reduceRight(entry, (acc, val, key) => {
      return `'${key}': ${getSCSS(val)},` + acc;
    }, '').replace(/,(\s|\S)?$/g, '');
    return `(${temp})`;
  } else {
    return entry;
  }
}

그냥 이 정도다. (정말로 단순하게 만들었...)

가장 잘 지원하지만 (옵션도 빵빵한?) root key가 무조건 생성되는 패키지 github에 나와 같이 root key를 제외시키는 방법을 묻는 사용자가 있었는데, 얼마전 거기에 혹 해결했는지를 묻는 코멘트를 달았더니 해당 패키지 주인(?)이 이 요구사항을 받아들이는 걸 고민해보겠다고 답글이 달렸다!! 만쉐!!

그나저나... 이번에 포스팅 한 것들 회사 프로젝트에도 역으로 다시 적용해야 하는데... 이건 또 언제하나... 하하...