본문 바로가기

기본 라우팅

1. 기본 라우팅 및 세팅

1.1 기본 라우팅

라우팅은 경로를 동작에 연결하는 것입니다. 이번 챕터에서는 여러 경로를 만들어보고, 각 경로에 대한 응답을 확인해보겠습니다.

경로함수명설명
/read_root{"Hello": "World"}를 반환합니다.
/aboutabout{"message": "about page"}를 반환합니다.
/contactcontact{"message": "contact page"}를 반환합니다.
/noticenotice공지사항 목록을 반환합니다.

라우팅을 만들 때에는 이렇게 먼저 경로와 함수명을 정의하는 것이 좋습니다. 실제 프로젝트에서는 어떻게 테이블을 만드는지 아래 프로젝트를 참고해주세요. 샘플 프로젝트입니다.

샘플 프로젝트

여기에는 다양한 경로, 함수명, 메서드, 권한 등이 정의 되어 있습니다. 우리는 점차 위 테이블을 실무에 가깝게 만들어볼 예정입니다.

1.2 기본 세팅

매번 실습 하는 코드가 달라지고, 각각의 챕터로 바로 오신 분들을 위해 매 챕터마다 새로운 폴더를 만들어서 진행하겠습니다. 이번 실습 폴더는 02_1_route입니다. VSC 터미널에서 사용할 명령어 입니다. 가상환경은 벗어난 상태에서 실행해야 합니다. 앞 실습의 서버가 실행 중이면 Ctrl + C로 멈추고, 터미널 입력창 앞에 (venv)라고 되어 있다면 deactivate 명령어로 가상환경을 나간 상태에서 cd ..으로 상위 폴더로 나와 아래 명령어를 실행해주세요.

mkdir 02_1_route
cd 02_1_route
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install "fastapi[standard]"

저는 Windows로 실습을 하고 있기 때문에 .\venv\Scripts\Activate.ps1로 가상환경을 활성화하고 있습니다. Mac이나 Linux를 사용하시는 분들은 python3 -m venv venv로 가상환경을 만들고 source ./venv/bin/activate로 활성화하시면 됩니다. 이후 명령은 가상환경이 활성화된 상태에서 실행합니다.

02_1_route 폴더 안에 main.py 파일을 생성하고 아래 코드를 입력합니다.

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}

개발 서버를 실행하여 127.0.0.1:8000접속이 되는 것을 확인합니다.

fastapi dev

2. 다양한 URL 요청 처리하기

이번에는 다양한 URL을 만들어보겠습니다. 위에서 정의한 테이블을 참고하여 코드를 작성합니다.

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/about")
def about():
    return {"message": "about page"}


@app.get("/contact")
def contact():
    return {"message": "contact page"}


@app.get("/notice")
def notice():
    return {
        "notice": [
            {"title": "공지사항1", "content": "내용1"},
            {"title": "공지사항2", "content": "내용2"},
            {"title": "공지사항3", "content": "내용3"},
        ]
    }

이 코드를 main.py 파일에 저장합니다. 서버가 이미 실행 중이라면 저장하는 순간 자동으로 다시 시작되므로 별도로 명령을 입력할 필요가 없습니다.

브라우저에서 위에서 정의한 URL을 차례대로 접속합니다. 여기서 직접 클릭해서 확인해보기 위해서는 Ctrl + 클릭을 사용하면 됩니다. Ctrl을 누르지 않고 클릭을 하면 위니북스를 벗어납니다.

  1. http://localhost:8000/
  2. http://localhost:8000/about
  3. http://localhost:8000/contact
  4. http://localhost:8000/notice

각 URL에 접속하면 각각의 응답을 받게 됩니다. 이러한 딕셔너리 형태의 응답을 JSON 응답이라고 합니다. 주로 웹 개발에서 데이터를 주고 받을 때 사용하며 프론트엔드 개발자가 이러한 데이터를 받아 화면을 구성할 수 있습니다.

다만 이렇게 만든 URL을 가지고 앞서 테스트한 about:blank에서 js코드로 테스트할 수는 없습니다. 이는 CORS(Cross-Origin Resource Sharing) 정책 때문입니다. 이 정책은 보안을 위해 브라우저가 다른 도메인으로부터 리소스를 요청할 때 제한을 두는 정책입니다. 이를 해결하기 위해서는 FastAPI에서 CORS를 허용하는 설정을 추가해야 합니다. 이 설정은 지금 필요하지 않으므로 5장에서 추가할 예정입니다.

