Next.js 15 + Tailwind 4 환경에서 재사용 가능한 Gap 컴포넌트를 만들다가 예상치 못한 벽에 부딪혔다. "왜 이렇게 간단한 컴포넌트가 동작을 안 하지?"라는 의문에서 시작해, 해결 과정을 정리해본다.
프로젝트에서 반복되는 flex 레이아웃 패턴을 정리하기 위해 공통 컴포넌트를 만들었다:
interface GapProps {
direction?: "flex-row" | "flex-col";
children?: React.ReactNode | React.ReactNode[];
gap: string;
items?: "center" | "start" | "end" | "between" | "around" | "evenly";
justify?: "center" | "start" | "end" | "between" | "around" | "evenly";
width?: string;
height?: string;
}
export default function Gap({
direction,
children,
gap,
items = "center",
justify = "center",
width = "auto",
height = "auto",
}: GapProps) {
return (
<div
className={`flex ${direction} items-${items} justify-${justify} w-${width} h-${height} gap-${gap}`}
>
{children}
</div>
);
}
재사용 가능해 보였다. 그런데 막상 사용해보니:
<Gap direction="flex-col" gap="40" width="full">
<Gap direction="flex-col" gap="20">
<Image src="/logo.svg" alt="logo" width={100} height={100} />
<p className="text-center text-head24 text-gray-900">
복잡한 일정 조율, 만나에서 심플하게!
</p>
</Gap>
<Gap direction="flex-col" gap="20" width="full">
<button>카카오로 시작하기</button>
<button>Google로 시작하기</button>
</Gap>
</Gap>
바깥쪽 gap="40"이 완전히 무시되었다..! ㅠㅠ
개발자 도구로 확인해보니 신기한 현상이 발견되었다:
<Gap gap="40"> {/* 동작 안 함! */}
<Gap gap="20"> {/* 동작함 (?!) */}
...
</Gap>
<Gap gap="20"> {/* 동작함 (?!) */}
...
</Gap>
</Gap>
gap-20: CSS가 정상 생성되어 동작함 gap-40: 클래스 자체가 생성되지 않음같은 컴포넌트, 같은 방식인데 왜 어떤 건 되고 어떤 건 안 될까?
정확한 원인은 파악하지 못했지만, 동적으로 클래스를 생성하는 과정에서 Tailwind의 JIT 컴파일러가 일부 클래스를 누락하는 것으로 보인다. 이런 비일관적인 동작은 더 큰 문제다. 에러가 명확하게 나면 바로 알아차릴 수 있지만, "어떤 때는 되고 어떤 때는 안 되는" 상황은 디버깅을 매우 어렵게 만든다. ㅠㅠ
추정한 핵심 원인은 Tailwind의 JIT(Just-In-Time) 컴파일러가 동적으로 생성된 클래스명을 인식하지 못한다는 것이다.
className={`flex ${direction} items-${items} gap-${gap}`}
이렇게 템플릿 리터럴로 클래스를 조합하면, Tailwind는 빌드 시점에 코드를 스캔할 때 gap-40이라는 완전한 클래스명을 찾을 수 없다. 그저 gap-${gap} 패턴만 보일 뿐이다.
Tailwind는 빌드 시점에 모든 파일을 스캔해서 완전한 클래스명만 CSS로 생성한다:
className="gap-4 flex-col" → 감지됨className={condition ? "gap-4" : "gap-8"} → 둘 다 감지됨 className={\gap-${value}`}` → 감지 안 됨참고 링크: https://tailwindcss.com/docs/detecting-classes-in-source-files
이는 Tailwind는 빌드 시점에 사용된 클래스만 생성함으로써 최종 CSS 번들 크기를 최소화할 수 있기 때문이다.
디자인 시스템에 --spacing 변수를 정의해두었기 때문에, 인라인 스타일로 직접 적용하는 방식을 선택했다.
그리고 이외에도 동적으로 생성하던 flex-${direction} 같은 코드들도 전부 제거했다.
먼저 내 디자인 시스템 설정:
@import "tailwindcss";
:root {
--background: #f5f5f5;
--foreground: #171717;
font-size: 16px;
}
@theme {
/* 디자인 기준 16px: 1rem */
/* w-, h-, gap-, p- 등에서 디자인 숫자만 입력하여 사용 */
--spacing: calc(1 / 16 * 1rem);
}
interface GapProps {
direction?: "row" | "col";
children?: React.ReactNode | React.ReactNode[];
gap: number;
items?: "center" | "start" | "end";
justify?: "center" | "start" | "end" | "between" | "around" | "evenly";
width?: "auto" | "full" | "fit";
height?: "auto" | "full" | "fit";
className?: string; // 추가로 필요한 경우를 위해
}
export default function Gap({
direction = "row",
children,
gap,
items = "center",
justify = "center",
width = "auto",
height = "auto",
className,
}: GapProps) {
const classes = [
"flex",
direction === "row" ? "flex-row" : "flex-col",
items === "center" && "items-center",
items === "start" && "items-start",
items === "end" && "items-end",
justify === "center" && "justify-center",
justify === "start" && "justify-start",
justify === "end" && "justify-end",
justify === "between" && "justify-between",
justify === "around" && "justify-around",
justify === "evenly" && "justify-evenly",
width === "full" && "w-full",
width === "fit" && "w-fit",
width === "auto" && "w-auto",
height === "full" && "h-full",
height === "fit" && "h-fit",
height === "auto" && "h-auto",
className,
]
.filter(Boolean)
.join(" ");
return (
<div className={classes} style={{ gap: `calc(${gap} * var(--spacing))` }}>
{children}
</div>
);
}
핵심 포인트:
gap prop을 number 타입으로 받아서 디자인 숫자 그대로 사용calc(${gap} * var(--spacing))로 계산하여 rem 단위 적용gap={40} → calc(40 * 1/16 * 1rem) → 2.5rem (40px)--spacing 변수를 그대로 활용gap={35})--spacing 값을 변경하면 모든 Gap에 자동 반영className={clsx(
"flex",
{
"gap-4": gap === 4,
"gap-8": gap === 8,
"gap-12": gap === 12,
"gap-16": gap === 16,
"gap-20": gap === 20,
"gap-40": gap === 40,
// 필요한 모든 값 추가...
}
)}
단점: 사용 가능한 값이 제한되고, 코드가 길어진다.
<div className="flex flex-col gap-40 items-center justify-center w-full">
<div className="flex flex-col gap-20 items-center justify-center">
...
</div>
</div>
단점: 반복적이고, 디자인 시스템 규칙을 강제하기 어렵다.
"그래도 인라인 스타일이면 성능이 안 좋지 않을까?" 하는 의문이 들었다.
이론적으로 className이 약간 빠르지만, 실제로는:
gap 하나 정도의 간단한 속성은 측정 불가능한 수준만약 gap-4, gap-8, gap-12, ..., gap-40 등 10개 값을 사용한다면:
사실 큰 차이는 아니지만, 많은 값을 사용할수록 inline style이 유리하다.
성능 차이는 무시 가능한 수준이고, 오히려:
<Gap gap={40}> vs className="gap-40"이런 장점들이 훨씬 크다고 판단했다.
물론 항상 컴포넌트가 답은 아니다. 이런 경우는 직접 className 사용할 예정이다.
// 한두 번만 사용하는 특수한 레이아웃
<div className="flex flex-col gap-8 p-4 bg-white rounded-lg shadow-md hover:shadow-lg">
...
</div>
// Tailwind의 다른 유틸리티와 복잡하게 조합
<div className="flex items-center gap-4 md:gap-6 lg:gap-8">
...
</div>
Styled-components같은 동적 스타일링 방식에 익숙해져있다 Next를 주로 사용하며 Tailwind 사용법을 익히려하니 생각보다 주의할 게 많음을 배웠다..!
같은 문제로 고민하는 분들께 도움이 되길 바라며..!