ddunkun.kiwi
fire_overlay
개발

블로그 5회차, 테일윈드 입문 후기

HoonKun
2026-02-15, 15:27
13분 읽기

25년 겨울의 연례행사로써 블로그를 또또또또 엎고 테일윈드에 입문한 후기

서론

25년을 맞이하여 또 블로그를 엎은 것이다. 저번이 몇회차였는지 이제는 기억도 나지 않는다.
아마 4회차였던 것 같은데. 아무튼.

이번 개인 페이지에서는 몇 가지 개선들과 함께 Tailwind 에 입문해봤다:

  • UI 를 좀 더 이쁘게! 주변인에게 정갈하다는 말도 들었다 기쁘다
  • 메인 페이지에 Discord Rich Presence 와 연동되는 페이지 주인장의 현재 활동 상태를 표시한다.
  • 기타 기능 개선 - 게시글 페이지와 발자취 페이지의 우측 네비게이터의 편의성, 게시글 공유 시 스크롤 위치의 대략적 기억 등

이 게시글은 그들에 대한 간단한 후기에 대해 작성하기로 한다.

Discord Rich Presence

기존 페이지에 아래와 같은 메시지가 있었다:

마인크래프트는 개발하는 것도 좋아하지만 같이 플레이하는 것도 좋아합니다! 같이 할 사람 절찬리에 모집 중!(??)

이걸 Now Playing: Minecraft 같은걸로 바꾸자 싶었는데, 사실 마인크래프트를 하루종일 하는건 아니니 그걸로 고정하기엔 문제가 있었다.
그런데 잘 생각해보니, 디스코드에 이런 NowPlaying 을 실시간으로 표시해주는 기능이 이미 있었고, WebSocket(읽기) 과 IPC(쓰기) 를 통해 접근할 수 있었던 것이다!
다만 기존 Discord 데스크톱 앱으로 WebStorm, PyCharm 등의 IDE 는 핸들링이 되었으나, 개인적인 바람으로 Procreate 라는 모바일 아이패드 앱도 여기에 떴으면 하는 바람이 있었다.

대략적으로는 아래 사진처럼 방에서 잠자는 PC 하나에 Ktor 서버를 올리고, 이 서버가 Discord 와 IPC 로 통신하는 동시에 아이패드와도 Http 를 통해 통신하도록 구현했다.

플로우

이런걸 왜 피그마로 만들어요?

  • 기존 Minecraft 같은 게임이나, IntelliJ IDEA 등의 IDE 도구들은 Discord 가 알아서 잘 감지해주므로 그것을 Kotlin 의 Kord 를 통해 읽어 응답한다.
  • Procreate 는 아이패드에서 돌아가므로, Discord 가 이걸 감지할 현실적인 방법이 없다. 따라서, 아이패드 단축어의 자동화 기능을 통해 Procreate 가 열리고 닫힐 때 서버에 이벤트를 날려서 서버가 IPC 를 통해 디스코드에 대신 알리도록 한다.

이렇게 할 때의 문제점은, 그림에도 보이지만 서버에서 Discord 가 항상 켜져있어야 한다는 점이다.
Discord 가 항상 켜져있으면 오프라인으로 전환되는 일이 없고 최소한 '자리비움'으로는 표시된다는 점이 문젠데, 사실 내 디스코드 친구는 10명 남짓이 끝이고 그마저도 아무도 내 상태를 신경쓰지는 않으므로 넘어가기로 한다.

또한, 이렇게 IPC 로 활동 상태를 업데이트하면 실제 Discord 클라이언트에 표시되는 활동 명은 내가 지정한 활동의 이름이 아니라 IPC 연결을 위해 등록한 앱의 이름이 뜬다.
즉, Discord 앱 하나를 만들어놓고 Procreate 를 실행하면 'Procreate 플레이 중', 명일방주를 실행하면 '명일방주 플레이 중' 같은 표시를 하는건 어렵다는 것이다.
이걸 하려면 표시하려는 활동의 갯수만큼 앱을 하나하나 만들어줘야 하며, 실제로 그렇게 했다간 앱을 사칭하는 일과 다름 없는 짓이 되므로 하면 안될 것 같았다.
물론 유저 목록에서만 그렇게 보일 뿐 마우스를 가져다 대거나 하면 아이콘까지 명확하게 보이니까 딱히 상관 없기는 한데... 살짝 걸리기는 한다.