2.1 HTTP 메서드별 데코레이터

지금까지는 @app.get만 사용했습니다. FastAPI는 HTTP 메서드마다 데코레이터를 제공합니다.

데코레이터HTTP 메서드주 용도
@app.getGET데이터 조회
@app.postPOST새 데이터 생성
@app.putPUT데이터 전체 수정
@app.patchPATCH데이터 일부 수정
@app.deleteDELETE데이터 삭제

브라우저 주소창에 URL을 입력하면 항상 GET 요청이 나갑니다. 그래서 @app.get으로 만든 경로만 브라우저에서 바로 확인할 수 있습니다. 나머지 메서드는 2-3절에서 배울 도구로 테스트합니다.

2.2 경로가 겹치면 위에서부터 찾습니다

FastAPI는 위에서부터 순서대로 경로를 확인하고, 처음 일치하는 것을 실행합니다. 이 성질 때문에 아래 코드는 의도대로 동작하지 않습니다.

@app.get("/notice/{notice_id}")
def notice_detail(notice_id: str):
    return {"notice_id": notice_id}


@app.get("/notice/latest")
def notice_latest():
    return {"message": "가장 최근 공지"}

/notice/latest로 접속하면 두 번째 함수가 아니라 첫 번째 함수가 실행되어 {"notice_id": "latest"}가 나옵니다. {notice_id} 자리에 latest라는 문자열이 들어맞기 때문입니다.

해결책은 고정된 경로를 위에 두는 것입니다.

@app.get("/notice/latest")
def notice_latest():
    return {"message": "가장 최근 공지"}


@app.get("/notice/{notice_id}")
def notice_detail(notice_id: str):
    return {"notice_id": notice_id}

처음 배울 때 잘 드러나지 않다가 프로젝트가 커진 뒤에 이유를 못 찾고 헤매는 문제이니 기억해두시기 바랍니다.

2.3 자동 문서 활용하기

http://localhost:8000/docs 에 접속하면 FastAPI가 자동으로 생성한 API 문서를 확인할 수 있습니다. 이 문서는 Swagger UI를 사용하여 생성되며, API의 엔드포인트, 요청/응답 형식, 파라미터 등을 확인할 수 있습니다. 읽기에 최적화된 ReDoc 문서는 http://localhost:8000/redoc 에서 확인할 수 있으며 http://localhost:8000/openapi.json 에서 JSON 형식으로 확인할 수도 있습니다.

이 세 가지 관계를 정리하면 아래와 같습니다.

이 문서를 더 상세하게 작성하고 싶다면 데코레이터에 정보를 추가하면 됩니다.

from fastapi import FastAPI

app = FastAPI(
    title="위니브 공지 API",
    description="FastAPI 베이스캠프 2장 실습용 API입니다.",
    version="0.1.0",
)


@app.get("/", tags=["기본"], summary="루트", description="Hello World를 반환합니다.")
def read_root():
    return {"Hello": "World"}


@app.get("/notice", tags=["공지"], summary="공지 목록")
def notice():
    """
    공지사항 전체 목록을 반환합니다.

    현재는 메모리에 있는 고정된 값을 반환하며, 4장에서 데이터베이스로 옮깁니다.
    """
    return {"notice": [{"title": "공지사항1", "content": "내용1"}]}
항목문서에서 보이는 위치
title, description, version문서 맨 위 제목 영역
tags엔드포인트를 묶는 그룹 이름
summary엔드포인트 목록에 한 줄로 표시
description 또는 docstring엔드포인트를 펼쳤을 때 나오는 설명

description을 따로 적지 않으면 함수의 docstring이 자동으로 설명이 됩니다. 코드에 주석을 적는 것이 곧 문서를 쓰는 일이 되는 구조입니다.

이제 http://localhost:8000/docs 에 접속하면 위에서 작성한 설명이 문서에 추가된 것을 확인할 수 있습니다.

3. 요청에 다양한 방식으로 응답

3.1 HTML 응답하기

