팀 프로젝트 기술 스택 기본 정리 (Vite · React 19 · TypeScript · Tailwind v4 · React Router · TanStack Query · React Compiler)

Alchemist·2026년 2월 9일

팀 프로젝트 기술 스택 기본 정리 (Vite · React 19 · TypeScript · Tailwind v4 · React Router · TanStack Query · React Compiler)

목적: 팀 프론트엔드 프로젝트를 시작할 때 각 기술의 역할/개념/필수 용어/설정 포인트/실무 주의사항을 한 문서로 정리했다.
범위: 아래 7개 스택의 “기본적으로 알아야 하는 내용”을 가능한 한 빠짐없이 포함했다.


0. 한 줄 역할 맵

  • Vite: 개발 서버/빌드(번들링, HMR)
  • React 19: UI 컴포넌트 기반 렌더링(상태 기반)
  • TypeScript: 타입 안정성(협업/리팩토링 안전)
  • Tailwind CSS v4: 유틸리티 퍼스트 스타일링(빠른 UI/일관성)
  • React Router: URL 기반 화면 구성(레이아웃/중첩 라우트)
  • TanStack Query: 서버 데이터 패칭/캐싱/동기화 표준
  • React Compiler: 렌더 최적화 자동화(자동 memoization 방향)

1) Vite

1-1. Vite가 뭔가

  • 프론트엔드 빌드 도구(Dev Server + Bundler).
  • 개발 중에는 ESM(ECMAScript Modules) 기반으로 빠르게 로딩하고,
  • 빌드 시에는 Rollup 기반 번들링을 수행한다.

1-2. 왜 쓰나

  • 개발 서버가 매우 빠름: 필요한 모듈만 즉시 서빙(필요할 때만 변환).
  • HMR(Hot Module Replacement)이 빠르고 안정적.
  • React/TS/Tailwind 플러그인 생태계가 좋고 설정이 단순한 편.

1-3. 개발/빌드 동작 방식 (핵심)

개발(Dev)

  • 브라우저가 import 하는 모듈을 Vite dev server가 그때그때 변환해서 제공.
  • 전체 번들링을 매번 하지 않으니 시작/수정 반영이 빠름.

프로덕션 빌드(Build)

  • Rollup으로 번들 파일 생성, 코드 스플리팅(청크 분리), 트리쉐이킹 적용.
  • 보통 dist/에 산출물 생성(설정에 따라 변경 가능).

1-4. 프로젝트에서 꼭 아는 설정 포인트

  • vite.config.ts에서 플러그인/빌드 옵션/별칭(alias)/프록시(proxy) 등을 설정한다.
  • 팀 프로젝트에서 특히 자주 만지는 것:
    • base: 서브 디렉토리 배포 시 정적 리소스 경로 대응
    • server.proxy: 로컬에서 API 프록시(CORS 회피)
    • resolve.alias: @/ 별칭 통일

예시(개념용):

    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";

    export default defineConfig({
        base: "/",
        plugins: [react()],
        resolve: {
            alias: {
                "@": "/src",
            },
        },
        server: {
            proxy: {
                "/api": {
                    target: "https://example.com",
                    changeOrigin: true,
                },
            },
        },
    });

1-5. 환경변수 기본

  • Vite는 import.meta.env를 사용한다.
  • 클라이언트에서 접근 가능한 변수는 기본적으로 VITE_ prefix가 필요하다.
    • 예: VITE_API_BASE_URL, VITE_APP_BASENAME

1-6. 흔한 실수

  • base 미설정으로 운영에서 CSS/JS 404
  • SPA 라우팅인데 서버에서 index.html 리라이트 설정이 없어 새로고침 404
  • .env 수정 후 dev 서버 재시작 안 해서 환경변수 반영 안 됨

2) React 19

2-1. React가 뭔가

  • UI를 컴포넌트 단위로 구성하는 라이브러리.
  • 상태(state)가 바뀌면 UI를 다시 그린다는 선언적(Declarative) 모델.