게다가 사실 아이패드의 자동화 동작도 영 정확하지는 않지만, 어차피 표시되는 데이터도 굳이 정확해야 할 이유는 없으므로...

Tailwind 영업맨이야 너?

기존에는 Tailwind 를 싫어했다. 약간 그런 게시글이 있었는데:

어느 날 갑자기 for (int i = 0; i < 8; i++) { ... }f:i-0;i<8;i++ 라고 쓰자고 한다면 좋아하는 사람은 아무도 없을 것이다. 왜 굳이 이런 짓을 해야하는가?

솔직히 이 생각에 깊이 공감했다. 멀쩡히 있는 css 를 왜 굳이? 이런 짓을 해야하는거지? 싶은 생각에 가까웠다. 그러나 Tailwind 입문 4일차에 이 생각은 완전히 뒤엎어지게 된다.

앗 들켰네 하지만 들어보세요

정말이지 이것만큼 편한게 없다:

  • 단순히 css의 축약이 아니다. 소스를 전체 분석하여 Tailwind 의 className 으로 인식되는 애들을 추출, css 로 변환하는 애이다.
    즉, 커스텀 색상 hex 값이나 너비/높이 수치 등을 얼마든지 넣을 수 있고, 걔네들만 따로 분리해서 css 를 만들어준다.
  • 매 css 속성이 값의 단위를 포함해 굉장히 짧게 축약된다. 타이핑 부담이 정말 체감이 될 정도로 줄어든다.
    기존에 플렉스 컨테이너를 만들기 위해 css 를 작성하면 disp 까지 적고 Enter, fl 까지 적고 Enter 혹은 flex 모두를 작성했다. 언어 서버가 자동완성을 내려주기 까지의 딜레이를 감안하면 길다.
    그러나 Tailwind 를 사용하면 flex 하나면 된다. padding: 0.25rem; 또한 p-1 로 축약되며(!), widthheight 또한 w, h 로 축약된다.
  • 단위를 명시적으로 신경쓸 필요가 없다. spacing 과 단위가 이미 내부적으로 rem 을 쓰도록 숨겨져있다.
  • 몇몇 속성은 css 로 작성할 때보다 더 직관적인 명명으로 축약되어있다. 예를 들면, line-clamp-2 같은 것들이 있다. 이 속성은 css 로 작성하면 대략 4개의 속성을 적어줘야한다.

게다가, 번들 사이즈도 획기적으로 줄어들 것이다. 기존의 styled 를 그대로 썼다면, 플랙스 컨테이너인 styled 요소가 100개라면 걔네들이 전부 다른 className 으로 해싱되어 스타일 테그에 박히겠지만, 테일윈드를 쓰면 display: flex;style 태그 안에 .flex 라는 클래스로 단 한 번만 작성되고 요소에는 flex 만 작성될 것이다.

외우기 싫다고? 그럼 아래 사진을 보자:

플로우

이정도로 친절하게 자동완성을 해준다. 실제로 이게 css 로 뭘로 번역되는지도 보여준다.
솔직히 대강 프로퍼티 이름이나 속성 이름을 치면 어지간해서는 자동완성이 알려준다.

그러면 처음 입문한건데 삽질은 안했냐고 묻는다면 그것은 또 아니다. 몇 가지 삽질을 하기는 했다:

삽질은 안했냐?

첫날은 뭐든 그렇겠지만 머리가 아프다. 새로운 걸 배운다는건 항상 그렇기에. 하지만 WebStorm 의 Tailwind Language Server 와 자동완성과 함께라면 어떻게든 된다.

그러다 슬슬 tailwind 클래스 적는 것에 익숙해진다. flex, flex-col, p-n, relative, 뭐 이런 것들이 이제 자동적으로 나오기 시작한다.
근데? 그렇게 막 적다가 보니 갑자기 이런게 안된다:

const color = someCriteria ? "emerald" : "blue"
return <div className={`bg-${color}-400`}>...</div>

뭐지? 왜 안되지?

소스 분석 및 번들링

