자 이제 물과 기름같은 이 두 패키지를 어떻게 중매시켜줄까 라는 실질적인 고민을 할 시점이다.
우선 part1 에서 말했던 초기 디자인은 다음과 같다.
그리고 이 네가지 아이디어로 등장한 api는 다음과 같다.
const box = tailwind({
display: 'flex',
alignItems: 'items-center',
justifyContents: 'justify-center',
backgroundColor: 'bg-gray-50',
'@dark': {
backgroundColor: 'dark:bg-gray-900'
}
})
또한 tailwind에서 좀 귀찮은 부분이 있는데 그것은 nest 조건을 prefix로 매번 붙여줘야 한다는 것이다.
예컨데 dark
모드에서 borderColor
와 backgroundColor
를 바꾸고 싶으면 dark:bg-xxx dark:border-xxx
와 같이 적어 줘야 하는데, 깊이가 깊어질수록 가독성 장난 없다.
그래서 5번째 조건으로 nest 조건을 object
로 묶어 응집성을 올려보자는 목표를 잡았다.
당시 기억으로 분명히 일은 벌렸는데 어떻게 할지 사실 진짜 감이 전혀 안왔다. 그럼에도 단 한가지는 명확했는데, 구상한 api로 tailwind를 사용하면 진짜 맛집이 오픈 될 것을 알고 있었다는 점이다. 이에 wimhoff 호흡법으로 우선 마음을 추스르고 필요한 기능을 최소 단위로 쪼개보기로 했다.
분할 정복으로 느낌으로 생각을 해보니, 우선 tailwind에 대한 타입이 존재해야 한다는 사실을 알게 되었다(당연한 소리).
예컨데 css property와 tailwind 속성이 일대일 대응된 타입같은 것을 의미한다.
type Tailwind = {
backgroundColor: "bg-red-100" | "bg-red-200" | ...
borderColor: "border-red-100" | "border-red-200" | ...
}
들뜬 마음에 베프 구글과 덕덕고에게 물어보니 걔네가 던져주는 것은 덧없는 tailwind.config.js
의 타입 밖에 없었다. tailwind의 설정 타입밖에 없다는 사실은 이걸 직접 만들라는 의미였다.
/** @type {import('tailwindcss').Config} */
// 아 이거 아닌데...
(확률과 통계는 이렇게 푸는 것이다)
강력한 영감을 받고 곧바로 tailwind 공식 문서로 들어가서 우선 복붙 하기로 마음 먹었다.
그러나 이것을 무턱대고 시작했다가는 지문이 먼저 사라질 거라는 확신이 들었다. 왜냐하면 tailwind 공식문서의 property 소개 페이지는 157페이지이기 때문이다.
조금만 뇌를 사용해보자는 생각에 우선 tailwind가 세운 property 이름의 패턴에 대해 분석해보기로 했다.
조사해보니 일관된 규칙을 발견할 수 있었다.
tailwind는 accent
, color
, spacing
3가지를 property를 모든 color와 spacing 관련 css 속성에서 공유하고 있다.
accent
, color
spacing
여기서 color 관련 속성이란 backgroundColor
, borderColor
와 같은 css 속성을 의미하고 sizing은 width
, height
, padding
과 같은 요소의 크기 그리고 위치 등을 결정 짓는 css 속성을 뜻한다.
bg-red-500/50
라는 스타일을 분석해보자.
이는 크게 2가지 영역으로 구분할 수 있다.
prefix
: 어떤 스타일을 적용하는 건가color
: color
+ accent
+ opacity
그리고 이 color의 카테고리를 color, accent, opacity로 분류한 다음, 다시 하나로 조합하면 모든 color의 집합을 얻을 수 있는 것이다.
// color 속성 모음
type TailwindColor =
| "slate"
| "gray"
| "neutral"
| "stone"
| "red"
| "orange"
| "yellow"
| "lime"
| "amber"
| "green"
| "teal"
| "blue"
| "indigo"
| "sky"
| "cyan"
| "emerald"
| "violet"
| "fuchsia"
| "pink"
| "rose"
| "purple"
| "inherit"
| "current"
| "transparent"
| "black"
| "white"
// color 강조(진한 정도) 속성 모음
type TailwindColorAccent =
| "50"
| "100"
| "200"
| "300"
| "400"
| "500"
| "600"
| "700"
| "800"
| "900"
| "950"
// color 투명 속성 모음
type TailwindOpacity =
| "0"
| "5"
| "10"
| "15"
| "20"
| "25"
| "30"
| "35"
| "40"
| "45"
| "50"
| "55"
| "60"
| "65"
| "70"
| "75"
| "80"
| "85"
| "90"
| "95"
| "100"
그리고 앞서 말했듯이 모든 color는 3가지 타입의 조합이다.
type TailwindColor = `${TailwindColor}-${TailwindColorAccent}/${TailwindOpacity}` | `${TailwindColor}-${TailwindColorAccent}
// red-50 | red-100/10 | red-200/20 ...
TailwindColor
를 선언하는 과정에서 template literal의 매직이 돋보인다. 이 template literal은 엄청난 잠재력을 지녔으니 반드시 응용 기법을 알아보기를 권장한다.
(토스 블로그에 굉장히 굉장한 글이 있으니 이것 또한 참고하길 바란다)
공통 구조를 추출할 수 있는 property를 제외하고 나머지는 임의의 이름으로 지어졌기 때문에 이 부분은 노가다를 해야한다.
예컨데 shadow
라는 속성의 경우 shadow
라는 prefix와 sm
, md
, lg
등의 size로 이루어져있다.
여기서 shadow의 size는 모든 속성에서 공유되는 단위가 아니기 때문에 이 부분은 복붙을 진행하면 된다.
(태연하게 적었지만 고통스럽다)
규칙과 패턴을 통해 꽤 간소화 할 줄 알았으나 막막하긴 했다. 그래서 좀 vscode
의 도움을 받기로 했는데 그것은 바로 code snippets이다.
code snippets란 미리 적어둔 코드 매크로이다.
.vscode
에 [매크로_파일_이름].code-snippets
를 만들고 다음 명령어를 넣어준다.
{
"원하는 타이틀": {
"scope": "typescript",
"prefix": "twt",
"body": [
"import { PlugBase, Pluggable } from '../plugin'",
"import { TailwindArbitrary } from '../tailwind.common/@arbitrary'",
"",
"type Tailwind$1Variants<Plug extends PlugBase = ''> = '' | '' | '' | '' | '' | '' | Pluggable<Plug> | TailwindArbitrary",
"type Tailwind$1<Plug extends PlugBase = ''> = `$2-${Tailwind$1Variants<Plug>}`",
"export type Tailwind$1Type<Plug extends PlugBase = ''> = {",
" /**",
" *@description $5",
" *@see {@link https://tailwindcss.com/docs/$3 $3}",
" */",
" $4: Tailwind$1<Plug>",
"}"
],
"description": "easy type def with twt command"
},
}
여기서 prefix
로 적어준 것이 명령어다.
이제 새롭게 만든 .ts
파일에 twt
만 치면
import { PlugBase, Pluggable } from '../plugin'
import { TailwindArbitrary } from '../tailwind.common/@arbitrary'
type TailwindVariants<Plug extends PlugBase = ''> = '' | '' | '' | '' | '' | '' | Pluggable<Plug> | TailwindArbitrary
type Tailwind<Plug extends PlugBase = ''> = `-${TailwindVariants<Plug>}`
export type TailwindType<Plug extends PlugBase = ''> = {
/**
*@description
*@see {@link https://tailwindcss.com/docs/ }
*/
: Tailwind<Plug>
}
이런 파일이 생성되고, $1, $2, $3
의 숫자에 맞춰서 필요한 내용을 한개씩 채워주면 되는 것이다.
여기서 잠깐. "
@see
@link
@description
이게 뭐냐?" 라고 한다면 JSdoc 고급 주석 스킬을 읽어 보는 것을 추천한다. js 고수처럼 만들어주는 간교한 기술이기 때문에 익혀두면 여러모로 쓸모가 많다.
이런 방식으로 tailwind에 대한 타입을 정의하면 vscode에서 이렇게 보인다.
@description
에는 tailwind 문서에 적힌 sub 타이틀을 적어주고 @see
로 곧바로 해당 속성에 대한 tailwind 사이트로 이동할 수 있게 만들어준다.
이게 진짜 꿀단지 그 자체다.
혹시 css 속성은 아는데 tailwind에서 어떻게 쓸 지 기억이 안나는 상황이 있지 않았는가? 그때 든든한 백과사전 있다 생각하고 알고 있는 css 속성 적으면 공식문서 읽으러 들어갈 수 있다.
사실 퀘스트1만 마쳤을 시점에도 굉장히 쓸만해서 이정도만 할까라는 생각을 했었다. 그럼에도 불구하고 멈출순 없었다.
왜냐하면 초반에 nest로 응집성을 올리겠다는 야심찬 목표가 있었기 때문이다. 한번 예시를 살펴보자.
const 자동지원_하세요 = tailwind({
dark: {
backgroundColor: "dark:bg-teal-950",
borderColor: "dark:border-teal-800",
hover: {
backgroundColor: "dark:hover:bg-teal-900",
borderColor: "dark:hover:border-teal-600",
},
},
})
dark
와 dark:hover
에 대한 조건을 nested object 형식으로 묶은 가상의 코드이다. 문제는 나는 현재 tailwind에 대한 타입을 각각의 nest 깊이 까지는 만들어 주지 않았다는 점이다.
즉 type Tailwind
로는 dark
, hover
, active
와 같은 nest상태에서의 className을 자동완성 받을 수 없다.
type TailwindDark = Tailwind["@dark"]
type TailwindHover = Tailwind[":hover"]
type TailwindActive = Tailwind[":active"]
// 그런거 없는데요?
이젠 초반 노가다 부분이 오히려 그리워지기 시작했다. 그것은 주황버섯과 같은 잡몹이었던 것이다. 그런데 필자는 이게 왜 문제라고 주장하는 걸까? 쉬운데 생색내는 것 아닐까?
우선 케이스를 나누어 생각해보자. 아마 tailwind를 사용하는 사람은 무조건 tailwind intellisense 확장을 사용하고 있을 것이다(없다면 당장 깔아야한다.). 그리고 만약 nest condition을 중첩시켜 사용하고 싶다면, 설치한 확장이 계속해서 tailwind code를 제안해준다.
여기서 중요한 질문은 dark:hover:required:first-letter:first-line:marker:selection:file:placeholder
와 같이 무한 중첩된 property를 자동완성 할 수 있는가 라는 것이다. 만약 기존과 같은 방식으로 정의하고자 한다면 모든 nest condition의 조합에 대해서 만들어 질 수 있는 className을 일일이 정의해줘야 할 것이다.
type Tailwind = { ... }
// 지옥 오픈
type TailwindDark = { "dark:... -> 를 모든 property에 추가 " }
type TailwindDarkHover = { "dark:hover:... -> 를 모든 property에 추가" }
type TailwindDarkHoverRequired = { "dark:hover:required... -> 를 모든 property에 추가" }
shxxt
만약 모든 nest condition의 조합에 대해 타입을 이전처럼 노가다로 지원하고자 한다면, 기존에 정의한 Tailwind
타입의 property에 nest 조건을 추가적으로 적어줘야 할 것이다. 과연 이걸 일일히 정해줄 수 있을까?
정답은 불가능하다. 우선 저 많은 것에 대한 조합을 직접 만들기도 어려울 뿐더러 만약 tailwind에서 한가지 속성만 추가해도 조합의 숫자가 기하급수적으로 늘어나기 때문에 유지 보수성 또한 좋지 않다.
즉 타입의 조합은 구성 갯수가 많아질 수록 기하급수적으로 계산량이 는다.
예컨데 과일을 평가하는 기준이 있다고 생각하자.
type 과일 = "사과" | "바나나" | "딸기" | "망고"
type 평점 = "매우별로임" | "별로임" | "주면먹지" | "한번씩생각나요" | "없으면나주거"
여기서 과일 선호도에 대한 모든 조합을 얻고 싶다. 그럼 어떻게 하면 될까?
전에 사용했던 string literal를 사용하면 된다.
type 과일선호도 = `${과일}-${평점}`
현재는 4 x 5
, 즉 20개의 조합 밖에 없는 상황이지만 만약 과일 평가 조건 N개로 늘어버리면 어떻게 될까? M x N x P
이 될 것이다. 그럼 임의의 숫자에 대해서는 어떻게 될까? 진짜 엄청나게 큰 숫자가 될 것이다.
타입을 선언하면 타입스크립트는 매번 타입을 검증하고 각각의 연산을 수행해야 한다. 즉 union 타입 수에 비례하여 발생가능한 조합의 수는 M x N x P
… 와 같이 기하급수적으로 증가하고, 그만큼 타입스크립트 서버는 매번 수많은 연산을 진행해야 한다.
실제로 tailwind nested property에 대해 3중 union 타입을 합성해보면
type TailwindNested = "hover" | "active" | "after" | ...
type Combinations<T extends string, Depth extends 2 | 3> = Depth extends 2
? `${T}${T}`
: `${T}${T}${T}`;
export type TailwindTrippleNested = Combinations<TailwindNested, 3>;
error
TS2590
: Expression produces a union type that is too complex to represent식에서는 너무 복잡해서 표시할 수 없는 공용 구조체 형식을 생성합니다.ts(2590)
TS2509
에러를 뱉고 전사해버리는 타스를 볼 수 있다. 그것도 단 3개의 조합에 대해서만 연산을 진행한 것이다. 또한 tailwind의 nest condition은 순서가 상관없는 중복순열로 계산되므로 M^N
의 꼴로 복잡도가 빠르게 증가한다.
즉 조합으로는 불가능한 것이다.
조합은 파멸의 지름길임을 간단한 구현을 통해서 알아봤다.
이제는 어떻게 할까라는 질문보다는 애초에 이것이 타입스크립트로 가능한가라는 질문에 도달 했었다. 괜한 도구를 탓하고 있는 것이다. 그때 nest의 조합이라는 부분에 너무 집중하고 있는게 아닌가 라는 생각이 들었다.
사실 조금 더 근본적인 기능에 대한 질문은 다음과 같다.
nested 조건이 적용되는 child의 object의 property에 parent의
key
를 추가하면 되지 않을까?
즉 @dark
로 묶인 child의 backgroundColor
라는 property 바로 앞에 dark:
를 자동으로 추가시키고, 또 :hover
로 묶인 nested child에도 동일한 방식으로 진행하는 것이다. 이때 중요한 것은 위 그림에서 :hover
와 같이 중첩된 child의 경우 이전 부모의 key(@dark
)또한 받아야 한다는 점이다.
다시말해 이전에 미리 nest의 조합을 계산하고자 한것과는 달리, 조합을 미리 정의하지 말고 generic으로 type을 받아 매번 계산하도록 유도하자는 것이다.
이렇게 되면 이전에 모든 발생 가능한 킹우의 수를 미리 연산할 필요가 없기 때문에 성능적 이점이 있다. 즉 현재 접근한 nested property에 대한 계산을 수행하므로 타입스크립트의 인권을 보장할 수 있는 것이다.
자, 이제 전두엽을 데굴데굴 굴려볼 시간이다. 이제 만들 것은 Tailwind 타입을 input으로 받고 output으로 모든 nest condition에 접근 가능한 새로운 Tailwind 타입을 만들어주는 것이다.
그럼 간단하게 현재 가지고 있는 조건을 도식화 해보자.
만들어야 하는 것을 한줄로 설명하면, Tailwind
타입을 input으로 받고, output으로는 nest condition이 자동 계산된 타입을 던져주는 <타입 함수>를 만들면 되는 것이다.
우선 타입이 어떤 값을 미리 지정한다는 것에 벗어나서, 어떤 input을 목표하는 바의 output으로 변환시키는 것에 있다는 점에 집중하자.
그리고 타입스크립트에서 input과 output의 연산 에 가장 적합한 것은 바로 generic이다.
제네릭이란 타입을 input 으로 받을 수 있게 만들어주는 마법의 도구이다.
흔히 쓰는 타입을 생각하면, 정의된 상태가 변경되지 않기 때문에 유연성이 떨어진다는 단점이 있다. 그리고 이는 꽤 불필요한 작업을 동반하기도 한다.
예시를 살펴보자.
어떤 값이 들어오면 그 값의 타입을 유지하면서 그냥 배열로 만들어주는 함수에 대한 타입을 만들어보자.
type TransformNumberToNumberArray = (target: number) => number[]
type TransformStringToStringArray = (target: string) => string[]
number
와 string
에 대해 이렇게 타입으로 정의해줄 수 있다.
지금 보면 인자의 타입은 다르지만 구조 자체는 반복되는 것을 확인할 수 있다. 이렇게 반복되거나 인자를 통해 더욱 유연하게 타입을 정의하고 싶을 때, generic을 사용하면 된다.
type TransformToArray<Target> = (target: Target) => Target[]
<Target>
은 generic, 즉TransformToArray
의 타입 input이다.
이제 generic의 유용성을 알게 되었다. 타입을 인자로 받게 돕는 것. 그것이 진짜 가치이다.
generic이 타입 "함수"의 인자라는 것을 이해했다면 그 다음부터는 쉬워진다. 왜냐하면 함수의 인자와 동일한 기능을 수행할 수 있기 때문이다.
우리가 타입스크립트를 사용하는 이유는 들어올 타입을 제한하기 위해서다.
const add = (first: number, second: number) => first + second
덧셈 함수에 number로 인자를 제한한 것처럼 generic, 즉 인자로 들어올 타입의 타입(?) 또한 제한할 수 있다.
type Add<First extends number, Second extends number> = `${First}${Second}`
type Twelve = Add<1, 2>
type CantDoThis = Add<1, "2">
자 이제 Twelve
타입은 12
(타입은 더하기 못한다). 그러나 "2"를 인자로 준 CantDoThis
의 타입은 타입스크립트가 에러를 뱉는다.
여기서 제한의 의미를 다시 곱씹어 보면, 이걸 분기처리의 조건으로 활용할 수 없을까라는 의문이 든다. 그렇다 인자 또한 제한이 가능하며, 이 제한 조건으로 분기 처리를 할 수 있다.
type Add<First, Second> = First extends string | number ? Second extends string | number ? `${First}${Second}` : never : never
type Twelve = Add<1, 2>
type CantDoThis = Add<1, "2">
만약 First라는 인자가 string
혹은 number
라면 그리고 Second라는 인자가 string
혹은 number
라면 ${First}${Second}
를 반환하라.
이를 가상의 코드로 표현하면 다음과 같다.
const add = (
first: string | number | boolean,
second: string | number | boolean
) => {
if (typeof first === "string" || typeof first === "number") {
if (typeof second === "string" || typeof second === "number") {
return `${first}${second}`
}
return "never = 이건 진짜 안된다는 의미"
}
return "never = 이건 진짜 안된다는 의미"
}
즉 ?
로 타입의 인자에 대해 조건 처리가 가능하다.
이제 받은 인자를 활용해보자.
예컨데 방금 전 처럼 string
, number
와 같은 원시 타입이 들어가면 바로 그 값을 사용해주면 된다.
그렇다면 오브젝트 타입은 어떨까?
// 한국 최고
type GetKoreanHellow<
Greetings extends {
korea: string
japan: string
china: string
america: string
},
> = Greetings["korea"]
type 안녕 = GetKoreanHellow<{
korea: "좀 할맛 나냐?",
japan: "gg",
china: "ni",
america: "hi"
}>
// "좀 할맛 나냐?"
자 타입 인자로 Greetings
라는 object
형식의 타입 변수가 들어오고, 반환 값은 Greetings
인풋 값에서 korea
라는 속성이다. 그리고 object 타입에서는 Greetings["korea"]
같은 방식으로 속성에 접근할 수 있다.
이와 같이 generic 인자로 구조화된 타입이 들어온다면, 우리는 그 중 미리 정의된 값을 가져올 수 있다.
여기서 마법의 keyof
키워드와 함께 사용하면 들어온 타입 구조의 key를 추출할 수도 있다.
type GetObjectKeys<SomeObject> = keyof SomeObject
type AandB = GetObjectKeys<{
a: string
b: string
}>
// "a" | "b"
꽤나 유용한 기술이므로 알아가면 도움이 된다.
오늘의 하이라이트다. 결국 generic이 타입에 인자를 추가한다는 말을 곱씹어보면, 타입이 함수가 되었단 말이다.
그렇다는 것은 함수의 재귀 호출과 마찬가지로 타입 또한 재귀적으로 호출하여 어떤 연산을 시킬 수가 있다.
그리고 타입스크립트는 다른 강타입 언어와 달리 string
을 구체적으로 지정할 수 있다.
이 3가지 특성으로 우리는 타입을 string 계산 기계로 진화시킬 수 있다.
(위 3가지 기능을 모두 활용하면 wordle게임 같은 것도 만들수 있다.)
자 이제 다시 돌아와서 목표를 다시 곱씹어보자.
input으로 Tailwind
라는 타입을 받고, output으로 어떤 타입
을 다시 만든다는 것은 앞서 학습한 지식에 따르면 generic을 사용해야 한다는 의미이다.
이제는 구현의 시간이다.
우선 퀘스트 1에서 정의한 Tailwind
를 받고, 모든 nest 조건(hover, active, before...)을 인자로 받는 타입을 정의해보자.
type TailwindWithNest<AllNestConditions extends string, Tailwind> = {}
이제 모든 nest condition을 받고 이를 Tailwind
의 key에 추가해주자.
type TailwindWithNest<AllNestConditions extends string, Tailwind> = {
[NestedKey in AllNestConditions]?: Tailwind
} & Tailwind
이렇게 하면 다음과 같이 사용할 수 있다.
// 가상의 tailwind
type Tailwind = {
bgColor:
| "bg-red-100"
| "bg-red-200"
| "bg-red-300"
| "bg-red-400"
| "bg-red-500"
}
type AllNestCondtions = "hover" | "active"
type TailwindNested = TailwindWithNest<AllNestCondtions, Tailwind>
const ex: TailwindNested = {
active: {
bgColor: "bg-red-100",
},
hover: {
bgColor: "bg-red-400",
},
}
첫번째 인수로 모든 가능한 nest 조건에 대한 목록을 넣어주고 두번째 인수로는 실제 Tailwind
타입을 넣어주면 기존에는 없었던 hover과 active에 대한 스타일이 스타일이 추가된다.
그런데 지금 문제는 hover
의 children object의 value는 여전히 동일하다는 것이다. 이제 이 부분을 좀 고민해보자.
generic은 함수이며 재귀적으로 호출할 수 있다는 것을 알고있다.
그리고 현재 문제 상황을 종합하면 다음과 같다.
즉 이전에 접급했던 parent object의 key를 저장해주는 변수가 필요한 것이다.
type TailwindWithNest<AllNestConditions extends string, Tailwind, ParentNestCondition extends string = ""> = {
[NestedKey in AllNestConditions]?: Tailwind
} & Tailwind
이전의 상태를 기억해야 하므로, ParentNestCondition
이라는 변수를 만들어주자. 그리고 이전에 접근했던 parent object의 key를 저장해주는 로직을 만들면 될 것 같다.
type DEFAULT_VALUE = ""
type TailwindWithNest<
AllNestConditions extends string,
Tailwind,
ParentNestCondition extends string = DEFAULT_VALUE,
> = {
[CurrentNestCondition in AllNestConditions]?: TailwindWithNest<
AllNestConditions,
Tailwind,
`${CurrentNestCondition}:${ParentNestCondition}`
>
} & Tailwind
(여기서 가운데에 :
를 넣어준 이유는 tailwind에서 nest를 엮을 때 사용하는 기호이기 때문이다.(md:hover:dark ...
))
조금 코드가 복잡해졌지만, ${CurrentNestCondition}:${ParentNestCondition}
에서 현재 nest 조건을 다음 연산에서 사용할 수 있도록 넣어준다는 점을 기억하자.
이제 접근했던 parent object의 key를 저장해줬다. 그러나 아직 ParentNestCondition
변수를 사용하고 있지는 않다. 이제 이 값을 활용해서 children의 property에 저장한 parent의 key를 넣어주는 함수를 따로 만들어보자.
type AddParentNestedConditionToChildren<
Tailwind,
ParentKey extends string = DEFAULT_VALUE,
> = {
[Key in keyof Tailwind]: Tailwind[Key] extends string
? ParentKey extends DEFAULT_VALUE
? Tailwind[Key]
: `${ParentKey}${Tailwind[Key]}`
: Tailwind[Key]
}
AddParentNestedConditionToChildren
함수는 ParentKey
를 인자로 받아서 children의 properties에 value를 넣어주는 함수이다.
여기서 눈여겨 볼 점은 Tailwind[Key]
가 string
인 경우로 분기처리를 했다는 점이다.
const ex = {
bgColor: "bg-red-100",
active: {
bgColor: "active:bg-red-100",
},
}
원하는 값을 다시한번 확인해보자.
ex.bgColor
와 같이 string
이라면, 우리는 parent의 key를 넣어줌.ex.active
와 같이 object의 값이 또 object라면 스킵. 그래서 Tailwind[Key]
, 즉 현재 object의 값의 타입이 string
이라면 ${ParentKey}${Tailwind[Key]}
로 parent key를 추가하고
아니라면 object 타입이므로 Tailwind[Key]
를 그대로 사용하라는 의미이다.
(ParentKey extends DEFAULT_VALUE
로 또 조건을 건 이유는, 첫번째 ParentKey
의 기본값으로 DEFAULT_VALUE
넣어주고 있다는 사실을 기억하면 된다. 즉 만약 ParentKey
가 DEFAULT_VALUE
라면 아직 깊이가 1인(=중첩되지 않은) 상태이므로 ParentKey를 넣어줄 필요가 없다.)
자 이제 테스트 해보자.
계산대로 매번 parent의 Key가 함수의 호출로 계속 저장되고, 그걸 사용하는 모습이다.
드디어 웃을 수 있다. :)
사실 스무스한 논리적 전개 과정을 거친것 처럼 글을 작성했지만, 그냥 이것은 나의 오랜시간(거의 몇백 시간)의 고민과 논리를 함축해서 설명한 것으로 보면 된다. 그러니 혹여나 전개과정을 따라오지 못했더라도 그것은 글의 문제이므로 필자를 욕하면 된다.
개구리 건배
만약 프로덕션 버전으로 리팩토링 된 코드를 돌려보고 싶다면 여기를 누르면 된다. ts playground에서 핵심적인 부분을 구현해뒀다.
처음에는 막막함에 사로잡혀 있었는데 다양한 라이브러리와 많은 개발자 분들이 써준 글, 무엇보다 이걸 하고자 하는 강력한 의지가 있으니 할 수 있었다. 지레 겁을 먹기 보단, nike의 문구처럼 Just do it 정신도 중요하다는 것을 몸소 체험했다.
특히 이번 프로젝트를 하면서 많은 점을 깨달았는데, 규모의 크고 작음과 상관 없이 다른 사람이 사용할 수 있는 실제 제품을 만들고 배포하고 유지하는 것은 정말 갚진 일이란 점이다. 누군가 나의 코드를 사용하고 좋았다고 말해주면 개발자로써 그 뿌듯함은 말로 이룰 수 없다.
또한 운좋게 velog 상단 인기글에 올라갔는데, 그것 또한 너무 감사하다.
다들 재밌게 읽어주셨으면 하는 마음에 무리한 드립이 포함되어 있는데 하는데 불편하다면 양해를 부탁드리고 조심스레 자가 모자이크를 부탁드린다.
진짜 마지막으로 이 모든 고민이 종합된 그리고 tailwind에 최상급 est를 붙인 다소 오만한 이름을 가진 패키지를 홍보하겠다. 이름하여 tailwindest 패키지다.
앞서 함께 살펴본 모든 기술적 고민들을 종합한 패키지다.
처음에 말했던 5가지를 모두 만족하도록 만들었다.
거기에 추가적으로 tailwind.config.js
에서 설정한 커스텀 값들도 추가할 수 있도록 만들어서 사용성을 최대한 고려해서 프로덕션 환경에서도 적용가능하게 설계했다.
npm i tailwindest
를 통해 바로 사용해 볼 수 있고 repo 에 들어가면 모든 코드를 볼 수 있다.
사용하기 쉽게 공식 문서도 만들었으니 혹여나 흥미롭다면 들어가보기를 추천한다.
반응이 좋다면 패키지 배포는 어떻게 하고, 프로젝트의 지속적인 관리에 대한 방법도 이야기 하면 좋을것 같다.
이상 모두 감사합니다! :)
1 편 재밌게 봤었는데, 2편도 잘 보고 갑니다! 마동석 썸네일은 아직도 생각나네요 :)