
이번 고급 프로젝트를 하면서 새로 해보는 것들이 있다.
간단하게 새로 해보는 것에 대해서 알아보고 가자!
이번 프로젝트에서는
Tailwind를 사용하게 되었다.
Tailwind에 대해 간단하게 알아보자.🎨
Tailwind CSS란 무엇인가?
Tailwind CSS는 유틸리티 퍼스트(Utility-First) 방식의CSS프레임워크이다.
기존의 HTML처럼 직접 클래스를 만들고 스타일을 정의하는 방식이 아니라, 미리 정의된 유틸리티 클래스를 조합하여 스타일을 구성하는 방식이다.<!-- 예시 --> <button class="bg-blue-500 text-white px-4 py-2 rounded"> 버튼 </button>위와 같이
class에 여러 유틸리티 클래스를 조합해 하나의UI를 완성할 수 있다.
🔧 유틸리티 클래스란?
유틸리티 클래스는 하나의 스타일 속성만을 담당하는 단일 기능 클래스이다.
예를 들어,
bg-blue-500: 배경색을 파란색으로 지정text-white: 텍스트 색상을 흰색으로 설정px-4 py-2: 좌우, 상하 패딩을 설정rounded: 테두리를 둥글게 처리이처럼 여러 유틸리티 클래스를 조합해서 원하는
UI를 구성할 수 있다.
⚡
Tailwind CSS의 장점
- ✅ 빠른 개발 속도
HTML안에서 바로 스타일을 입힐 수 있어CSS파일을 별도로 작성하지 않아도 된다.
- ✅ 일관된 디자인 시스템
클래스가 체계적으로 정리되어 있어서 디자인의 일관성을 유지하기 좋다.
- ✅ 작은
CSS번들 사이즈
실제 사용하는 클래스만CSS에 포함되기 때문에 최종 번들 크기가 작아진다.
🚫
Tailwind CSS의 단점
- ❌
HTML가독성 저하
클래스가 많아지면HTML코드가 복잡해 보일 수 있다.
- ❌ 처음에는 낯선 클래스 명
클래스 이름이 직관적이지 않다고 느낄 수 있어서 익숙해지는 데 시간이 걸릴 수 있다.
💡
Tailwind CSS는 이런 경우에 추천
- 디자인 시스템을 일관되게 유지하고 싶은 팀
- 빠르게 프로토타입을 만들어야 하는 프로젝트
- 컴포넌트 중심
UI개발을 선호하는 프론트엔드 개발자
📚 참고 자료
🚀
Next.js App Router란?
Next.js의App Router는pages디렉토리 기반의 기존 라우팅 시스템을 대체하는 새로운 파일 기반 라우팅 시스템이다.
버전 13부터 도입되었으며, 더 유연한 구조와 향상된 기능을 제공한다.
📁 디렉토리 기반 라우팅
기존의
pages디렉토리 대신,app디렉토리를 사용하여 라우팅을 구성한다.
app/page.tsx→ 루트 경로(/)app/about/page.tsx→/about경로app/blog/[slug]/page.tsx→ 동적 라우팅(/blog/123)
🧱 주요 구성 요소
page.tsx: 해당 경로의 페이지 컴포넌트layout.tsx: 공통 레이아웃 정의 (헤더, 푸터 등)loading.tsx: 해당 경로 로딩 시 보여줄 UIerror.tsx: 에러 발생 시 보여줄 UItemplate.tsx: 레이아웃을 공유하지 않고 새로 마운트할 때 사용
🔄 서버 컴포넌트 기반
App Router는 기본적으로React Server Components를 사용한다.
클라이언트와 서버의 역할을 구분해 성능 최적화가 가능하다.
- 서버에서 데이터를 미리 불러오고 렌더링 → 빠른 응답
- 클라이언트 컴포넌트는
"use client"지시어로 지정
⚙️ 기타 특징
- ✅ 경로마다 자체적인
layout구성 가능- ✅ 중첩 라우팅(
Nested Routes) 지원- ✅ 폴더 구조만으로 명확한 라우팅 설정 가능
- ✅ 파일 기반 로딩, 에러 처리 내장
📚 참고 자료
📘
Storybook이란?
Storybook은 컴포넌트 기반 개발을 위한 UI 개발 환경 도구이다.
React,Vue,Svelte,Angular등 다양한 프레임워크에서 사용할 수 있으며, 컴포넌트를 독립적으로 개발하고 테스트할 수 있게 도와준다.
🎯 주요 목적
- 컴포넌트를 독립적인 상태에서 확인하고 테스트할 수 있다.
- 디자인 시스템이나 컴포넌트 라이브러리 문서화에 적합하다.
- 개발자, 디자이너, QA가 함께 협업하기 쉬운 환경을 제공한다.
🧱 핵심 개념
Stories: 각 컴포넌트의 다양한 상태를 보여주는 단위 파일 (Button.stories.tsx)Canvas: 컴포넌트를 시각적으로 확인하고 상호작용할 수 있는 화면Docs: 자동 생성되는 컴포넌트 설명 문서 탭Controls:props값을 조작할 수 있는 UI 패널
⚙️ 사용 예시
// Button.stories.tsx import { Button } from './Button'; // export default { title: 'Components/Button', component: Button, }; // export const Primary = () => <Button label="Primary" variant="primary" />; export const Secondary = () => <Button label="Secondary" variant="secondary" />;위처럼 각 상태를
스토리로 정의하면Canvas에서 다양한 모습의 컴포넌트를 확인할 수 있다.
✅ 장점
- UI 개발 시 빠른 피드백 루프 제공
디자인 시스템 문서화,테스트 자동화,비주얼 회귀 테스트등 다양한 확장 기능<span style="color:skyblue">컴포넌트 재사용성과 일관성</span>강화
📚 참고 자료
GlobalNomad프로젝트에서 작성한Button컴포넌트는 다양한 상황에서 재사용할 수 있도록 사이즈와 스타일을 조합 가능하게 만든 유연한 버튼 컴포넌트이다.
🏗 컴포넌트 구조 및 코드 분석
✅
/components/button/Button.tsx버튼 컴포넌트의 핵심 파일로, 주요 동작 방식은 아래와 같다.
import { ButtonProps, SizeVariant } from "@/types/button"; // const sizeStyles: Record<SizeVariant, string> = { calendar: "mobile:w-[327px] mobile:h-[50px] mobile:text-14-m tablet:w-[695px] tablet:h-[50px] tablet:text-16-m desktop:w-[135px] desktop:h-[50px] desktop:text-16-m", chooseModal: "mobile:w-[113px] mobile:h-[41px] mobile:text-14-m tablet:w-[135px] tablet:h-[47px] tablet:text-16-m desktop:w-[135px] desktop:h-[47px] desktop:text-16-m", empty: "mobile:w-[182px] mobile:h-[19px] mobile:text-16-m tablet:w-[182px] tablet:h-[19px] tablet:text-16-m desktop:w-[182px] desktop:h-[19px] desktop:text-16-m", experienceRegister: "mobile:w-[138px] mobile:h-[48px] mobile:text-16-m tablet:w-[138px] tablet:h-[48px] tablet:text-16-m desktop:w-[138px] desktop:h-[48px] desktop:text-16-m", experienceRegister2: "mobile:w-[120px] mobile:h-[41px] mobile:text-14-m tablet:w-[120px] tablet:h-[41px] tablet:text-14-m desktop:w-[120px] desktop:h-[41px] desktop:text-14-m", modal: "mobile:w-[180px] mobile:h-[41px] mobile:text-14-m tablet:w-[200px] tablet:h-[47px] tablet:text-16-m desktop:w-[200px] desktop:h-[47px] desktop:text-16-m", myInfo: "mobile:w-[157.5px] mobile:h-[48px] mobile:text-14-m tablet:w-[120px] tablet:h-[41px] tablet:text-16-m desktop:w-[120px] desktop:h-[41px] desktop:text-16-m", reservation: "mobile:w-[157.5px] mobile:h-[37px] mobile:text-14-m tablet:w-[232px] tablet:h-[37px] tablet:text-14-m desktop:w-[71px] desktop:h-[29px] desktop:text-14-m", review: "mobile:w-[309px] mobile:h-[37px] mobile:text-14-m tablet:w-[476px] tablet:h-[37px] tablet:text-14-m desktop:w-[71px] desktop:h-[29px] desktop:text-14-m", reviewModal: "mobile:w-[279px] mobile:h-[41px] mobile:text-14-m tablet:w-[325px] tablet:h-[54px] tablet:text-16-m desktop:w-[325px] desktop:h-[54px] desktop:text-16-m", search: "mobile:w-[85px] mobile:h-[41px] mobile:text-14-m tablet:w-[120px] tablet:h-[50px] tablet:text-16-m desktop:w-[120px] desktop:h-[50px] desktop:text-16-m", sign: "mobile:w-[328px] mobile:h-[54px] mobile:text-16-m tablet:w-[640px] tablet:h-[54px] tablet:text-16-m desktop:w-[640px] desktop:h-[54px] desktop:text-16-m", }; // const variantStyles = { primary: "bg-brand-500 text-white", secondary: "bg-gray-200 text-[#EDEEF2]", outline: "bg-white text-[#707177] border border-[#C6C8CF]", } as const; // function mergeClasses(...classes: (string | false | undefined)[]) { return classes.filter(Boolean).join(" "); } // export default function Button({ children, onClick, disabled = false, size = "sign", variant = "primary", fullWidth = false, rounded = true, className = "", type = "button", }: ButtonProps) { const baseStyle = "font-medium transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed"; // const finalClassName = mergeClasses( baseStyle, sizeStyles[size], variantStyles[variant], fullWidth && "w-full", rounded && "rounded", className, ); // return ( <button type={type} onClick={onClick} className={finalClassName} disabled={disabled} style={rounded ? { borderRadius: "16px" } : undefined} {children} </button> ); }✅ 전체 흐름 요약
ButtonProps타입을 기반으로 다양한props를 전달받아 버튼을 유연하게 조작할 수 있다.
size: 버튼의 크기와 폰트 크기 (sizeStyles로 지정)variant: 색상 종류 (variantStyles로 지정)fullWidth: 버튼 너비를 100%로 만들지 여부rounded: 테두리 둥글게 처리 여부 (border-radius: 16px)className: 사용자 추가 클래스type,disabled,onClick,children:HTML기본 속성모든 스타일은
mergeClasses()함수를 통해 결합되어 최종Tailwind클래스 문자열을 만들어<button>에 넣어준다.
🧩 주요 코드 설명
1.
sizeStylesconst sizeStyles: Record<SizeVariant, string> = { ... sign: "mobile:w-[328px] mobile:h-[54px] mobile:text-16-m tablet:w-[640px] tablet:h-[54px] ...", };
- 버튼의 크기를 정의하는 부분
mobile:,tablet:,desktop:prefix는Tailwind의 반응형 구문이다SizeVariant타입을 기준으로key들을 정의했기 때문에 타입 안정성이 있다
2.
variantStylesconst variantStyles = { primary: "bg-brand-500 text-white", secondary: "bg-gray-200 text-[#EDEEF2]", outline: "bg-white text-[#707177] border border-[#C6C8CF]", } as const;
- 버튼 색상(배경 + 글자색)을 정의하는 곳
variant="primary"처럼 쓰면 이 스타일이 적용된다
3.
mergeClasses함수function mergeClasses(...classes: (string | false | undefined)[]) { return classes.filter(Boolean).join(" "); }
undefined,false, 빈 값 등을 제거하고 유효한Tailwind클래스만 결합- 가령
fullWidth && "w-full"같은 조건 클래스가 있을 때 유용함
4.
finalClassName조합const finalClassName = mergeClasses( baseStyle, sizeStyles[size], variantStyles[variant], fullWidth && "w-full", rounded && "rounded", className, );
- 여기서
size,variant, 사용자 정의 클래스가 한 줄로 합쳐짐rounded가true면Tailwind의rounded클래스가 붙고fullWidth가true면w-full이 추가됨
🧾
ButtonProps타입 정의 (/types/button.ts)export type SizeVariant = | "calendar" | "chooseModal" | "empty" | "experienceRegister" | "experienceRegister2" | "modal" | "myInfo" | "reservation" | "review" | "reviewModal" | "search" | "sign"; // export type ButtonProps = { children: React.ReactNode; onClick?: () => void; disabled?: boolean; size?: SizeVariant; variant?: "primary" | "secondary" | "outline"; fullWidth?: boolean; rounded?: boolean; className?: string; type?: "button" | "submit" | "reset"; };
- 총 13가지 사이즈 옵션을 가진
SizeVariant타입을 제공variant는primary,secondary,outline중 선택 가능fullWidth,rounded,disabled등 다양한 UI 옵션 제공
🧪 사용 예시
<Button size="review" variant="primary">리뷰 작성</Button> // <Button size="sign" variant="secondary" fullWidth>회원가입</Button> // <Button size="modal" variant="outline" onClick={handleClick}>닫기</Button>
- 여러 사이즈와 여러 스타일을 적용한 버튼
🔍 스토리북 설정 분석
📄
/stories/button/Button.stories.tsxconst meta: Meta<ButtonProps> = { title: "Components/Button", component: Button, tags: ["autodocs"], argTypes: { size: { control: "select", options: [ "calendar", "chooseModal", "empty", "experienceRegister", "experienceRegister2", "modal", "myInfo", "reservation", "review", "reviewModal", "search", "sign" ] }, variant: { control: "select", options: ["primary", "secondary", "outline"] }, fullWidth: { control: "boolean" }, rounded: { control: "boolean" }, disabled: { control: "boolean" }, }, };
Storybook에서 드롭다운, 토글 스위치로 컴포넌트의 다양한prop을 직접 조작해볼 수 있도록 설정되어 있다.argTypes를 활용해select,boolean타입으로 컨트롤 UI를 자동 생성한다.export const Default: Story = { args: { children: "버튼", size: "sign", variant: "primary", fullWidth: false, rounded: true, disabled: false, }, };
Default스토리를 통해 기본 버튼 예제를 시각적으로 확인 가능children,size,variant등을 설정하여 버튼을 렌더링한다.
🧾 Storybook 화면 예시
![]()
위는
Button컴포넌트의Storybook화면이다.
- 상단에는 실제 버튼이 렌더링된 모습을 확인할 수 있고,
- 하단 표에서는 각
props에 대한 타입, 기본값, 제어 컨트롤을 UI로 조작할 수 있도록 구성되어 있다.children(필수),size,variant,type,disabled,rounded,fullWidth등을 직접 조작하며 테스트해볼 수 있다.
🛠️ Tailwind 기반 버튼 컴포넌트 리팩토링 기록
팀원의 피드백이 있었기 때문에 코드를 수정해야했다.
어떤 것을 바꿔야하는지 살펴보자.✅ 피드백 내용
1.
style속성 사용 → Tailwind 클래스로 교체
- ❌
style={{ borderRadius: "16px" }}사용 중이었음- ✅ Tailwind 클래스인
rounded-[16px]으로 대체
📷 Beforestyle={rounded ? { borderRadius: "16px" } : undefined}📷 After
{rounded && "rounded-[16px]"}.
2. className 병합 함수 통일:
tMerge→cn
- ❌ 기존에 사용하던 병합 함수가
tailwind-merge기반이 아님- ✅
cn유틸 함수로 병합 로직 일원화 →props.className도 정상 반영
📷 Before
const finalClassName = mergeClasses(...)📷 After
import { cn } from "@/utils/classNames"; const finalClassName = cn(baseStyle, variantStyles[type], sizeStyles[size], ...).
3. variant 스타일 수정 (Figma 기준 색상 반영)
- ❌ 버튼의 색깔이 1가지 더 있었기 때문에 버튼 색깔을 더 추가해주었다.
- ✅ 실제 Figma에서는
text-gray-600이 기준
📷 Beforesecondary: "bg-gray-200 text-[#EDEEF2]"📷 After
secondary1: "bg-gray-200 text-[#EDEEF2]" secondary2: "bg-gray-50 text-gray-600".
💬 회고
- 단순 스타일 개선을 넘어 팀 내 공통 UI 컴포넌트의 확장성과 재사용성을 높인 점이 핵심
- Tailwind의 조건부 클래스 구성 패턴에 더 익숙해짐
- 디자이너 피그마 기준을 기반으로 컴포넌트 스타일을 맞추는 협업이 중요함을 다시 체감했다.
GlobalNomad프로젝트의Footer컴포넌트는 페이지 하단에 들어가는 공통 요소로, 소셜 아이콘, 회사 정보, 링크 메뉴 등을 포함한 반응형 레이아웃을 제공한다.
✅ 전체 구조 요약
©codeit - 2023: 좌측 하단 표시Privacy Policy,FAQ: 가운데 정렬된 링크 메뉴YouTube,X: 우측 소셜 아이콘모든 콘텐츠는
Tailwind CSS의grid+flex조합으로 반응형 배치되며,sm브레이크포인트 이상에서 데스크탑 레이아웃으로 자동 전환된다.
🧩 주요 코드 설명
1. 소셜 아이콘 배열
const socialIcons = [ { href: "https://www.facebook.com/", src: FacebookIcon, alt: "Facebook" }, { href: "https://www.instagram.com/", src: InstagramIcon, alt: "Instagram" }, { href: "https://www.youtube.com/", src: YoutubeIcon, alt: "YouTube" }, { href: "https://x.com/", src: XIcon, alt: "X" }, ];
- 각 소셜 링크와
SVG아이콘을 미리 정의한 배열.map()을 통해 반복 렌더링하며 접근성과key속성을 포함
2. 그리드 레이아웃 구조
<div className=" w-[90%] mx-auto grid grid-cols-3 grid-rows-2 sm:grid-rows-1 sm:grid-cols-3 items-center gap-2 text-center sm:text-left ">
- 기본 2행 3열
grid에서sm사이즈 이상에서는 1행 3열로 변경- 가운데 링크, 좌측 카피라이트, 우측 소셜 아이콘이 자연스럽게 배치됨
3. 각 영역 설명
📌 링크 영역
<div className=" row-start-1 col-span-3 flex justify-center gap-2 sm:col-span-1 sm:col-start-2 sm:row-start-1 ">
- 모바일에선 첫 번째 줄 가운데 정렬
- 데스크탑에선 가운데 열(
col-start-2)에 배치📌 카피라이트 영역
<div className=" row-start-2 col-start-1 flex justify-start sm:row-start-1 sm:col-start-1 ">
- 모바일에선 두 번째 줄 왼쪽 정렬
- 데스크탑에선 첫 번째 열로 이동
📌 소셜 영역
<div className=" row-start-2 col-start-3 flex justify-end gap-1 sm:row-start-1 sm:col-start-3 ">
- 모바일에선 두 번째 줄 오른쪽 정렬
- 데스크탑에선 세 번째 열로 이동
🧪
Footer사용 예시import Footer from "@/components/footer/Footer"; // export default function Page() { return ( <> {/* 본문 콘텐츠 */} <Footer /> </> ); }.
🧾
Storybook화면 예시![]()
위는
Footer컴포넌트의Storybook에서의 미리보기 화면이다.
- 좌측에
©codeit - 2023- 가운데에
Privacy Policy,FAQ링크- 우측에 소셜 아이콘이 배치됨
- 반응형이 적용되어
sm이하 해상도에서는 상하 2줄로 구성됨
😥 Footer 개발에서 어려웠던 점
처음에는 전체 레이아웃을
flex로 구현하려고 했다.
데스크탑 환경에서는flex를 활용해 각 요소들을 수평으로 잘 정렬할 수 있었지만, 모바일 환경에선 문제가 발생했다.
- 링크 메뉴가 위로 올라가거나,
- 저작권 문구와 소셜 아이콘이 예상치 못한 위치로 쏠리는 등,
화면 크기에 따라 요소들의 순서와 정렬이 완전히 무너지는 현상이 나타났다.
이 문제를 해결하기 위해flex대신grid 레이아웃으로 전환했다.
grid의row-start, col-start속성을 활용해 각 요소가 어떤 화면 크기에서도 예상한 위치에 정확히 배치되도록 조절했다.결과적으로
grid를 사용함으로써 모바일과 데스크탑 환경 모두에서 안정적인 레이아웃 구조를 유지할 수 있게 되었다.
목 디스크 부상으로 인해 일상생활이 불가능해 다른 팀원에게 넘겼다,,,,, 😭