2-2. 핵심 개념

컴포넌트

  • 함수 컴포넌트가 기본.
  • props로 입력을 받고 JSX를 반환한다.

상태(state)

  • useState 등으로 관리.
  • state가 바뀌면 해당 컴포넌트가 리렌더링된다.

렌더링/리렌더

  • 리렌더는 “DOM을 다 갈아엎는다”가 아니라, 변경점을 계산해 DOM에 반영한다.
  • 다만 컴포넌트 함수 자체는 다시 실행되므로 렌더 안에서 만드는 객체/함수는 매번 새로 만들어질 수 있다(성능 포인트).

이펙트(effect)

  • useEffect는 렌더 결과가 화면에 반영된 뒤 실행된다.
  • 데이터 요청/구독/DOM 핸들링 같은 “렌더 외부 작업”에 사용한다.

2-3. 실무에서 자주 나오는 포인트

  • 서버에서 오는 데이터는 useState에 복사해서 들고 있기보다 React Query로 관리하는 편이 안전하다(싱크 이슈 감소).
  • 상태 업데이트에서 이전 값을 기반으로 할 때는 함수형 업데이트를 사용한다.
    setCount((prev) => prev + 1);

2-4. 흔한 실수

  • useEffect 의존성 배열을 잘못 잡아서 무한 루프
  • 렌더 중에 무거운 계산/정렬/필터를 매번 수행(메모/컴파일러/구조 개선 고려)

3) TypeScript

3-1. TypeScript가 뭔가

  • JavaScript에 정적 타입 시스템을 추가한 언어(컴파일하면 JS).
  • 런타임이 아니라 개발/빌드 시점에 타입 에러를 잡는다.

3-2. 왜 쓰나

  • 협업/유지보수에서:
    • API 데이터 형태 변경을 컴파일 에러로 감지
    • 컴포넌트 props 계약 명확화
    • 리팩토링 안전성 증가

3-3. 기본 타입 개념 (필수)

  • 타입 추론(type inference)
  • 타입 선언(type annotation)
  • 유니온/인터섹션: A | B, A & B
  • 제네릭(generics): 타입을 매개변수로 받는 패턴(React Query, 공용 컴포넌트)
  • 타입 vs 인터페이스
    • 복잡한 타입 연산/유니온: type 선호
    • 객체 확장/선언 병합: interface 고려

3-4. React에서 자주 쓰는 TS 패턴

Props 타입

    type ButtonProps = {
        label: string;
        onClick: () => void;
        disabled?: boolean;
    };

    export function Button(props: ButtonProps) {
        return (
            <button disabled={props.disabled} onClick={props.onClick}>
                {props.label}
            </button>
        );
    }

이벤트 타입

    function onChange(e: React.ChangeEvent<HTMLInputElement>) {
        console.log(e.target.value);
    }

API 응답 타입

    type User = {
        id: string;
        name: string;
    };

    async function fetchUser(): Promise<User> {
        const res = await fetch("/api/user");
        if (!res.ok) throw new Error("Failed");
        return (await res.json()) as User;
    }

3-5. 흔한 실수/팁

  • any 남발 금지(가능하면 unknown + 타입가드)
  • API 응답을 무조건 as Type로 단언하면 런타임 오류를 놓칠 수 있음(필요 시 검증 레이어 고려)

4) Tailwind CSS v4 (@tailwindcss/vite)

4-1. Tailwind가 뭔가

  • 유틸리티 클래스를 조합해 UI를 만드는 유틸리티-퍼스트 CSS 프레임워크.
  • 예: bg-black text-white px-4 py-2 rounded

4-2. 왜 쓰나

  • 스타일링 속도가 빠르고, 컴포넌트와 스타일이 가까워 유지보수가 쉬운 경우가 많다.
  • 반응형/상태(hover/focus)/다크모드 패턴을 빠르게 적용 가능.
  • 팀에서 규칙을 잡으면 UI 통일이 쉬움.

