03. Myblog 학습정리: 기본 UI 컴포넌트 및 레이아웃 시스템

Phase 3 학습정리: 기본 UI 컴포넌트 및 레이아웃 시스템
개요
Phase 3에서는 사용자 경험의 기반이 되는 UI 컴포넌트 시스템을 구축했습니다. shadcn/ui와 Tailwind CSS를 활용하여 일관성 있고 접근 가능한 디자인 시스템을 완성했으며, 반응형 레이아웃과 테마 시스템을 통해 현대적인 웹 애플리케이션의 사용자 인터페이스를 구현했습니다.
특히 컴포넌트 기반 아키텍처와 재사용 가능한 디자인 패턴을 통해 확장성과 유지보수성을 확보했으며, Phase 1-2에서 구축한 기술 스택 위에 완전한 사용자 인터페이스 레이어를 완성했습니다.
핵심 학습 내용
1. 컴포넌트 기반 레이아웃 아키텍처
계층적 레이아웃 시스템 설계
레이아웃 컴포넌트 구조:
typescript// src/components/layout/Layout.tsx - 메인 레이아웃 컨테이너 import { Header } from './Header'; import { Footer } from './Footer'; interface LayoutProps { children: React.ReactNode; } export function Layout({ children }: LayoutProps) { return ( <div className="flex min-h-screen flex-col"> <Header /> <main className="flex-1"> {children} </main> <Footer /> </div> ); }
학습 포인트:
- Flexbox 레이아웃:
flex min-h-screen flex-col로 전체 화면 높이 활용 - 유연한 메인 영역:
flex-1로 헤더/푸터 제외한 모든 공간 활용 - 컴포넌트 합성: Header, Footer 컴포넌트를 조합한 완전한 레이아웃
- 타입 안전성:
LayoutProps인터페이스로 children prop 타입 보장
반응형 헤더 컴포넌트 구현
typescript// src/components/layout/Header.tsx - 반응형 네비게이션 'use client'; import Link from 'next/link'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { ThemeToggle } from '@/components/ui/theme-toggle'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; export function Header() { const { user, isAuthenticated, isAdmin, signOut, isLoading } = useAuth(); return ( <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"> {/* 로고 및 메인 네비게이션 */} <div className="flex items-center space-x-6"> <Link href="/" className="flex items-center space-x-2"> <span className="text-xl font-bold">MyBlog</span> </Link> <nav className="hidden md:flex items-center space-x-6"> <Link href="/posts" className="text-sm font-medium transition-colors hover:text-primary" > 글 목록 </Link> <Link href="/about" className="text-sm font-medium transition-colors hover:text-primary" > 소개 </Link> {isAdmin && ( <Link href="/admin" className="text-sm font-medium text-primary transition-colors hover:text-primary/80" > 관리자 </Link> )} </nav> </div> {/* 사용자 액션 영역 */} <div className="flex items-center space-x-4"> <ThemeToggle /> {isLoading ? ( <div className="h-8 w-8 animate-pulse rounded-full bg-muted" /> ) : isAuthenticated && user ? ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="relative h-8 w-8 rounded-full"> <Avatar className="h-8 w-8"> <AvatarImage src={user.avatar_url || undefined} alt={user.full_name || user.email} /> <AvatarFallback> {user.full_name?.charAt(0) || user.email.charAt(0)} </AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-56" align="end" forceMount> <div className="flex items-center justify-start gap-2 p-2"> <div className="flex flex-col space-y-1 leading-none"> {user.full_name && ( <p className="font-medium">{user.full_name}</p> )} <p className="w-[200px] truncate text-sm text-muted-foreground"> {user.email} </p> </div> </div> <DropdownMenuSeparator /> <DropdownMenuItem asChild> <Link href="/profile">프로필</Link> </DropdownMenuItem> {isAdmin && ( <DropdownMenuItem asChild> <Link href="/admin/posts">글 관리</Link> </DropdownMenuItem> )} <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => signOut()} className="text-red-600 focus:text-red-600" > 로그아웃 </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) : ( <Button asChild> <Link href="/auth/login">로그인</Link> </Button> )} </div> </div> </header> ); }
학습한 핵심 개념:
- Sticky 헤더:
sticky top-0 z-50으로 스크롤 시에도 상단 고정 - 백드롭 블러:
backdrop-blur로 현대적인 반투명 효과 - 조건부 렌더링: 인증 상태와 권한에 따른 동적 UI 구성
- 접근성: ARIA 속성과 키보드 네비게이션 지원
- 반응형 디자인:
hidden md:flex로 화면 크기별 메뉴 표시/숨김
푸터 컴포넌트와 소셜 링크
typescript// src/components/layout/Footer.tsx - 반응형 푸터 import Link from 'next/link'; export function Footer() { return ( <footer className="border-t bg-background"> <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8"> <div className="flex flex-col items-center justify-between space-y-4 sm:flex-row sm:space-y-0"> <div className="text-center text-sm text-muted-foreground sm:text-left"> © 2024 MyBlog. All rights reserved. </div> <div className="flex items-center space-x-4"> <Link href="https://github.com/yourusername" className="text-muted-foreground transition-colors hover:text-foreground" target="_blank" rel="noopener noreferrer" > <span className="sr-only">GitHub</span> <svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 0C5.374 0 0 5.373 0 12 0 17.302 3.438 21.8 8.207 23.387c.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/> </svg> </Link> <Link href="mailto:your.email@example.com" className="text-muted-foreground transition-colors hover:text-foreground" > <span className="sr-only">Email</span> <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> </svg> </Link> </div> </div> </div> </footer> ); }
학습 포인트:
- 반응형 레이아웃:
flex-col sm:flex-row로 모바일/데스크탑 레이아웃 분기 - 접근성:
sr-only클래스로 스크린 리더 지원 - 외부 링크 보안:
rel="noopener noreferrer"로 보안 강화 - SVG 아이콘: 벡터 기반 아이콘으로 선명한 표시
2. 테마 시스템 구현
next-themes를 활용한 다크/라이트 모드
typescript// src/components/providers/theme-provider.tsx - 테마 컨텍스트 제공 'use client'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { type ThemeProviderProps } from 'next-themes/dist/types'; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return ( <NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange {...props} > {children} </NextThemesProvider> ); }
typescript// src/components/ui/theme-toggle.tsx - 테마 토글 컴포넌트 'use client'; import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; export function ThemeToggle() { const { setTheme } = useTheme(); return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" size="icon"> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">테마 변경</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => setTheme('light')}> 라이트 </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme('dark')}> 다크 </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme('system')}> 시스템 </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }
학습한 핵심 개념:
- CSS 변수 기반 테마:
attribute="class"로 CSS 클래스 기반 테마 전환 - 시스템 테마 감지:
enableSystem으로 OS 테마 설정 자동 감지 - 부드러운 전환:
disableTransitionOnChange로 테마 전환 시 깜빡임 방지 - 아이콘 애니메이션: CSS 트랜지션으로 Sun/Moon 아이콘 부드러운 전환
테마 시스템과 CSS 변수 연동
css/* src/app/globals.css - 테마별 CSS 변수 정의 */ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { /* 라이트 테마 */ --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96%; --secondary-foreground: 222.2 84% 4.9%; --muted: 210 40% 96%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96%; --accent-foreground: 222.2 84% 4.9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { /* 다크 테마 */ --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } }
학습 포인트:
- HSL 색상 시스템:
hsl(var(--primary))로 색상 값과 투명도 분리 - 의미론적 색상 명명:
primary,secondary,muted등 용도별 색상 정의 - 일관된 디자인 토큰: 모든 컴포넌트에서 동일한 색상 변수 사용
- 자동 테마 적용:
.dark클래스로 다크 모드 자동 전환
3. shadcn/ui 컴포넌트 시스템 확장
커스텀 컴포넌트 개발 패턴
typescript// src/components/ui/avatar.tsx - 아바타 컴포넌트 확장 'use client'; import * as React from 'react'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; import { cn } from '@/lib/utils'; const Avatar = React.forwardRef< React.ElementRef<typeof AvatarPrimitive.Root>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> >(({ className, ...props }, ref) => ( <AvatarPrimitive.Root ref={ref} className={cn( 'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className )} {...props} /> )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef<typeof AvatarPrimitive.Image>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> >(({ className, ...props }, ref) => ( <AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} /> )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> >(({ className, ...props }, ref) => ( <AvatarPrimitive.Fallback ref={ref} className={cn( 'flex h-full w-full items-center justify-center rounded-full bg-muted', className )} {...props} /> )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarImage, AvatarFallback };
학습한 핵심 개념:
- Radix UI 기반: 접근성이 내장된 headless 컴포넌트 활용
- forwardRef 패턴: ref 전달로 DOM 접근성 확보
- Compound Components: 관련 컴포넌트들의 논리적 그룹화
- 타입 안전성: Radix UI 타입을 확장한 완전한 타입 지원
드롭다운 메뉴 컴포넌트 활용
typescript// 사용자 메뉴에서 드롭다운 활용 예시 <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="relative h-8 w-8 rounded-full"> <Avatar className="h-8 w-8"> <AvatarImage src={user.avatar_url} alt={user.full_name} /> <AvatarFallback>{user.full_name?.charAt(0)}</AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-56" align="end" forceMount> <div className="flex items-center justify-start gap-2 p-2"> <div className="flex flex-col space-y-1 leading-none"> <p className="font-medium">{user.full_name}</p> <p className="w-[200px] truncate text-sm text-muted-foreground"> {user.email} </p> </div> </div> <DropdownMenuSeparator /> <DropdownMenuItem asChild> <Link href="/profile">프로필</Link> </DropdownMenuItem> <DropdownMenuItem onClick={() => signOut()}> 로그아웃 </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu>
학습 포인트:
- asChild 패턴: 기존 컴포넌트를 래핑하지 않고 props 전달
- 정렬 옵션:
align="end"로 드롭다운 위치 조정 - 키보드 네비게이션: 자동으로 화살표 키와 Enter 키 지원
- 포커스 관리: 메뉴 열기/닫기 시 포커스 자동 관리
4. 반응형 디자인 시스템
모바일 우선 반응형 접근법
typescript// 반응형 네비게이션 패턴 <nav className="hidden md:flex items-center space-x-6"> {/* 데스크탑에서만 표시되는 메뉴 */} <Link href="/posts">글 목록</Link> <Link href="/about">소개</Link> </nav> <div className="flex items-center space-x-4"> {/* 모든 화면 크기에서 표시 */} <ThemeToggle /> {/* 사용자 메뉴 */} {isAuthenticated ? <UserMenu /> : <LoginButton />} </div>
Tailwind CSS 반응형 브레이크포인트:
css/* 모바일 우선 접근법 */ .container { /* 기본 (모바일): 320px+ */ padding: 1rem; } @media (min-width: 640px) { /* sm: 640px+ */ .container { padding: 1.5rem; } } @media (min-width: 768px) { /* md: 768px+ */ .container { padding: 2rem; } } @media (min-width: 1024px) { /* lg: 1024px+ */ .container { padding: 2rem 3rem; } }
학습한 핵심 개념:
- 모바일 우선: 작은 화면부터 시작하여 점진적 향상
- 브레이크포인트 전략:
sm:,md:,lg:접두사로 화면별 스타일 - 콘텐츠 우선순위: 중요한 기능부터 표시하고 부가 기능은 큰 화면에서 추가
- 터치 친화적: 모바일에서 충분한 터치 영역 확보
접근성 고려사항
typescript// 접근성을 고려한 컴포넌트 구현 <Button variant="outline" size="icon"> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">테마 변경</span> {/* 스크린 리더 전용 텍스트 */} </Button> <Link href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer" // 보안 강화 className="text-muted-foreground transition-colors hover:text-foreground" > <span className="sr-only">GitHub</span> {/* 스크린 리더 지원 */} <GithubIcon className="h-5 w-5" /> </Link>
학습 포인트:
- 스크린 리더 지원:
sr-only클래스로 시각적으로는 숨기고 스크린 리더에는 제공 - 의미론적 HTML:
<nav>,<main>,<header>,<footer>태그 활용 - 키보드 네비게이션: 모든 인터랙티브 요소에 키보드 접근 가능
- 색상 대비: WCAG 가이드라인 준수하는 색상 조합
고민했던 부분과 해결책
1. 레이아웃 중앙 정렬 vs 전체 너비 활용
문제: Header에서 로고와 메뉴를 양쪽 끝에 배치하면서도 콘텐츠 최대 너비 제한
시도한 방식들:
- Container 클래스 사용 (문제 발생):
typescript// ❌ 중앙 정렬로 인해 양쪽 끝 배치 불가 <div className="container flex items-center justify-between"> <Logo /> <UserMenu /> </div>
- Max-width와 Margin Auto (선택된 방식):
typescript// ✅ 양쪽 끝 배치와 최대 너비 제한 동시 달성 <div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"> <Logo /> <UserMenu /> </div>
- Grid 레이아웃 (복잡함):
typescript// 🤔 가능하지만 불필요하게 복잡 <div className="grid grid-cols-3 items-center max-w-7xl mx-auto"> <Logo /> <div></div> {/* 빈 공간 */} <UserMenu /> </div>
학습한 내용:
- Flexbox vs Grid: 1차원 레이아웃에는 Flexbox가 더 적합
- 반응형 패딩:
px-4 sm:px-6 lg:px-8로 화면 크기별 여백 조정 - 최대 너비 제한:
max-w-7xl로 초대형 화면에서도 가독성 유지
2. 테마 전환 시 깜빡임 현상
문제: 페이지 로드 시 테마가 적용되기 전 잠깐 기본 테마가 보이는 현상
발생 원인:
typescript// 클라이언트 사이드에서 테마 적용으로 인한 지연 useEffect(() => { // 이 시점에서 테마가 적용되어 깜빡임 발생 setTheme(savedTheme); }, []);
해결책:
typescript// next-themes의 suppressHydrationWarning 사용 <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange // 전환 시 애니메이션 비활성화 suppressHydrationWarning // 하이드레이션 경고 억제 > {children} </ThemeProvider>
추가 최적화:
typescript// 테마 스크립트를 head에 인라인으로 삽입 (향후 개선) const themeScript = ` (function() { const theme = localStorage.getItem('theme') || 'system'; if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } })(); `;
학습한 내용:
- 하이드레이션 이슈: 서버와 클라이언트 렌더링 불일치 문제
- 블로킹 스크립트: 테마 적용을 위한 인라인 스크립트의 필요성
- 사용자 경험: 깜빡임 없는 부드러운 테마 전환의 중요성
3. 모바일 네비게이션 메뉴 구현
문제: 작은 화면에서 모든 메뉴를 표시할 공간 부족
고려한 옵션들:
- 햄버거 메뉴 (향후 구현 예정):
typescript// 🔮 Phase 4에서 구현 예정 const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); return ( <> <Button variant="ghost" size="icon" className="md:hidden" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} > <Menu className="h-5 w-5" /> </Button> {isMobileMenuOpen && ( <MobileMenu onClose={() => setIsMobileMenuOpen(false)} /> )} </> );
- 탭 네비게이션 (현재 방식):
typescript// ✅ 현재: 핵심 메뉴만 표시, 나머지는 숨김 <nav className="hidden md:flex items-center space-x-6"> <Link href="/posts">글 목록</Link> <Link href="/about">소개</Link> {isAdmin && <Link href="/admin">관리자</Link>} </nav>
- Bottom Navigation (모바일 앱 스타일):
typescript// 🤔 블로그에는 부적합 <nav className="fixed bottom-0 left-0 right-0 bg-background border-t md:hidden"> <div className="flex justify-around py-2"> <Link href="/">홈</Link> <Link href="/posts">글</Link> <Link href="/profile">프로필</Link> </div> </nav>
학습한 내용:
- 점진적 개선: 핵심 기능부터 구현하고 점진적으로 확장
- 사용자 패턴: 웹 사용자는 모바일 앱과 다른 네비게이션 패턴 선호
- 우선순위: 모든 기능을 작은 화면에 억지로 넣기보다는 핵심 기능에 집중
4. 컴포넌트 재사용성과 커스터마이징 균형
문제: shadcn/ui 컴포넌트를 프로젝트에 맞게 커스터마이징하면서도 재사용성 유지
접근 방식:
typescript// 기본 shadcn/ui 컴포넌트 확장 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ); } ); // 프로젝트 특화 컴포넌트 생성 const LoginButton = () => ( <Button asChild> <Link href="/auth/login">로그인</Link> </Button> ); const LogoutButton = ({ onLogout }: { onLogout: () => void }) => ( <Button variant="ghost" onClick={onLogout} className="text-red-600 hover:text-red-700" > 로그아웃 </Button> );
학습한 내용:
- 컴포지션 패턴: 기본 컴포넌트를 조합하여 특화된 컴포넌트 생성
- Props 확장: 기본 HTML 속성을 확장하면서 타입 안전성 유지
- 스타일 오버라이드:
cn()함수로 기본 스타일과 커스텀 스타일 병합
기존 Phase에서 활용한 기술
Phase 1-2 기반 기술의 확장
TypeScript 타입 시스템 활용
- Phase 1-2: 기본 인터페이스와 데이터베이스 타입 정의
- Phase 3: 컴포넌트 Props와 이벤트 핸들러 타입 안전성 확보
- 확장 내용: React 컴포넌트 타입과 이벤트 타입 완전 활용
React Query 인증 상태 연동
- Phase 2: 인증 상태 관리 시스템 구축
- Phase 3: UI 컴포넌트에서 인증 상태 기반 조건부 렌더링
- 확장 내용:
useAuth훅을 통한 실시간 UI 업데이트
Tailwind CSS 디자인 시스템 완성
- Phase 1: 기본 Tailwind CSS 설정
- Phase 3: 완전한 디자인 토큰과 컴포넌트 시스템 구축
- 확장 내용: CSS 변수 기반 테마 시스템과 반응형 디자인
핵심 의사결정과 그 이유
1. shadcn/ui vs 다른 UI 라이브러리
결정: shadcn/ui 지속 사용
이유:
- Phase 1 연속성: 기존 설정과 학습 곡선 활용
- 커스터마이징 자유도: 소스 코드 직접 수정 가능
- 번들 크기: 필요한 컴포넌트만 포함하여 최적화
- 접근성: Radix UI 기반으로 웹 접근성 자동 보장
- 타입 안전성: TypeScript와 완벽한 통합
2. 테마 시스템: CSS-in-JS vs CSS 변수
결정: CSS 변수 기반 테마 시스템
이유:
- 성능: 런타임 스타일 계산 없이 CSS 엔진 레벨에서 처리
- SSR 호환성: 서버 사이드 렌더링과 완벽 호환
- 브라우저 지원: 모던 브라우저에서 네이티브 지원
- 개발자 도구: 브라우저 DevTools에서 쉽게 디버깅 가능
- 번들 크기: JavaScript 런타임 오버헤드 없음
3. 모바일 네비게이션: 햄버거 메뉴 vs 숨김
결정: 현재는 메뉴 숨김, 향후 햄버거 메뉴 구현
이유:
- 단계적 구현: 핵심 기능부터 구현하고 점진적 확장
- 사용자 패턴: 블로그는 콘텐츠 소비가 주목적이므로 복잡한 네비게이션 불필요
- 성능: 추가 JavaScript 없이 CSS만으로 반응형 구현
- 접근성: 키보드 네비게이션과 스크린 리더 지원 용이
4. 컴포넌트 구조: Atomic Design vs Feature-based
결정: Atomic Design 원칙 적용
이유:
- 재사용성: 작은 컴포넌트부터 큰 컴포넌트로 조합
- 일관성: 디자인 시스템의 일관된 적용
- 유지보수성: 변경 사항이 전체 시스템에 일관되게 적용
- 확장성: 새로운 페이지와 기능 추가 시 기존 컴포넌트 재사용
성능 및 접근성 고려사항
성능 최적화
컴포넌트 최적화
typescript// React.memo로 불필요한 리렌더링 방지 const Header = React.memo(() => { const { user, isAuthenticated, isAdmin } = useAuth(); return ( <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur"> {/* 헤더 내용 */} </header> ); }); // useCallback으로 함수 참조 안정화 const ThemeToggle = () => { const { setTheme } = useTheme(); const handleThemeChange = useCallback((theme: string) => { setTheme(theme); }, [setTheme]); return ( <DropdownMenu> {/* 테마 토글 내용 */} </DropdownMenu> ); };
CSS 최적화
css/* 하드웨어 가속 활용 */ .theme-toggle-icon { transform: translateZ(0); /* GPU 레이어 생성 */ transition: transform 0.2s ease-in-out; } /* 중요한 스타일 우선 로딩 */ @layer base { /* 기본 스타일 */ } @layer components { /* 컴포넌트 스타일 */ } @layer utilities { /* 유틸리티 스타일 */ }
접근성 강화
키보드 네비게이션
typescript// 키보드 이벤트 처리 const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleClick(); } }; // 포커스 관리 const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; const firstFocusableElement = modal.querySelectorAll(focusableElements)[0]; const lastFocusableElement = modal.querySelectorAll(focusableElements)[ modal.querySelectorAll(focusableElements).length - 1 ];
ARIA 속성 활용
typescript<Button aria-label="테마 변경" aria-expanded={isOpen} aria-haspopup="menu" onClick={toggleTheme} > <Sun className="h-4 w-4" /> <span className="sr-only">테마 변경</span> </Button> <nav role="navigation" aria-label="메인 네비게이션"> <ul> <li><Link href="/posts" aria-current={pathname === '/posts' ? 'page' : undefined}>글 목록</Link></li> <li><Link href="/about" aria-current={pathname === '/about' ? 'page' : undefined}>소개</Link></li> </ul> </nav>
향후 개선 방향
1. 모바일 사용자 경험 향상
햄버거 메뉴 구현
typescript// 모바일 네비게이션 메뉴 const MobileMenu = ({ isOpen, onClose }: MobileMenuProps) => { return ( <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, x: '100%' }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: '100%' }} className="fixed inset-y-0 right-0 z-50 w-full max-w-sm bg-background shadow-lg md:hidden" > <nav className="flex flex-col space-y-4 p-6"> <Link href="/posts" onClick={onClose}>글 목록</Link> <Link href="/about" onClick={onClose}>소개</Link> {isAdmin && <Link href="/admin" onClick={onClose}>관리자</Link>} </nav> </motion.div> )} </AnimatePresence> ); };
터치 제스처 지원
typescript// 스와이프 제스처로 메뉴 열기/닫기 const useSwipeGesture = (onSwipeLeft: () => void, onSwipeRight: () => void) => { const [touchStart, setTouchStart] = useState<number | null>(null); const [touchEnd, setTouchEnd] = useState<number | null>(null); const minSwipeDistance = 50; const onTouchStart = (e: TouchEvent) => { setTouchEnd(null); setTouchStart(e.targetTouches[0].clientX); }; const onTouchMove = (e: TouchEvent) => { setTouchEnd(e.targetTouches[0].clientX); }; const onTouchEnd = () => { if (!touchStart || !touchEnd) return; const distance = touchStart - touchEnd; const isLeftSwipe = distance > minSwipeDistance; const isRightSwipe = distance < -minSwipeDistance; if (isLeftSwipe) onSwipeLeft(); if (isRightSwipe) onSwipeRight(); }; return { onTouchStart, onTouchMove, onTouchEnd }; };
2. 애니메이션 시스템 구축
Framer Motion 통합
typescript// 페이지 전환 애니메이션 const PageTransition = ({ children }: { children: React.ReactNode }) => { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3, ease: 'easeInOut' }} > {children} </motion.div> ); }; // 컴포넌트 등장 애니메이션 const FadeInUp = ({ children, delay = 0 }: FadeInUpProps) => { return ( <motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay, ease: 'easeOut' }} > {children} </motion.div> ); };
3. 고급 테마 시스템
커스텀 테마 생성기
typescript// 사용자 정의 테마 색상 interface CustomTheme { name: string; colors: { primary: string; secondary: string; accent: string; background: string; foreground: string; }; } const ThemeCustomizer = () => { const [customTheme, setCustomTheme] = useState<CustomTheme>(); const applyCustomTheme = (theme: CustomTheme) => { const root = document.documentElement; Object.entries(theme.colors).forEach(([key, value]) => { root.style.setProperty(`--${key}`, value); }); }; return ( <div className="space-y-4"> <ColorPicker label="Primary Color" value={customTheme?.colors.primary} onChange={(color) => updateThemeColor('primary', color)} /> {/* 다른 색상 선택기들 */} </div> ); };
4. 컴포넌트 문서화
Storybook 통합
typescript// Button.stories.tsx export default { title: 'UI/Button', component: Button, parameters: { layout: 'centered', }, tags: ['autodocs'], } satisfies Meta<typeof Button>; export const Default: Story = { args: { children: '기본 버튼', }, }; export const Variants: Story = { render: () => ( <div className="flex space-x-2"> <Button variant="default">Default</Button> <Button variant="secondary">Secondary</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> </div> ), };
결론
Phase 3 기본 UI 컴포넌트 및 레이아웃 시스템 구축을 통해 현대적이고 접근 가능한 사용자 인터페이스의 견고한 기반을 마련할 수 있었습니다.
특히 shadcn/ui와 Tailwind CSS를 활용한 디자인 시스템을 통해 일관성 있고 확장 가능한 UI 컴포넌트를 구축했으며, next-themes를 활용한 테마 시스템으로 사용자 맞춤형 경험을 제공할 수 있게 되었습니다. 또한 반응형 디자인과 접근성 고려사항을 통해 모든 사용자가 편리하게 사용할 수 있는 인터페이스를 완성했습니다.
컴포넌트 기반 아키텍처와 Atomic Design 원칙을 적용하여 재사용성과 유지보수성을 극대화했으며, 타입 안전한 컴포넌트 시스템을 통해 개발자 경험과 코드 품질을 동시에 향상시켰습니다.
이러한 경험은 향후 대규모 디자인 시스템 구축과 사용자 중심의 인터페이스 설계에서도 활용할 수 있는 실무 역량이 될 것입니다.
다음 단계 (Phase 4)
Phase 4에서 구현할 기능들
1. 사용자 인증 및 권한 관리 시스템
- 로그인/로그아웃 페이지 UI 구현
- 사용자 프로필 관리 페이지
- 관리자 권한 확인 및 보호된 라우트
2. 인증 상태 기반 UI 개선
- 로딩 상태 표시 개선
- 에러 상태 처리 및 사용자 피드백
- 인증 실패 시 적절한 리디렉션
3. 사용자 경험 향상
- 토스트 알림 시스템 구현
- 폼 검증 및 에러 메시지 표시
- 접근성 개선 및 키보드 네비게이션
Phase 3에서 구축한 기반이 Phase 4에서 활용되는 방식:
- 레이아웃 시스템 → 인증 페이지 일관된 디자인 적용
- 테마 시스템 → 로그인 폼과 프로필 페이지 테마 지원
- 컴포넌트 시스템 → 폼 컴포넌트와 버튼 재사용
- 반응형 디자인 → 모바일 친화적 인증 플로우
참고 자료
공식 문서
- shadcn/ui Components - 컴포넌트 사용법과 커스터마이징
- Tailwind CSS - 유틸리티 클래스와 반응형 디자인
- Radix UI - 접근성 중심 headless 컴포넌트
- next-themes - Next.js 테마 시스템
디자인 시스템 & UX
- Atomic Design - 컴포넌트 기반 디자인 시스템
- Material Design - 구글의 디자인 언어 시스템
- Human Interface Guidelines - 애플의 인터페이스 가이드라인
접근성 & 성능
- WCAG 2.1 Guidelines - 웹 접근성 가이드라인
- Web.dev Accessibility - 웹 접근성 모범 사례
- React Performance - React 성능 최적화 가이드
개발 도구
- Lucide Icons - 아이콘 라이브러리
- Framer Motion - React 애니메이션 라이브러리
- Storybook - 컴포넌트 문서화 도구