July 26, 20255 minutes
지난 튜토리얼에서 우리는 React를 사용하여 백엔드 API와 연동되는 프론트엔드를 구축하고, 이를 Docker로 통합하는 과정을 마쳤습니다.
하지만 웹 기술의 발전 속도는 눈부시게 빠르며, 개발자 경험과 애플리케이션 성능을 극대화하는 새로운 도구들이 끊임없이 등장하고 있습니다.
이번 4편은 단순한 기능 구현을 넘어, ‘어떻게 하면 더 효율적이고 전문적으로 프론트엔드를 개발할 수 있는가’에 대한 해답을 제시하는 ‘심화 재구축’ 과정입니다.
우리는 이전의 프론트엔드 코드를 과감히 버리고, 2024년 현재 가장 주목받는 최첨단 기술 스택으로 완전히 새롭게 무장할 것입니다.
‘Vite’의 빛처럼 빠른 개발 서버와 빌드 속도, ‘TanStack(Router & Query)‘이 제공하는 완벽한 타입-세이프 라우팅과 선언적 데이터 관리, ‘Zustand’의 놀랍도록 간결한 전역 상태 관리, 그리고 ‘shadcn/ui’와 ‘Tailwind CSS’를 통한 미려하고 일관성 있는 UI 시스템까지.
이 모든 것을 유기적으로 결합하여, 단순히 동작하는 애플리케이션이 아닌, 유지보수성이 뛰어나고 성능이 최적화된 프로덕션급 웹 애플리케이션을 만드는 전 과정을 경험하게 될 것입니다.
먼저, 프로젝트의 기반을 완전히 새롭게 다지겠습니다.
‘Vite를 이용한 TypeScript 기반 React 프로젝트 생성’
프로젝트 루트 디렉터리에서 다음 명령어로 Vite 기반의 React + TypeScript 프로젝트를 생성합니다.
npx create-vite@latest frontend -- --template react-ts‘핵심 라이브러리 설치’
새로 생성된 frontend 디렉터리로 이동하여 필요한 모든 라이브러리를 설치합니다.
cd frontend
# UI 및 스타일링
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# 상태 관리 및 라우팅, 데이터 페칭
npm install zustand @tanstack/react-router @tanstack/react-query axios better-react-auth
# shadcn/ui 초기 설정
npx shadcn-ui@latest init인증 상태는 앱 전반에서 사용되므로, 간결한 전역 상태 관리 라이브러리인 Zustand와 인증 라이브러리를 결합하여 관리합니다.
‘Zustand 스토어 생성 (frontend/src/store/authStore.js)’
로그인 상태와 토큰 정보를 담을 간단한 스토어를 만듭니다.
import { create } from 'zustand';
const useAuthStore = create((set) => ({
  token: localStorage.getItem('token') || null,
  setToken: (token) => {
    if (token) {
      localStorage.setItem('token', token);
    } else {
      localStorage.removeItem('token');
    }
    set({ token });
  },
}));
export default useAuthStore;‘인증 컨텍스트 제공 (frontend/src/App.tsx)’
better-react-auth의 AuthProvider를 사용하여 앱 전체에 인증 관련 함수를 제공합니다.
// ... imports
import { AuthProvider } from 'better-react-auth';
import useAuthStore from './store/authStore';
import apiClient from './api/client';
function App() {
  const { setToken } = useAuthStore();
  const login = async (credentials) => {
    const { data } = await apiClient.post('/login', credentials);
    setToken(data.token);
  };
  const logout = () => {
    setToken(null);
  };
  
  // ...
  return (
    <AuthProvider authClient={{ login, logout, register: ... }}>
      {/* 라우터 컴포넌트가 위치할 곳 */}
    </AuthProvider>
  );
}TanStack Router는 파일 기반이 아닌 코드 기반의 완전한 타입-세이프 라우팅을 제공하여, 경로와 파라미터를 안전하게 관리할 수 있게 해줍니다.
‘라우트 트리 정의 (frontend/src/routeTree.gen.ts는 자동생성)’
src/routes 디렉터리에 각 라우트 파일을 정의하면, TanStack Router가 이를 감지하여 타입-세이프한 라우트 트리를 자동으로 생성합니다.
‘루트 라우트 설정 (frontend/src/routes/__root.tsx)’
모든 라우트의 기반이 되는 최상위 레이아웃을 정의합니다.
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { useAuth } from 'better-react-auth';
export const Route = createRootRoute({
  component: () => {
    const { user } = useAuth(); // 인증 상태 확인
    return (
      <>
        {/* 네비게이션 바 등 공통 UI */}
        <hr />
        <Outlet />
      </>
    );
  },
});beforeLoad 옵션을 사용하여, 인증되지 않은 사용자가 이 경로에 접근하려고 하면 로그인 페이지로 리디렉션시킵니다.
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/memos')({
  beforeLoad: ({ context, location }) => {
    // context.auth는 AuthProvider에서 주입된 상태입니다.
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: {
          redirect: location.href,
        },
      });
    }
  },
  component: MemosPage,
});
function MemosPage() {
  // 메모 페이지 컴포넌트
  return <div>메모 페이지</div>;
}로딩, 에러, 상태 관리를 직접 하던 방식에서 벗어나, TanStack Query를 통해 데이터를 선언적으로 관리합니다.
‘메모 페이지 완성 (frontend/src/routes/memos.tsx)’
useQuery로 데이터를 가져오고, useMutation으로 데이터를 변경하며, shadcn/ui로 멋진 UI를 구성합니다.
// ... MemosPage 컴포넌트 내부
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../api/client';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
// API 호출 함수
const fetchMemos = async () => (await apiClient.get('/memos')).data;
const addMemo = async (newMemo) => (await apiClient.post('/memos', newMemo)).data;
const deleteMemo = async (id) => (await apiClient.delete(`/memos/${id}`)).data;
function MemosPage() {
  const queryClient = useQueryClient();
  // 데이터 조회
  const { data: memos, isLoading } = useQuery({ 
    queryKey: ['memos'], 
    queryFn: fetchMemos 
  });
  // 데이터 추가 (Mutation)
  const mutation = useMutation({
    mutationFn: addMemo,
    onSuccess: () => {
      // 성공 시 'memos' 쿼리를 무효화하여 자동으로 데이터를 다시 불러옵니다.
      queryClient.invalidateQueries({ queryKey: ['memos'] });
    },
  });
  // ... (삭제, 수정 Mutation도 유사하게 구현)
  if (isLoading) return <div>로딩 중...</div>;
  return (
    <Card className="w-[350px]">
      <CardHeader>
        <CardTitle>내 메모장</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={...}>
          <Input placeholder="새 메모..." />
          <Button type="submit">추가</Button>
        </form>
        {/* memos.map(...)을 통해 메모 목록 렌더링 */}
      </CardContent>
    </Card>
  );
}백엔드와 최신 프론트엔드 스택을 하나의 Docker 이미지로 통합합니다.
Vite의 빌드 결과물 디렉터리는 build가 아닌 dist라는 점에 유의해야 합니다.
‘백엔드 수정 (main.go)’
정적 파일을 서빙하는 경로를 build에서 dist로 변경합니다.
// ... main.go
    // React 정적 파일 서빙 경로를 'dist'로 변경합니다.
    router.Static("/assets", "./frontend/dist/assets") // Vite는 보통 assets 폴더를 사용합니다.
    router.StaticFile("/", "./frontend/dist/index.html")
    router.StaticFile("/favicon.ico", "./frontend/dist/favicon.ico")
    // 클라이언트 사이드 라우팅을 위한 대체 경로 설정
    router.NoRoute(func(c *gin.Context) {
        c.File("./frontend/dist/index.html")
    })