4-3. v4에서 알아둘 포인트

  • 프로젝트 구성에 따라 다르지만, src/index.css에 아래처럼 단순하게 시작하는 형태가 많다.
    @import "tailwindcss";
  • Vite 통합은 @tailwindcss/vite 플러그인을 통해 이뤄진다.

4-4. 핵심 사용 규칙

반응형

    <div className="p-4 md:p-8 lg:p-12">...</div>

상태

    <button className="bg-black text-white hover:opacity-80 disabled:opacity-40">
        Save
    </button>

다크모드

    <div className="bg-white text-black dark:bg-neutral-900 dark:text-white">
        ...
    </div>

4-5. 흔한 실수/팁

  • className이 너무 길어짐 → 공용 UI로 추출하거나 cn() 유틸로 조건부 결합
  • 디자인 토큰(간격/라운드/텍스트 크기)을 정하지 않으면 화면마다 제각각이 되기 쉬움

5) React Router (createBrowserRouter)

5-1. React Router가 뭔가

  • SPA에서 URL에 따라 컴포넌트를 렌더링하는 라우팅 라이브러리.
  • createBrowserRouter는 라우트 트리를 객체로 선언하고 RouterProvider로 주입한다.

5-2. 기본 구성 예시

    import { createBrowserRouter } from "react-router-dom";
    import App from "./App";

    export const router = createBrowserRouter([
        { path: "/", element: <App /> },
        { path: "/about", element: <div>안녕~</div> },
    ]);
    import { RouterProvider } from "react-router-dom";
    import { router } from "./routes";

    export function Root() {
        return <RouterProvider router={router} />;
    }

5-3. 기본적으로 알아야 할 개념

  • Route: pathelement로 매핑
  • Nested Routes(중첩 라우트): 레이아웃 공유 시 사용, <Outlet />로 자식 렌더
  • Loader/Action(데이터 라우터 기능): 라우트 레벨 데이터 처리(팀 정책에 따라 사용 여부 결정)

6) @tanstack/react-query (TanStack Query)

6-1. React Query가 뭔가

  • React에서 “서버 상태(server state)”를 관리하기 위한 라이브러리.
  • 패칭(fetch), 캐싱(cache), 동기화(sync), 재시도(retry), 백그라운드 갱신을 표준화한다.

6-2. 왜 필요한가

React 기본 state로 서버 데이터를 관리하면:

  • 로딩/에러/성공 상태를 매번 수동 구현
  • 캐싱/중복요청 방지/재시도/갱신 전략을 직접 구현
  • 여러 화면에서 같은 데이터를 쓸 때 동기화가 어렵다

React Query는 이걸 규칙으로 해결한다.

6-3. 가장 중요한 개념들(필수)

  • Query: 읽기 요청(조회)
  • Mutation: 쓰기 요청(생성/수정/삭제)
  • Query Key: 캐시 식별자(배열 형태 권장)
  • staleTime / gcTime(cacheTime): 신선도/캐시 유지 시간
  • refetch 옵션: mount/focus/reconnect 시 재요청 제어

6-4. 기본 사용 패턴

QueryClientProvider

    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

    const queryClient = new QueryClient();

    export function Root() {
        return (
            <QueryClientProvider client={queryClient}>
                {/* router 등 */}
            </QueryClientProvider>
        );
    }

useQuery

    import { useQuery } from "@tanstack/react-query";

    type User = { id: string; name: string };

    async function fetchUsers(): Promise<User[]> {
        const res = await fetch("/api/users");
        if (!res.ok) throw new Error("Failed to fetch users");
        return (await res.json()) as User[];
    }

    export function Users() {
        const { data, isLoading, error } = useQuery({
            queryKey: ["users"],
            queryFn: fetchUsers,
            staleTime: 60_000,
        });

        if (isLoading) return <div>Loading...</div>;
        if (error) return <div>Error</div>;

        return (
            <ul>
                {data?.map((u) => (
                    <li key={u.id}>{u.name}</li>
                ))}
            </ul>
        );
    }

