[업무 후 웹앱 공부] Vite + React + FastAPI 환경 구성

목표

업무 후에 Vite + React + FastAPI를 사용하면서 실질적으로 궁금한 것들을 테스트 해보는 용도로 환경을 만들어 두려한다.

 

기본 환경 설정

  • frontend/ 폴더에 Vite 기반 React + TypeScript 템플릿 생성.
  • 아래 커맨드를 실행해준다.
cd <원하는 폴더>
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm run dev

 

그럼 이렇게 기본 5173 포트로 React Template이 담긴 페이지가 뜬다. 

 


 

프론트-백엔드 연동 환경설정

  • frontend/ 폴더에 Vite 기반 React + TypeScript 템플릿이 있고, backend/ 폴더에 파이썬으로 된 기본적인 백엔드가 있으며, docker-compose.yml 이용하여 간단하게 빌드하는 환경 만들기

 

파일

오늘 해보는 실습의 코드는 아래 레포에 있다.

코드에서 알 수 있는 주요한 특징만 글로 정리하고자 한다.

https://github.com/i-zro/react-250701/tree/gh-pages/250923-base

 

react-250701/250923-base at gh-pages · i-zro/react-250701

Contribute to i-zro/react-250701 development by creating an account on GitHub.

github.com

 

Docker로 빌드 구성

먼저 docker로 기본 빌드 구성을 만들어줬으며, 아래는 각각 프론트/백엔드 Dockerfile이다.

# 프론트엔드 Dockerfile
FROM node:20-alpine

# 작업 디렉토리 설정
WORKDIR /app

# package.json과 package-lock.json 복사
COPY package*.json ./

# 의존성 설치
RUN npm install

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 5173

# 개발 서버 실행
CMD ["npm", "run", "dev", "--", "--host"]
# 백엔드 Dockerfile
FROM python:3.11-slim

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 파일 복사
COPY requirements.txt .

# 의존성 설치
RUN pip install --no-cache-dir -r requirements.txt

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 8000

# FastAPI 서버 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

 

docker-compose.yml은 아래와 같다.

version: '3.8'

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
    environment:
      - PYTHONPATH=/app
    networks:
      - app-network
    restart: unless-stopped

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - VITE_API_URL=http://localhost:8000
    networks:
      - app-network
    depends_on:
      - backend
    restart: unless-stopped

networks:
  app-network:
    driver: bridge

 

프론트 톺아보기

가장 먼저 진입하는 파일은 index.html 파일이다.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

 

react 화면 자체는 tsx 속 function의 return으로 반환

다시, 이 index.html 파일은 /src/main.tsx를 본다. 다시 main.tsx를 가보면 App.tsx를 반환함을 알 수 있다.

App.tsx에서 먼저 return을 보자.

알 수 있는 점은, return을 통해 우리가 보는 화면을 렌더해준다는 것이다.

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React + FastAPI</h1>
      <div className="card">
        {error && <p style={{ color: 'red' }}>{error}</p>}
        <p>현재 카운트: <strong>{count}</strong></p>
        <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
          <button onClick={incrementCount} disabled={loading}>
            {loading ? '로딩...' : '카운트 증가'}
          </button>
          <button onClick={resetCount} disabled={loading}>
            {loading ? '로딩...' : '리셋'}
          </button>
          <button onClick={fetchCount} disabled={loading}>
            {loading ? '로딩...' : '새로고침'}
          </button>
        </div>
        <p>
          백엔드 API와 연동된 카운터입니다
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>

 

다음으로, count라는 변수를 쓴 게 보일 것이다.

 

const [현재 state, state 를 변경하기 위한 함수] = useState()

const [count, setCount] = useState(0)

주로 이런 식으로 쓰는데, [현재 state, state 를 변경하기 위한 함수]

이며 변수값을 셋팅해 주는 역할을 한다.

 

백엔드 통신함수는 아래와 같다.

  const fetchCount = async () => {
    try {
      setLoading(true)
      const response = await fetch(`${API_URL}/api/count`)
      if (!response.ok) throw new Error('Failed to fetch count')
      const data = await response.json()
      setCount(data.count)
      setError(null)
    } catch (err) {
      setError('백엔드 연결에 실패했습니다')
      console.error('Error fetching count:', err)
    } finally {
      setLoading(false)
    }
  }

 

  • setLoading(true): 버튼을 비활성화하고 “로딩…” 문구를 보이게 함(사용자 연타 방지).
  • fetch(url): 브라우저의 표준 HTTP 요청 함수.
  • response.ok: 상태코드 200~299면 true. 아니면 throw로 에러 처리 진입.
  • await response.json(): JSON 본문 파싱.
  • 성공 시 setCount(data.count)로 UI 업데이트, setError(null)로 에러 문구 제거.
  • 실패 시 사용자에게 보여줄 한글 에러 메시지를 setError(...)에 저장하고, 상세 원인은 console.error에 남김.
  • finally: 성공/실패와 무관하게 항상 loading을 false로 돌려놓음.

 

즉, 백엔드에 현재 count 변수를 주면,  백엔드에서는 해당 변수를 기반으로 돌려주는 식으로 초기 실습을 구성하였다.