Tailwind 는 빌드 타임에 소스파일에 포함되는 모든 tailwind 클래스명들을 집계하여 '그 클래스만' 번들에 포함시킨다.
예를 들어, 어떤 프로젝트에서 Tailwind 로 'flex' 만 썼다면, tailwind 는 번들링 시 .flex { display: flex; } 만 포함시킨다.
이 말인 즉 모든 클래스명은 빌드타임에 분석이 가능해야한다는 의미가 된다. 위의 예제에서는 런타임에 결정되는 변수를 클래스명에 갖다 넣었으므로 안되는게 맞다.

하지만 대부분의 경우 아래처럼 우회가 된다:

const background = someCriteria ? "bg-emerald-400" : "bg-blue-400"
return <div className={background}/>

혹은, tailwind 의 색상 변수가 아닌 것을 넣는다면:

const background = someCriteria ? "bg-[#ff0000]" : "bg-[#0000ff]"
return <div className={background}/>

정말 최악의 경우로 뭔지 알 수 없는 색상이 들어온다면 근데 이럴거면 그냥 인라인 스타일을 쓰는게 어떨까:

return (
    <div 
        className={"bg-(--arbitrary-color)"} 
        style={{ ["--arbitrary-color" as never]: arbitraryColor }}
    />
)

props.className 과 twMerge

className 중첩으로 아래와 같은 코드를 만든다:

return (<div className={`flex flex-col ${props.className}`}/>)

위의 코드에는 두 가지 문제가 있는데:

  • props.classNameflex-row 가 들어온다고 생각해보자. React 에 너무 오래동안 빠져있던 상태로 생각하면 flex-rowflex-col 를 대치할것이라고 생각한다.
    그러나 물론이지만 그렇지 않다. 아아아주 옛날에 className 이 여러 개가 올 때 속성이 중복되는 것이 있으면 styles 태그에 정의된 순서를 따른다 라고 배운 바가 있다. styles 태그는 tailwind 가 작성하므로, 그 순서를 세밀하게 조절할 수 없으며 조절해서도 안된다.
  • props.className 에 아무것도 들어오지 않았을 경우 undefined 클래스가 삽입된다. 이거 잘못하면 htmlundefined 라는 클래스명이 남발될 수도 있다.

이럴 때 우리는 twMerge 를 쓸 수 있다. twMerge 는 tailwind-merge 의 함수로, 서로 중복되는 클래스들을 뒤에 온 것으로 대치해준다. 요컨데, 아래처럼 작성하면 최종적으로는 flex flex-row 만 남는다:

return (<div className={twMerge("flex flex-col", "flex-row")}/>)

똑똑하게도, Falsey 한 값들은 모두 무시해준다. 예를 들어 아래와 같은 것들이 모두 기대한 대로 동작한다:

return (<div className={twMerge("flex flex-col", props.className)}/>)
return (<div className={twMerge("flex flex-col", someBooleanCondition && "flex-row")}/>)
return (<div className={twMerge("flex flex-col", "")}/>)

솔직한 감상

솔직히 이제 새 프로젝트 파면 tailwind 를 쓸 것 같다.
기존 프로젝트에서 styled 쓰는게 벌써 괴롭다. 새 styled 컴포넌트에 이름 붙히는것도 지치고... css 너무 장황해.... (이런 발언)

후기

매번 블로그를 엎을 때마다 새로 배우는게 있다. 신기하게도... 웹의 변화가 그만큼 급진적이라는 얘기이기도 하겠지만...
그리고 기존 블로그는 내린지 좀 되긴 했지만 아마 계속 올려두었으면 Next 의 10점짜리 취약점에 걸렸을 것이다.

생긴 것도 꽤 이쁘게 잘 나온 것 같고(주변인이 이쁘다고 해줬다), 나름 괜찮은 기능도 하나 추가해서 마음에 들게 잘 나왔다고 생각한다.
다만 댓글 기능을 아직 적용 못했는데, 기존에 버셀에 올려뒀던 낡은 저장소를 로컬로 옮기려고 보니 애가 쿠키를 자꾸 이상하게 구워서 로그인이 안되길래 일단 치웠다.

이 건은 시간과 정신적 여유가 충분할 때 해보는 것으로 하려고 한다. 어차피 아무도 댓글 안달잖아 깃헙 계정이 필요한데 누가 달겠냐

키위새의 아무말 저장소생각 날 때마다 들러서 아무말을 합니다.