FastAPI는 기본적으로 딕셔너리 형태의 JSON 응답을 반환하는 것이 일반적입니다. 다만, 아래와 같이 HTML 형태로 응답할 수도 있습니다. HTML은 웹 서비스를 만들 때 사용하는 언어입니다.

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.get("/", response_class=HTMLResponse)
def index():
    return "<h1>hello world 1</h1>"


@app.get("/about", response_class=HTMLResponse)
def about():
    return "<h1>hello world 2</h1>"


@app.get("/contact", response_class=HTMLResponse)
def contact():
    return "<h1>hello world 3</h1>"


@app.get("/notice", response_class=HTMLResponse)
def notice():
    return "<h1>hello world 4</h1>"

response_class=HTMLResponse가 없으면 태그가 그대로 글자로 보입니다. 브라우저에게 "이건 HTML이니 해석해서 그려라"라고 알려주는 역할을 합니다.

이번에도 순서대로 URL에 접속하여 응답을 확인해보세요.

  1. http://localhost:8000/
  2. http://localhost:8000/about
  3. http://localhost:8000/contact
  4. http://localhost:8000/notice

각 URL에 접속하면 아래와 같이 HTML 형태의 응답을 받게 됩니다.

텍스트를 아래와 같이 수정하면 좀 더 다양한 형태의 HTML을 반환할 수 있습니다.

@app.get("/", response_class=HTMLResponse)
def index():
    return """
    <h1>hello world 1</h1>
    <p>안녕</p>
    <ol>
        <li>첫 번째</li>
        <li>두 번째</li>
        <li>세 번째</li>
    </ol>
    """

다시 한 번 URL에 접속하여 응답을 확인해보세요.

3.2 템플릿으로 응답하기

자주 사용되는 형태는 아니지만 .html 파일을 사용하여 응답할 수도 있습니다. 이렇게 만들어진 파일로 응답하는 것을 템플릿으로 응답한다고 합니다. 템플릿에 필요한 jinja2는 fastapi[standard]를 설치할 때 이미 함께 설치되었으므로 추가 설치가 필요 없습니다.

먼저 02_1_route 폴더 아래에 templates 폴더를 만들고, 그 안에 index.html 파일을 만듭니다.

<h1>hello world</h1>
<p>안녕하세요, {{ name }}님</p>

{{ name }} 부분이 템플릿 문법입니다. 파이썬에서 넘겨준 값이 이 자리에 들어갑니다. main.py 파일을 다음과 같이 수정합니다.

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates

app = FastAPI()

templates = Jinja2Templates(directory="templates")


@app.get("/")
def index(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={"name": "licat"},
    )

127.0.0.1:8000에 접속하면 hello world와 안녕하세요, licat님이 렌더링된 화면을 확인할 수 있습니다.

오래된 코드에서 보게 될 형태

인터넷 자료나 AI가 만들어 준 코드에서 아래 형태를 보게 될 수 있습니다.

return templates.TemplateResponse("index.html", {"request": request})

request를 딕셔너리 안에 넣는 방식으로, 지금도 동작은 하지만 권장되지 않습니다. 위 본문의 형태처럼 request=와 name=을 각각 넘기는 방식을 쓰시면 됩니다.

jinja2에 자세한 사용법은 이 강의의 범위를 벗어나므로 공식문서나 ChatGPT와 같은 LLM 참고하시기 바랍니다. FastAPI는 아키텍처가 단순하여 LLM을 사용하더라도 완성도 높은 코드를 작성할 수 있습니다.

앞으로 할 실습에서는 템플릿을 사용하지 않을 것이므로, templates 폴더를 삭제하셔도 됩니다. 남겨두셔도 문제는 되지 않습니다.

연습문제

  1. /notice/{notice_id} 경로를 만들고, 위 2.2절의 내용을 참고하여 /notice/latest가 정상 동작하도록 순서를 잡아보세요.
  2. 모든 엔드포인트에 tags를 붙여 /docs에서 그룹으로 묶어보세요.
  3. HTMLResponse를 사용하여 공지사항 목록을 <ul> 태그로 그려서 반환하는 엔드포인트를 만들어보세요.
기본 라우팅 - FastAPI 베이스캠프 | 위니버시티