// ...‘최종 Dockerfile 작성’
프론트엔드 빌드 스테이지가 Vite 환경에 맞게 수정됩니다.
# Dockerfile
# --- 1단계: 백엔드 빌드 환경 ---
FROM golang:1.21-alpine AS go-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o /go/bin/memo-api .
# --- 2단계: 프론트엔드 빌드 환경 (Vite) ---
FROM node:20-alpine AS fe-builder
WORKDIR /app
# frontend 디렉터리의 package.json, package-lock.json 복사
COPY frontend/package*.json ./
# 의존성 설치 (npm ci는 package-lock.json을 사용해 더 빠르고 일관된 설치를 보장)
RUN npm ci
# frontend 소스 코드 전체 복사
COPY frontend/ ./
# Vite를 사용하여 React 앱 빌드
RUN npm run build
# --- 3단계: 최종 실행 환경 ---
FROM alpine:latest
WORKDIR /app
# Go 빌더에서 컴파일된 바이너리 복사
COPY --from=go-builder /go/bin/memo-api .
# React 빌더에서 빌드된 정적 파일들을 복사 (Vite의 결과물은 'dist' 폴더)
COPY --from=fe-builder /app/dist ./frontend/dist
# 애플리케이션이 8080 포트를 사용함을 명시
EXPOSE 8080
# 컨테이너 시작 시 Go 서버 실행
CMD ["./memo-api"]ㄴ 이번 4편에서는 기존의 프론트엔드 코드를 완전히 버리고, Vite, TanStack, Zustand, shadcn/ui 등 현존하는 가장 현대적이고 강력한 도구들을 사용하여 애플리케이션을 재탄생시켰습니다.