useMutation + invalidate

    import { useMutation, useQueryClient } from "@tanstack/react-query";

    async function createUser(name: string) {
        const res = await fetch("/api/users", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ name }),
        });
        if (!res.ok) throw new Error("Failed to create user");
        return res.json();
    }

    export function CreateUser() {
        const qc = useQueryClient();

        const mutation = useMutation({
            mutationFn: createUser,
            onSuccess: async () => {
                await qc.invalidateQueries({ queryKey: ["users"] });
            },
        });

        return <button onClick={() => mutation.mutate("Kim")}>Add</button>;
    }

6-5. 실무에서 꼭 정해야 하는 규칙

  • queryKey 네이밍/구조(예: ["liveReserve", "list", params])
  • 에러 처리 UX(토스트/페이지/재시도)
  • 공통 fetch 래퍼(인증/에러 파싱/상태 코드 처리)

7) React Compiler (babel-plugin-react-compiler)

7-1. React Compiler가 뭔가

  • React 코드를 분석해 자동 memoization 최적화를 적용하려는 컴파일러.
  • 목표: 개발자가 useMemo, useCallback, React.memo를 “언제 붙일지” 덜 고민해도 되게.

7-2. 핵심 아이디어(쉽게)

  • 컴포넌트는 렌더 때마다 함수가 실행되므로:
    • 렌더 안에서 만든 객체/배열/함수는 매번 새 참조가 생길 수 있고
    • props로 내려가면 자식 리렌더가 늘어날 수 있다
  • 컴파일러는 “이 값은 변하지 않는다”를 분석해 참조를 안정적으로 재사용하도록 변환한다(가능한 경우).

7-3. 프로젝트에서 어떻게 적용되나

  • Vite의 React 플러그인(Babel 단계)에 babel-plugin-react-compiler로 포함된다.
  • 개발자는 별도 코드를 추가하지 않아도 빌드 단계에서 최적화 코드가 생성된다.

7-4. 장점

  • 수동 최적화(useMemo/useCallback)로 인한 복잡도를 줄일 수 있음
  • 컴포넌트가 많아질수록 불필요한 리렌더를 줄일 가능성

7-5. 주의할 점(실무)

  • 컴파일러는 “분석”을 하므로:
    • 일부 코드 패턴에서 제약/예상치 못한 동작이 있을 수 있음
    • 빌드/디버깅 경험에 영향이 있을 수 있음
  • “무조건 빨라진다”는 보장은 없음
    → 렌더 구조/데이터량/리스트 크기 같은 근본 병목이 더 중요함

부록 A) 이 스택으로 일할 때 기본 운영 원칙

  • 서버 데이터는 React Query로: 로딩/에러/캐싱/동기화를 표준화
  • 라우팅 경로는 상수화: 문자열 하드코딩 최소화
  • Tailwind 토큰 규칙: 간격/라운드/텍스트 기본값만이라도 팀 합의
  • Vite base/basename: 서브패스 배포 가능성 있으면 초기에 확정(운영 이슈 예방)
  • 타입은 억지로 엄격하게가 아니라 “핵심 경계(API/Props)”부터 잡기

부록 B) 체크리스트(도입/온보딩용)

  • bun install / bun run dev 동작 확인
  • 라우팅 새로고침(직접 URL 진입) 시 404 여부 확인(서버 리라이트)
  • 환경변수 VITE_ prefix 확인 + dev 서버 재시작
  • React Query 전역 Provider 주입 + queryKey 규칙 문서화
  • Tailwind 적용 확인 + 다크모드 정책(선택) 합의
  • 배포 경로(base/basename) 운영 환경과 일치 확인
profile
html_programming_language

0개의 댓글