파이썬 챗봇에 대화 기록 저장 기능 붙이기

배포링크

깃허브링크

1 – 대화를 담을 데이터베이스 준비

1.1 – 작업 환경 복원

  1. 앞서 만든 my-chatbot 폴더를 편집기에서 엽니다.
  2. 터미널을 열고 cd backend 를 입력해 백엔드 폴더로 이동합니다.
  3. 가상환경을 활성화합니다. 사용하는 운영체제에 맞는 명령을 입력합니다.

.venv\Scripts\activate
source .venv/bin/activate
  1. 터미널 앞에 (.venv) 표시가 붙은 것을 확인한 다음, 아래 명령으로 서버를 실행합니다.
uvicorn main:app --reload
  1. 브라우저 주소창에 http://localhost:8000/docs 를 입력하고 Enter 키를 누릅니다.

화면에 /chat 항목이 있는 문서 페이지가 나타나면 앞서 만든 챗봇 서버가 다시 실행된 상태입니다. 이 챗봇은 브라우저를 새로고침하면 대화가 사라집니다. 주고받은 메시지를 어디에도 적어 두지 않기 때문입니다.

여기서는 대화를 세션 단위로 저장하는 기능을 붙입니다. 세션은 대화 한 건을 묶는 단위입니다. 하나의 세션 안에 그 대화에서 오간 질문과 답변이 모두 들어가고, 세션이 여러 개 쌓이면 지난 대화 목록이 됩니다.

만들 기능은 네 가지입니다. 메시지를 세션에 저장하기, 세션을 만들고 지난 대화를 불러오기, 세션 이름 바꾸기, 세션 지우기입니다. 데이터를 만들고(Create) 읽고(Read) 고치고(Update) 지우는(Delete) 네 가지 작업을 묶어 CRUD라고 부릅니다.

가상환경을 활성화하지 않은 채 uvicorn 을 입력하면 command not found 메시지가 나타납니다. 3번 명령을 다시 입력해 (.venv) 표시를 확인한 다음 실행합니다.

1.2 – 세션과 메시지를 담을 표 만들기

  1. backend 폴더 안에 db.py 파일을 새로 만듭니다.
  2. 아래 코드를 db.py 에 그대로 붙여 넣습니다.
import sqlite3

DB_PATH = "chat.db"

def get_conn():
    conn = sqlite3.connect(DB_PATH, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn

def init_db():
    conn = get_conn()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS sessions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL DEFAULT '새 대화',
            created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
        )
    """)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
            role TEXT NOT NULL,
            text TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
        )
    """)
    conn.commit()
    conn.close()
  1. 파일을 저장합니다.

SQLite(에스큐엘라이트)는 파일 하나에 데이터를 담는 데이터베이스입니다. 별도 서버를 실행하지 않고 chat.db 파일 하나만 있으면 됩니다. 파이썬에는 이 데이터베이스를 다루는 sqlite3 모듈이 처음부터 들어 있어서 추가로 설치할 것이 없습니다.

get_conn 은 데이터베이스 파일에 연결하는 함수입니다. sqlite3.connectchat.db 파일이 없으면 새로 만들고, 있으면 그 파일을 엽니다. conn.row_factory = sqlite3.Row 를 지정하면 조회 결과를 열 이름으로 꺼낼 수 있습니다.

init_db 는 표 두 개를 만드는 함수입니다. 데이터베이스에서 데이터를 행과 열로 담는 저장 단위를 테이블(표)이라고 합니다. sessions 는 대화 한 건마다 행 하나를 두고 제목과 만든 시각을 담습니다. messages 는 오간 말 하나마다 행 하나를 두고, session_id 로 자기가 어느 대화에 속하는지 가리킵니다.

두 표를 잇는 것이 REFERENCES sessions(id) ON DELETE CASCADE 입니다. 외래 키는 다른 표의 행을 가리키는 열이고, ON DELETE CASCADE 는 가리키던 행이 지워지면 이 행도 함께 지우라는 뜻입니다. 이 덕분에 세션을 지울 때 그 안의 메시지를 따로 지우지 않아도 됩니다. get_connPRAGMA foreign_keys = ON 은 이 규칙을 켜는 명령입니다. SQLite는 이 설정이 꺼진 상태로 시작하므로 연결할 때마다 켜야 합니다.

CREATE TABLE IF NOT EXISTS 는 같은 이름의 표가 이미 있으면 만들지 않고 넘어가는 문장입니다. 서버를 실행할 때마다 이 함수를 불러도 기존 대화가 지워지지 않습니다.

check_same_thread=False 는 연결을 만든 곳과 다른 곳에서도 그 연결을 쓸 수 있게 하는 설정입니다. 파이썬 공식 문서는 이 값이 True(기본값)일 때 다른 스레드에서 연결을 쓰면 ProgrammingError 가 발생한다고 밝히고 있습니다. FastAPI는 요청을 여러 스레드로 나눠 처리하므로 이 값을 False 로 둡니다. (2026-09-07 확인, https://docs.python.org/3/library/sqlite3.html)

1.3 – 서버가 시작할 때 표를 만들게 하기

  1. backend/main.py 파일을 엽니다.
  2. 파일 맨 위 from dotenv import load_dotenv 줄 바로 아래에 아래 한 줄을 추가합니다.
import db
  1. app = FastAPI() 줄 바로 아래에 아래 한 줄을 추가합니다.
db.init_db()
  1. 파일을 저장합니다.
  2. 서버를 실행해 둔 터미널을 봅니다. --reload 옵션 덕분에 저장과 동시에 서버가 다시 실행됩니다.
  3. backend 폴더를 편집기의 파일 목록에서 확인합니다.

chat.db 파일이 backend 폴더 안에 나타납니다. db.init_db()main.py 를 불러올 때 한 번 실행되고, 그때 sqlite3.connect 가 파일을 만들기 때문입니다. 아직 안에 담긴 대화는 없고 빈 표 두 개만 있는 상태입니다.

import db 로 파일 전체를 가져오면 이 파일의 함수를 db.init_db() 처럼 앞에 db. 를 붙여 부릅니다. 함수를 하나씩 이름으로 가져오는 방법도 있지만, 앞으로 db.py 에 함수를 여러 개 더 만들 예정이므로 파일째 가져오는 편이 손이 덜 갑니다.

앞에서 데이터를 담을 자리를 마련했으니, 이제 대화 세션을 만들고 목록을 읽는 기능을 붙입니다.

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 코드가 나옵니다.


아래 요구사항대로 파이썬 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[파일] {파일 경로}
[사용 기술] 파이썬 표준 라이브러리 sqlite3만 사용합니다. 외부 라이브러리를 쓰지 않습니다.
[데이터베이스 파일] {DB 파일 이름}

[만들 함수]
1. 데이터베이스에 연결해서 연결 객체를 돌려주는 함수
   - 조회 결과를 열 이름으로 꺼낼 수 있게 합니다.
   - 웹 서버가 여러 스레드에서 쓸 수 있게 합니다.
   - 외래 키 제약을 켭니다.
2. 표 두 개를 만드는 함수 (이미 있으면 다시 만들지 않습니다)
   - {세션 표 이름}: 번호(자동 증가), 제목(기본값 '{기본 제목}'), 만든 시각
   - {메시지 표 이름}: 번호(자동 증가), 어느 세션인지 가리키는 번호, 역할, 내용, 만든 시각
   - 세션이 지워지면 그 세션에 속한 메시지도 함께 지워지게 합니다.

[지킬 것]
- 만든 시각은 데이터베이스가 자동으로 채우게 합니다.
- 값을 바꾼 뒤에는 커밋하고 연결을 닫습니다.
- 설명 주석을 코드 줄마다 달지 않습니다.
아래 요구사항대로 파이썬 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[파일] backend/db.py
[사용 기술] 파이썬 표준 라이브러리 sqlite3만 사용합니다. 외부 라이브러리를 쓰지 않습니다.
[데이터베이스 파일] chat.db

[만들 함수]
1. 데이터베이스에 연결해서 연결 객체를 돌려주는 함수
   - 조회 결과를 열 이름으로 꺼낼 수 있게 합니다.
   - 웹 서버가 여러 스레드에서 쓸 수 있게 합니다.
   - 외래 키 제약을 켭니다.
2. 표 두 개를 만드는 함수 (이미 있으면 다시 만들지 않습니다)
   - sessions: 번호(자동 증가), 제목(기본값 '새 대화'), 만든 시각
   - messages: 번호(자동 증가), 어느 세션인지 가리키는 번호, 역할, 내용, 만든 시각
   - 세션이 지워지면 그 세션에 속한 메시지도 함께 지워지게 합니다.

[지킬 것]
- 만든 시각은 데이터베이스가 자동으로 채우게 합니다.
- 값을 바꾼 뒤에는 커밋하고 연결을 닫습니다.
- 설명 주석을 코드 줄마다 달지 않습니다.
  1. {파일 경로} — 코드를 넣을 파일입니다. 예: backend/db.py
  2. {DB 파일 이름} — 데이터가 담길 파일입니다. 예: chat.db
  3. {세션 표 이름} · {메시지 표 이름} — 표 이름입니다. 예: sessions, messages
  4. {기본 제목} — 새 세션을 만들 때 붙는 이름입니다. 예: 새 대화

AI가 만든 코드를 그대로 쓰기 전에 위 본문의 코드와 견줘 봅니다. 특히 세 가지를 확인합니다. 외래 키 제약을 켜는 PRAGMA foreign_keys = ON 이 연결 함수 안에 있는지, ON DELETE CASCADE 가 메시지 표에 붙었는지, 표를 만들 때 IF NOT EXISTS 를 썼는지입니다. 이 셋 중 하나라도 빠지면 뒤 단계에서 세션을 지워도 메시지가 남거나, 서버를 다시 켤 때 오류가 납니다.

2 – 대화 세션 만들고 목록 보기

2.1 – 세션을 만드는 기능

  1. backend/db.py 파일 맨 아래에 아래 코드를 추가합니다.
def create_session():
    conn = get_conn()
    cur = conn.execute("INSERT INTO sessions DEFAULT VALUES")
    conn.commit()
    new_id = cur.lastrowid
    conn.close()
    return new_id
  1. backend/main.py 파일 맨 아래에 아래 코드를 추가합니다.
@app.post("/sessions")
def new_session():
    session_id = db.create_session()
    return {"id": session_id, "title": "새 대화"}
  1. 파일을 저장합니다.
  2. 브라우저에서 http://localhost:8000/docs 를 열고 F5 키를 눌러 새로고침합니다.
  3. POST /sessions 항목을 클릭하고 [Try it out] 과 [Execute] 를 차례로 클릭합니다.

Response body 칸에 {"id": 1, "title": "새 대화"} 가 나타납니다. 세션 하나가 만들어지고 번호 1이 붙었다는 뜻입니다.

INSERT INTO 는 표에 새 행을 하나 넣는 SQL 문장입니다. SQL(에스큐엘)은 데이터베이스에 일을 시킬 때 쓰는 언어입니다. INSERT INTO sessions DEFAULT VALUES 는 값을 하나도 지정하지 않고 행을 만들라는 뜻입니다. 표를 만들 때 제목과 시각의 기본값을 정해 두었으므로 그 값이 그대로 들어갑니다.

cur.lastrowid 는 방금 넣은 행의 번호입니다. 이 번호를 돌려주면 화면 쪽에서 “지금 만든 세션”을 곧바로 가리킬 수 있습니다. conn.commit() 은 바꾼 내용을 파일에 실제로 적는 명령입니다. 이 명령을 부르지 않으면 화면에는 오류가 없는데 chat.db 에 아무것도 적히지 않습니다.

2.2 – 세션 목록을 읽는 기능

  1. backend/db.py 파일 맨 아래에 아래 코드를 추가합니다.
def read_sessions():
    conn = get_conn()
    rows = conn.execute(
        "SELECT id, title, created_at FROM sessions ORDER BY id DESC"
    ).fetchall()
    conn.close()
    return [dict(row) for row in rows]
  1. backend/main.py 파일 맨 아래에 아래 코드를 추가합니다.
@app.get("/sessions")
def list_sessions():
    return {"sessions": db.read_sessions()}
  1. 파일을 저장합니다.
  2. 브라우저 주소창에 http://localhost:8000/sessions 를 입력하고 Enter 키를 누릅니다.

앞에서 만든 세션이 번호와 제목, 만든 시각이 붙은 목록으로 나타납니다.

SELECT 는 표에서 데이터를 꺼내는 SQL 문장입니다. ORDER BY id DESC 는 번호가 큰 것부터 늘어놓으라는 뜻입니다. 나중에 만든 대화가 목록 맨 위에 오게 하려는 것입니다. 채팅 서비스는 대개 최근 대화를 위에 두므로 이 순서를 씁니다.

fetchall() 은 조건에 맞는 행을 모두 가져오는 명령이고, dict(row) 는 가져온 행을 화면에 보낼 수 있는 형태로 바꿉니다. 앞에서 row_factory 를 지정해 두었기 때문에 이 변환이 {"id": 1, "title": "새 대화", ...} 처럼 열 이름이 붙은 형태로 이뤄집니다.

@app.get 은 주소를 열어 보는 요청을 받는 표시입니다. 주소창에 주소를 넣어 여는 동작이 이 요청이라 브라우저에서 바로 확인할 수 있습니다. 반면 @app.post 가 붙은 /sessions 는 주소창으로 열리지 않고 /docs 화면이나 프로그램에서 보내야 합니다. 같은 주소라도 방식이 다르면 다른 기능이 됩니다.

지금까지 한 작업이 제대로 됐는지 아래 다섯 가지로 따져 봅니다. 세 가지 이상 맞으면 다음으로 넘어갑니다.

  1. backend 폴더에 chat.db 파일이 있습니다.
  2. /docs 화면에 POST /sessionsGET /sessions 두 항목이 보입니다.
  3. POST /sessions 를 실행할 때마다 목록에 항목이 하나씩 늘어납니다.
  4. 목록의 각 항목에 번호가 겹치지 않고 붙어 있습니다.
  5. 서버를 껐다가 다시 실행해도 목록의 내용이 그대로 있습니다.

세션이라는 그릇을 만들었으니, 이제 그 안에 오가는 말을 담습니다.

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 코드가 나옵니다.


아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] {DB 파일 이름} 을 다루는 {DB 모듈 파일} 에 연결 함수와 표 만드는 함수가 이미 있습니다. 표는 {세션 표 이름} 과 {메시지 표 이름} 입니다.
[사용 기술] 파이썬, FastAPI, 표준 라이브러리 sqlite3

[만들 것]
1. {DB 모듈 파일} 에 추가할 함수 두 개
   - 세션을 하나 만들고 새로 붙은 번호를 돌려주는 함수. 제목과 시각은 표의 기본값을 그대로 씁니다.
   - 세션을 최근 것부터 순서대로 읽어 목록으로 돌려주는 함수. 번호, 제목, 만든 시각을 담습니다.
2. {서버 파일} 에 추가할 엔드포인트 두 개
   - 세션을 만드는 엔드포인트: {세션 주소} (POST)
   - 세션 목록을 돌려주는 엔드포인트: {세션 주소} (GET)

[지킬 것]
- SQL에 값을 넣을 때는 문자열을 이어 붙이지 말고 물음표 자리 표시로 넘깁니다.
- 값을 바꾼 뒤에는 커밋하고 연결을 닫습니다.
- 조회 결과는 열 이름이 붙은 형태로 돌려줍니다.
아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] chat.db 를 다루는 backend/db.py 에 연결 함수와 표 만드는 함수가 이미 있습니다. 표는 sessions 와 messages 입니다.
[사용 기술] 파이썬, FastAPI, 표준 라이브러리 sqlite3

[만들 것]
1. backend/db.py 에 추가할 함수 두 개
   - 세션을 하나 만들고 새로 붙은 번호를 돌려주는 함수. 제목과 시각은 표의 기본값을 그대로 씁니다.
   - 세션을 최근 것부터 순서대로 읽어 목록으로 돌려주는 함수. 번호, 제목, 만든 시각을 담습니다.
2. backend/main.py 에 추가할 엔드포인트 두 개
   - 세션을 만드는 엔드포인트: /sessions (POST)
   - 세션 목록을 돌려주는 엔드포인트: /sessions (GET)

[지킬 것]
- SQL에 값을 넣을 때는 문자열을 이어 붙이지 말고 물음표 자리 표시로 넘깁니다.
- 값을 바꾼 뒤에는 커밋하고 연결을 닫습니다.
- 조회 결과는 열 이름이 붙은 형태로 돌려줍니다.
  1. {DB 모듈 파일} — 데이터베이스를 다루는 파일입니다. 예: backend/db.py
  2. {서버 파일} — 엔드포인트를 적는 파일입니다. 예: backend/main.py
  3. {세션 주소} — 세션을 다루는 주소입니다. 예: /sessions

AI가 목록을 오래된 것부터 돌려주는 코드를 낼 때가 있습니다. 요청에 “최근 것부터”라고 적었는데도 그렇다면 ORDER BY 뒤에 DESC 가 붙었는지 확인합니다.

3 – 세션에 메시지 저장하기

3.1 – 메시지를 저장하고 읽는 함수

  1. backend/db.py 파일 맨 아래에 아래 코드를 추가합니다.
def create_message(session_id, role, text):
    conn = get_conn()
    cur = conn.execute(
        "INSERT INTO messages (session_id, role, text) VALUES (?, ?, ?)",
        (session_id, role, text)
    )
    conn.commit()
    new_id = cur.lastrowid
    conn.close()
    return new_id

def read_messages(session_id):
    conn = get_conn()
    rows = conn.execute(
        "SELECT id, role, text, created_at FROM messages "
        "WHERE session_id = ? ORDER BY id",
        (session_id,)
    ).fetchall()
    conn.close()
    return [dict(row) for row in rows]

def count_messages(session_id):
    conn = get_conn()
    row = conn.execute(
        "SELECT COUNT(*) AS n FROM messages WHERE session_id = ?",
        (session_id,)
    ).fetchone()
    conn.close()
    return row["n"]
  1. 파일을 저장합니다.

물음표 ? 는 값이 들어갈 자리를 표시하는 기호입니다. 실제 값은 execute 의 두 번째 인자로 넘깁니다. 파이썬 공식 문서는 문자열을 이어 붙여 SQL 문을 만들지 말고 항상 이 자리 표시를 쓰라고 밝히고 있습니다. (2026-09-07 확인, https://docs.python.org/3/library/sqlite3.html)

문자열을 이어 붙이면 사용자가 입력한 따옴표가 SQL 문장의 일부로 해석돼, 뜻하지 않은 명령이 실행될 수 있습니다. 이 문제를 SQL 인젝션이라고 부릅니다.

WHERE session_id = ? 가 이 세 함수의 핵심입니다. 메시지를 꺼낼 때 세션 번호로 범위를 좁히므로, 다른 대화의 메시지가 섞이지 않습니다. count_messages 는 그 세션에 메시지가 몇 개 있는지 세는 함수이고, 다음 단계에서 첫 질문인지 가릴 때 씁니다.

(session_id,) 의 끝에 붙은 쉼표를 눈여겨봅니다. 파이썬에서 값이 하나뿐인 묶음은 쉼표를 붙여야 묶음으로 인식됩니다. 쉼표를 빠뜨리면 Incorrect number of bindings supplied 라는 오류가 나타납니다.

3.2 – 앞선 대화를 함께 보내 답변받기

  1. backend/main.pyask_ai 함수를 아래 코드로 바꿉니다.
def ask_ai(history):
    token = os.getenv("HF_TOKEN")
    headers = {"Authorization": f"Bearer {token}"}
    payload = {
        "model": HF_MODEL,
        "messages": history,
        "max_tokens": 300
    }
    res = requests.post(HF_URL, headers=headers, json=payload)
    data = res.json()
    return data["choices"][0]["message"]["content"]

def build_history(session_id):
    rows = db.read_messages(session_id)
    return [
        {"role": "user" if r["role"] == "user" else "assistant", "content": r["text"]}
        for r in rows
    ]
  1. 파일을 저장합니다.

바뀐 곳은 ask_ai 가 받는 값입니다. 전에는 질문 한 개를 받아 그것만 AI에 보냈습니다. 이제는 대화 목록을 통째로 받아 그대로 보냅니다.

build_history 는 데이터베이스에 저장된 대화를 AI가 알아듣는 형태로 바꾸는 함수입니다. 허깅페이스 API는 말한 사람을 userassistant 두 가지로 구분하므로, 데이터베이스에 bot 으로 저장한 역할을 assistant 로 바꿔 넘깁니다.

이 한 가지가 챗봇의 성격을 바꿉니다. 질문 하나만 보내면 AI는 앞에서 무슨 말이 오갔는지 모르므로, “그건 왜 그래?” 같은 이어지는 질문에 답하지 못합니다. 대화 전체를 보내면 앞 내용을 알고 답합니다. 세션 단위로 저장한 대화가 여기서 쓰입니다.

대화가 길어지면 보내는 양도 함께 늘어납니다. 한 번에 보낼 수 있는 양에는 한도가 있어서, 대화가 아주 길어지면 오래된 부분을 잘라 내거나 요약해서 보내는 방법을 씁니다. 실습 범위에서는 대화가 그만큼 길어지지 않으므로 전체를 보냅니다.

3.3 – 메시지 전송 엔드포인트 만들기

  1. backend/main.py 파일 맨 아래에 아래 코드를 추가합니다.
@app.get("/sessions/{session_id}/messages")
def list_messages(session_id: int):
    return {"messages": db.read_messages(session_id)}

@app.post("/sessions/{session_id}/messages")
def send_message(session_id: int, msg: Msg):
    first = db.count_messages(session_id) == 0
    db.create_message(session_id, "user", msg.text)
    if first:
        db.update_session(session_id, msg.text[:20])
    reply = ask_ai(build_history(session_id))
    db.create_message(session_id, "bot", reply)
    return {"reply": reply}
  1. backend/db.py 파일 맨 아래에 아래 코드를 추가합니다.
def update_session(session_id, title):
    conn = get_conn()
    cur = conn.execute(
        "UPDATE sessions SET title = ? WHERE id = ?",
        (title, session_id)
    )
    conn.commit()
    changed = cur.rowcount
    conn.close()
    return changed
  1. 두 파일을 저장합니다.
  2. 브라우저에서 http://localhost:8000/docs 를 열고 F5 키를 눌러 새로고침합니다.
  3. POST /sessions/{session_id}/messages 항목을 클릭하고 [Try it out] 을 클릭합니다.
  4. session_id 칸에 1 을 입력하고, 아래 입력란을 {"text": "파이썬 리스트가 뭐야"} 로 바꾼 다음 [Execute] 를 클릭합니다.
  5. 브라우저 주소창에 http://localhost:8000/sessions 를 입력해 목록을 확인합니다.

세션 제목이 새 대화 에서 파이썬 리스트가 뭐야 로 바뀌어 있습니다. 첫 질문의 앞 20자를 제목으로 넣었기 때문입니다.

send_message 는 네 가지 일을 차례로 합니다. 먼저 이 세션에 메시지가 하나도 없는지 확인하고, 질문을 user 역할로 저장합니다. 첫 질문이었다면 그 내용으로 세션 제목을 바꿉니다. 그다음 지금까지의 대화를 AI에 보내 답변을 받고, 그 답변을 bot 역할로 저장합니다.

제목을 자동으로 넣는 이유는 목록에서 대화를 알아보게 하려는 것입니다. 모든 대화가 새 대화 라는 이름이면 어느 것이 무슨 내용인지 열어 봐야 알 수 있습니다. msg.text[:20] 은 글자 스무 개까지만 잘라 쓴다는 뜻이고, 목록에서 한 줄에 들어갈 만한 길이로 맞춘 값입니다.

주소에 적은 {session_id} 는 값이 들어갈 자리입니다. /sessions/3/messages 로 요청하면 session_id3 이 담깁니다. 옆에 붙인 : int 는 이 값을 정수로 받겠다는 표시이며, 숫자가 아닌 값이 들어오면 FastAPI가 요청을 막습니다.

이 단계에서 ask_ai() takes 1 positional argument 같은 오류가 나면 ask_ai 를 앞 단계에서 바꾸지 않은 것입니다. 3.2의 코드로 ask_ai 를 먼저 바꾼 다음 이 단계를 실행합니다.

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 코드가 나옵니다.


아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] FastAPI 챗봇 서버입니다. {DB 모듈 파일} 에 연결 함수와 세션 관련 함수가 있고, 표는 {세션 표 이름}({세션 표 열})과 {메시지 표 이름}({메시지 표 열})입니다. AI 호출은 {AI API 이름} 의 OpenAI 호환 형식을 씁니다.

[만들 것]
1. {DB 모듈 파일} 에 추가할 함수 세 개
   - 메시지를 저장하는 함수 (세션 번호, 역할, 내용을 받습니다)
   - 한 세션의 메시지를 저장한 순서대로 읽는 함수
   - 한 세션의 메시지 개수를 세는 함수
2. {서버 파일} 에 추가할 것
   - 저장된 대화를 AI가 받는 형식으로 바꾸는 함수. 역할 이름 {DB 역할 이름} 을 {API 역할 이름} 으로 바꿉니다.
   - 메시지 전송 엔드포인트 {메시지 주소} (POST): 질문을 저장하고, 그 세션의 대화 전체를 AI에 보내 답변을 받고, 답변을 저장합니다.
   - 그 세션의 첫 질문일 때만 질문의 앞 {제목 길이}자를 세션 제목으로 바꿉니다.
   - 한 세션의 메시지를 돌려주는 엔드포인트 {메시지 주소} (GET)

[지킬 것]
- SQL에 값을 넣을 때는 물음표 자리 표시를 씁니다.
- 메시지를 꺼낼 때는 세션 번호로 범위를 좁혀 다른 대화가 섞이지 않게 합니다.
- 주소의 세션 번호는 정수로 받습니다.
아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] FastAPI 챗봇 서버입니다. backend/db.py 에 연결 함수와 세션 관련 함수가 있고, 표는 sessions(id, title, created_at)와 messages(id, session_id, role, text, created_at)입니다. AI 호출은 허깅페이스 라우터의 OpenAI 호환 형식을 씁니다.

[만들 것]
1. backend/db.py 에 추가할 함수 세 개
   - 메시지를 저장하는 함수 (세션 번호, 역할, 내용을 받습니다)
   - 한 세션의 메시지를 저장한 순서대로 읽는 함수
   - 한 세션의 메시지 개수를 세는 함수
2. backend/main.py 에 추가할 것
   - 저장된 대화를 AI가 받는 형식으로 바꾸는 함수. 역할 이름 bot 을 assistant 로 바꿉니다.
   - 메시지 전송 엔드포인트 /sessions/{session_id}/messages (POST): 질문을 저장하고, 그 세션의 대화 전체를 AI에 보내 답변을 받고, 답변을 저장합니다.
   - 그 세션의 첫 질문일 때만 질문의 앞 20자를 세션 제목으로 바꿉니다.
   - 한 세션의 메시지를 돌려주는 엔드포인트 /sessions/{session_id}/messages (GET)

[지킬 것]
- SQL에 값을 넣을 때는 물음표 자리 표시를 씁니다.
- 메시지를 꺼낼 때는 세션 번호로 범위를 좁혀 다른 대화가 섞이지 않게 합니다.
- 주소의 세션 번호는 정수로 받습니다.
  1. {세션 표 열} · {메시지 표 열} — 앞에서 만든 표의 열 이름입니다. 예: id, title, created_at
  2. {DB 역할 이름} · {API 역할 이름} — 저장할 때 쓰는 이름과 AI가 받는 이름입니다. 예: bot, assistant
  3. {메시지 주소} — 메시지를 다루는 주소입니다. 예: /sessions/{session_id}/messages
  4. {제목 길이} — 제목으로 쓸 글자 수입니다. 예: 20

AI가 질문 하나만 보내는 코드를 낼 때가 있습니다. 요청서에 “대화 전체를 보낸다”고 적었는데도 그렇다면, AI에 넘기는 값이 문자열 하나인지 목록인지 확인합니다. 목록이 아니면 앞 대화를 기억하지 못하는 챗봇이 됩니다.

4 – 세션 이름 바꾸기와 세션 지우기

4.1 – 세션 이름을 바꾸는 기능

  1. backend/main.py 첫 줄의 from fastapi import FastAPI 를 아래 줄로 바꿉니다.
from fastapi import FastAPI, HTTPException
  1. class Msg(BaseModel): 묶음 아래에 아래 코드를 추가합니다.
class Title(BaseModel):
    title: str
  1. 파일 맨 아래에 아래 코드를 추가합니다.
@app.put("/sessions/{session_id}")
def rename_session(session_id: int, body: Title):
    title = body.title.strip()
    if not title:
        raise HTTPException(status_code=400, detail="제목이 비어 있습니다")
    if db.update_session(session_id, title) == 0:
        raise HTTPException(status_code=404, detail="해당 대화가 없습니다")
    return {"id": session_id, "title": title}
  1. 파일을 저장합니다.

UPDATE 는 이미 저장된 행의 값을 바꾸는 SQL 문장입니다. 3.3에서 만든 update_session 이 그 일을 합니다. SET title = ? 는 제목 열을 새 값으로 바꾸라는 뜻이고, WHERE id = ? 는 그 대상을 번호가 맞는 행 하나로 좁히는 조건입니다. WHERE 조건을 빼면 표에 있는 모든 대화의 제목이 같은 값으로 바뀝니다.

이 엔드포인트는 값을 두 번 확인합니다. 먼저 strip() 으로 앞뒤 공백을 없앤 뒤 내용이 남았는지 봅니다. 빈 제목을 허용하면 목록에 이름 없는 줄이 생깁니다. 다음으로 update_session 이 돌려준 값이 0 인지 봅니다. cur.rowcount 는 실제로 바뀐 행의 수이므로, 없는 번호를 넘기면 0 이 됩니다.

두 경우에 서로 다른 응답을 돌려주는 것이 중요합니다. 400은 보낸 값에 문제가 있다는 뜻이고, 404는 대상을 찾지 못했다는 뜻입니다. 화면 쪽에서 이 둘을 구분해야 “제목을 입력하세요”와 “없는 대화입니다” 중 알맞은 안내를 띄울 수 있습니다.

4.2 – 세션을 지우는 기능

  1. backend/db.py 파일 맨 아래에 아래 코드를 추가합니다.
def delete_session(session_id):
    conn = get_conn()
    cur = conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
    conn.commit()
    changed = cur.rowcount
    conn.close()
    return changed
  1. backend/main.py 파일 맨 아래에 아래 코드를 추가합니다.
@app.delete("/sessions/{session_id}")
def remove_session(session_id: int):
    if db.delete_session(session_id) == 0:
        raise HTTPException(status_code=404, detail="해당 대화가 없습니다")
    return {"deleted": session_id}
  1. 두 파일을 저장합니다.
  2. 브라우저에서 http://localhost:8000/docs 를 열고 F5 키를 눌러 새로고침합니다.
  3. POST /sessions 로 세션을 하나 더 만들고, POST /sessions/{session_id}/messages 로 그 세션에 메시지를 두어 개 넣습니다.
  4. DELETE /sessions/{session_id} 항목에서 방금 만든 세션 번호를 입력하고 [Execute] 를 클릭합니다.
  5. GET /sessions/{session_id}/messages 에 같은 번호를 넣고 [Execute] 를 클릭합니다.

세션 목록에서 그 대화가 사라지고, 메시지 목록도 빈 상태로 나옵니다. 메시지를 지우는 코드를 따로 쓰지 않았는데도 함께 사라진 것은 표를 만들 때 걸어 둔 ON DELETE CASCADE 때문입니다. 세션 행이 없어지면 그 번호를 가리키던 메시지 행도 데이터베이스가 함께 지웁니다.

이 규칙이 없으면 세션만 사라지고 메시지는 그대로 있습니다. 그 메시지는 어느 대화에 속하는지 알 수 없는 상태가 되고, 대화를 지워도 파일 크기만 계속 커집니다. 지금은 세션 하나를 지우는 것이 대화 한 건을 통째로 지우는 일과 같습니다.

DELETE FROM 은 표에서 행을 지우는 SQL 문장입니다. 여기서도 WHERE id = ? 로 대상을 하나로 좁힙니다. 지운 행은 되돌릴 수 없으므로 이 조건이 특히 중요합니다.

세션을 지웠는데 메시지가 남아 있으면 get_conn 안의 PRAGMA foreign_keys = ON 이 빠진 것입니다. SQLite는 이 설정이 꺼진 상태로 연결을 시작하므로, 연결을 만들 때마다 켜야 ON DELETE CASCADE 가 동작합니다.

지금까지 한 작업이 제대로 됐는지 아래 다섯 가지로 따져 봅니다. 세 가지 이상 맞으면 다음으로 넘어갑니다.

  1. 첫 질문을 보내면 세션 제목이 그 질문 내용으로 바뀝니다.
  2. 이어지는 질문에 답할 때 앞 대화를 알고 답합니다.
  3. 세션 이름을 바꾸면 목록에 바뀐 이름이 나타납니다.
  4. 빈 제목을 보내면 400, 없는 번호를 보내면 404가 돌아옵니다.
  5. 세션을 지우면 그 세션의 메시지 목록도 빈 상태가 됩니다.

서버 쪽 기능을 다 만들었으니, 이제 사용자가 실제로 쓰는 화면을 만듭니다.

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 코드가 나옵니다.


아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] FastAPI 챗봇 서버입니다. {세션 표 이름} 표에 대화가 저장돼 있고, {메시지 표 이름} 표의 메시지는 세션이 지워질 때 함께 지워지도록 외래 키가 걸려 있습니다.

[만들 것]
1. {DB 모듈 파일} 에 추가할 함수
   - 세션 번호로 그 세션을 지우고, 실제로 지워진 행 수를 돌려주는 함수
2. {서버 파일} 에 추가할 엔드포인트 두 개
   - 세션 이름 변경 {세션 주소}/{세션 번호 자리} (PUT): 새 제목을 받아 바꿉니다.
   - 세션 삭제 {세션 주소}/{세션 번호 자리} (DELETE)
3. 요청 본문으로 제목을 받는 데이터 모델

[응답 규칙]
- 제목이 비었거나 공백뿐이면 {빈 값 코드} 와 함께 이유를 돌려줍니다.
- 그 번호의 세션이 없으면 {없음 코드} 와 함께 이유를 돌려줍니다.
- 성공하면 바뀐 내용이나 지운 번호를 돌려줍니다.

[지킬 것]
- 없는 대상인지는 실제로 바뀐 행 수로 판단합니다.
- 메시지를 지우는 코드를 따로 쓰지 않습니다. 외래 키 설정에 맡깁니다.
아래 요구사항대로 코드를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[상황] FastAPI 챗봇 서버입니다. sessions 표에 대화가 저장돼 있고, messages 표의 메시지는 세션이 지워질 때 함께 지워지도록 외래 키가 걸려 있습니다.

[만들 것]
1. backend/db.py 에 추가할 함수
   - 세션 번호로 그 세션을 지우고, 실제로 지워진 행 수를 돌려주는 함수
2. backend/main.py 에 추가할 엔드포인트 두 개
   - 세션 이름 변경 /sessions/{session_id} (PUT): 새 제목을 받아 바꿉니다.
   - 세션 삭제 /sessions/{session_id} (DELETE)
3. 요청 본문으로 제목을 받는 데이터 모델

[응답 규칙]
- 제목이 비었거나 공백뿐이면 400 과 함께 이유를 돌려줍니다.
- 그 번호의 세션이 없으면 404 와 함께 이유를 돌려줍니다.
- 성공하면 바뀐 내용이나 지운 번호를 돌려줍니다.

[지킬 것]
- 없는 대상인지는 실제로 바뀐 행 수로 판단합니다.
- 메시지를 지우는 코드를 따로 쓰지 않습니다. 외래 키 설정에 맡깁니다.
  1. {세션 번호 자리} — 주소에서 번호가 들어가는 자리입니다. 예: {session_id}
  2. {빈 값 코드} · {없음 코드} — 응답 코드입니다. 예: 400, 404

AI가 세션을 지울 때 메시지를 지우는 SQL을 함께 넣을 때가 있습니다. 외래 키에 맡기기로 했으므로 그 줄은 빼고 씁니다. 남겨 두면 지우는 순서에 따라 오류가 나기도 합니다.

5 – 대화 목록이 있는 화면 만들기

5.1 – 화면 기본 스타일 정리하기

  1. 새 터미널 창을 하나 더 엽니다. 서버를 실행한 터미널은 그대로 둡니다.
  2. cd frontend 를 입력해 프론트엔드 폴더로 이동한 다음 npm run dev 를 입력합니다.
  3. frontend/src/index.css 파일을 열고 안에 있는 내용을 모두 지운 다음, 아래 한 줄만 남깁니다.
body { margin: 0; }
  1. 파일을 저장하고 브라우저에서 http://localhost:5173 을 확인합니다.

화면의 글자 크기가 작아지고, 가운데로 몰려 있던 내용이 왼쪽 위로 붙습니다. index.css 는 리액트 프로젝트를 만들 때 함께 생기는 기본 스타일 파일입니다. 화면을 가운데 정렬하고 제목을 크게 키우는 규칙이 들어 있어서, 이대로 두면 채팅 화면의 좌우 배치와 부딪힙니다.

여기서는 대화 목록과 대화창을 좌우로 나눠 배치할 예정이므로 이 파일을 비우고 시작합니다. 화면에 필요한 스타일은 App.css 에 모아 적습니다.

5.2 – 대화 목록과 대화창 만들기

  1. frontend/src/App.jsx 파일을 열고 아래 코드로 전체를 바꿉니다.
import { useState, useEffect } from "react";
import "./App.css";

const API = "http://localhost:8000";

export default function App() {
  const [sessions, setSessions] = useState([]);
  const [sessionId, setSessionId] = useState(null);
  const [msgs, setMsgs] = useState([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);

  const loadSessions = async () => {
    const res = await fetch(`${API}/sessions`);
    const data = await res.json();
    setSessions(data.sessions);
    return data.sessions;
  };

  const loadMsgs = async (id) => {
    if (!id) {
      setMsgs([]);
      return;
    }
    const res = await fetch(`${API}/sessions/${id}/messages`);
    const data = await res.json();
    setMsgs(data.messages);
  };

  useEffect(() => {
    loadSessions().then(list => {
      if (list.length > 0) {
        setSessionId(list[0].id);
        loadMsgs(list[0].id);
      }
    });
  }, []);

  const openSession = (id) => {
    setSessionId(id);
    loadMsgs(id);
  };

  const newSession = async () => {
    const res = await fetch(`${API}/sessions`, { method: "POST" });
    const data = await res.json();
    await loadSessions();
    setSessionId(data.id);
    setMsgs([]);
  };

  const send = async () => {
    if (!input.trim() || !sessionId) return;
    const text = input;
    setInput("");
    setLoading(true);
    await fetch(`${API}/sessions/${sessionId}/messages`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text })
    });
    await loadMsgs(sessionId);
    await loadSessions();
    setLoading(false);
  };

  const onKey = (e) => {
    if (e.key === "Enter") send();
  };

  return (
    <div className="app">
      <aside className="side">
        <button className="new" onClick={newSession}>+ 새 대화</button>
        <ul className="session-list">
          {sessions.map(s => (
            <li key={s.id} className={s.id === sessionId ? "session on" : "session"}>
              <button className="session-title" onClick={() => openSession(s.id)}>{s.title}</button>
            </li>
          ))}
        </ul>
      </aside>

      <main className="chat">
        <div className="box">
          {msgs.map(m => (
            <div key={m.id} className={m.role}>
              <p>{m.text}</p>
            </div>
          ))}
          {loading && <p className="loading">생각 중...</p>}
        </div>
        <div className="input-row">
          <input
            value={input}
            onChange={e => setInput(e.target.value)}
            onKeyDown={onKey}
            placeholder="메시지를 입력하세요"
          />
          <button onClick={send}>전송</button>
        </div>
      </main>
    </div>
  );
}
  1. 파일을 저장하고 브라우저 화면을 확인합니다.
  2. [+ 새 대화] 를 클릭한 다음 아래쪽 입력란에 질문을 입력하고 [전송] 을 클릭합니다.
  3. 다시 [+ 새 대화] 를 클릭하고 다른 질문을 보냅니다.
  4. 왼쪽 목록에서 앞서 만든 대화를 클릭합니다.

왼쪽에 대화 목록이 쌓이고, 목록에서 고른 대화의 내용이 오른쪽에 나타납니다. 지금 열려 있는 대화는 목록에서 굵게 표시됩니다.

화면 상태는 네 가지입니다. sessions 는 왼쪽 목록에 그릴 대화 목록, sessionId 는 지금 열어 둔 대화의 번호, msgs 는 그 대화의 메시지, input 은 입력란에 적은 글자입니다. 대화를 클릭하면 sessionId 를 바꾸고 그 번호로 메시지를 다시 불러옵니다.

useEffect(() => { ... }, []) 는 화면이 처음 그려질 때 한 번 실행하라는 뜻입니다. 여기서 목록을 불러오고, 목록이 비어 있지 않으면 맨 위 대화를 열어 둡니다. 두 번째 인자로 넘긴 빈 배열이 “처음 한 번만”이라는 조건입니다.

메시지를 보낸 뒤 loadSessions() 를 다시 부르는 까닭은 제목 때문입니다. 첫 질문을 보내면 서버가 세션 제목을 그 질문으로 바꾸므로, 목록을 다시 읽어야 왼쪽에 새 제목이 나타납니다.

[전송] 을 눌러도 아무 변화가 없으면 브라우저에서 F12 키를 눌러 Console(콘솔) 탭을 확인합니다. 붉은 글씨로 Failed to fetch 가 보이면 백엔드 서버가 꺼진 상태이므로, 서버를 실행한 터미널에서 uvicorn main:app --reload 를 다시 실행합니다.

5.3 – 목록에서 이름 바꾸고 지우기

  1. frontend/src/App.jsxconst [loading, setLoading] = useState(false); 줄 아래에 상태 두 개를 추가합니다.
  const [editId, setEditId] = useState(null);
  const [editTitle, setEditTitle] = useState("");
  1. send 함수 위에 아래 함수 세 개를 추가합니다.
  const startRename = (s) => {
    setEditId(s.id);
    setEditTitle(s.title);
  };

  const saveTitle = async (id) => {
    await fetch(`${API}/sessions/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: editTitle })
    });
    setEditId(null);
    await loadSessions();
  };

  const removeSession = async (id) => {
    await fetch(`${API}/sessions/${id}`, { method: "DELETE" });
    const list = await loadSessions();
    const next = list.length > 0 ? list[0].id : null;
    setSessionId(next);
    loadMsgs(next);
  };
  1. 목록을 그리는 <li> 안의 내용을 아래 코드로 바꿉니다.
              {editId === s.id ? (
                <span className="rename">
                  <input value={editTitle} onChange={e => setEditTitle(e.target.value)} />
                  <button onClick={() => saveTitle(s.id)}>저장</button>
                </span>
              ) : (
                <>
                  <button className="session-title" onClick={() => openSession(s.id)}>{s.title}</button>
                  <span className="session-tools">
                    <button onClick={() => startRename(s)}>이름</button>
                    <button onClick={() => removeSession(s.id)}>삭제</button>
                  </span>
                </>
              )}
  1. 파일을 저장하고 브라우저에서 목록의 [이름] 을 클릭합니다.
  2. 나타난 입력란에 새 제목을 적고 [저장] 을 클릭합니다.
  3. 다른 대화의 [삭제] 를 클릭합니다.

제목이 바뀌어 목록에 나타나고, 삭제한 대화는 목록에서 사라집니다. 삭제 뒤에는 남은 대화 중 맨 위 것이 자동으로 열립니다.

editId 는 지금 이름을 바꾸는 중인 대화의 번호를 담는 상태입니다. 아무것도 바꾸지 않을 때는 null 이고, [이름] 을 클릭하면 그 대화의 번호가 들어갑니다. 화면에서는 editId === s.id 조건으로 지금 항목이 편집 중인지 가려, 맞으면 입력란을, 아니면 제목 버튼을 보여 줍니다. 물음표와 콜론으로 두 갈림길을 적는 이 표기를 조건 연산자라고 합니다.

removeSession 이 하는 일에 주의합니다. 지운 대화가 지금 열려 있던 대화일 수 있으므로, 지운 뒤 목록을 다시 읽어 맨 위 대화를 엽니다. 이 처리가 없으면 이미 사라진 대화의 번호를 계속 들고 있어서, 메시지를 보낼 때 없는 세션에 저장을 시도하게 됩니다.

5.4 – 화면 모양 다듬기

  1. frontend/src/App.css 파일을 열고 아래 코드로 전체를 바꿉니다.
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; color: #222; background: #fff; }

.app { display: flex; height: 100vh; }

.side { width: 220px; border-right: 1px solid #ddd; padding: 12px; overflow-y: auto; }
.new { width: 100%; padding: 8px; cursor: pointer; margin-bottom: 12px; }
.session-list { list-style: none; margin: 0; padding: 0; }
.session { display: flex; align-items: center; gap: 4px; padding: 4px 0; }
.session.on .session-title { font-weight: 700; }
.session-title { flex: 1; text-align: left; background: none; border: none; padding: 4px; cursor: pointer; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-tools button { font-size: 11px; padding: 2px 4px; cursor: pointer; }

.chat { flex: 1; display: flex; flex-direction: column; max-width: 720px; margin: 0 auto; padding: 16px; }
.box { flex: 1; border: 1px solid #ccc; overflow-y: auto; padding: 16px; }

.user { text-align: right; margin: 10px 0; }
.bot { text-align: left; margin: 10px 0; }
.user p, .bot p { display: inline-block; padding: 8px 12px; border-radius: 8px; margin: 0; text-align: left; }
.user p { background: #d1e7ff; }
.bot p { background: #f0f0f0; }

.rename { display: flex; gap: 4px; width: 100%; }
.rename input { flex: 1; min-width: 0; padding: 4px; font-size: 13px; }
.rename button { font-size: 11px; padding: 2px 6px; cursor: pointer; }

.loading { color: #888; font-style: italic; }
.input-row { display: flex; gap: 8px; margin-top: 8px; }
.input-row input { flex: 1; padding: 10px; }
.input-row button { padding: 10px 18px; cursor: pointer; }
  1. 파일을 저장하고 브라우저 화면을 확인합니다.

왼쪽에 너비가 고정된 대화 목록이, 오른쪽에 대화창이 놓입니다. 사용자 질문은 오른쪽에 파란 바탕으로, 챗봇 답변은 왼쪽에 회색 바탕으로 나뉩니다.

.app { display: flex; } 가 좌우 배치를 만듭니다. 왼쪽 .side 는 너비를 220px로 고정하고, 오른쪽 .chatflex: 1 로 남은 자리를 모두 차지합니다. 목록 제목이 길면 text-overflow: ellipsis 가 뒤를 점으로 줄여 한 줄에 맞춥니다.

지금까지 한 작업이 제대로 됐는지 아래 다섯 가지로 따져 봅니다. 세 가지 이상 맞으면 이 과정을 마친 것으로 봅니다.

  1. 브라우저를 새로고침해도 왼쪽 목록과 대화 내용이 그대로 나타납니다.
  2. [+ 새 대화] 로 만든 대화에 첫 질문을 보내면 목록의 이름이 그 질문으로 바뀝니다.
  3. 목록에서 다른 대화를 클릭하면 오른쪽 내용이 그 대화로 바뀝니다.
  4. 이름을 바꾸면 새로고침 뒤에도 바뀐 이름이 남아 있습니다.
  5. 대화를 지우면 목록에서 사라지고, 남은 대화 중 하나가 열립니다.

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 코드가 나옵니다.


아래 요구사항대로 리액트 컴포넌트 하나를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[사용 기술] {프론트 기술}. 상태 관리 라이브러리를 쓰지 않고 훅만 씁니다. 서버 호출은 fetch를 씁니다.
[서버 주소] {서버 주소}

[쓸 수 있는 API]
- 세션 목록 조회: GET {세션 주소}
- 세션 생성: POST {세션 주소}
- 세션 이름 변경: PUT {세션 주소}/번호 (본문 {제목 본문 형식})
- 세션 삭제: DELETE {세션 주소}/번호
- 대화 조회: GET {메시지 주소}
- 메시지 전송: POST {메시지 주소} (본문 {메시지 본문 형식})

[화면 구성]
1. 왼쪽: 새 대화 버튼과 대화 목록. 목록 항목마다 제목, 이름 바꾸기, 삭제.
2. 오른쪽: 고른 대화의 메시지와 입력란, 전송 버튼.

[동작]
- 화면을 처음 열 때 목록을 불러오고, 비어 있지 않으면 맨 위 대화를 엽니다.
- 지금 열려 있는 대화를 목록에서 구분해 보여 줍니다.
- 메시지를 보낸 뒤 대화와 목록을 다시 불러옵니다. 첫 질문이면 제목이 바뀌기 때문입니다.
- 이름 바꾸기는 목록 안에서 입력란으로 처리합니다. 한 번에 한 항목만 편집합니다.
- 대화를 지운 뒤에는 남은 대화 중 맨 위 것을 엽니다. 남은 대화가 없으면 빈 화면을 둡니다.
- 답변을 기다리는 동안 {대기 문구} 를 보여 줍니다.

[지킬 것]
- 메시지에는 수정·삭제 기능을 넣지 않습니다.
- 클래스 이름만 붙이고 CSS 파일은 따로 작성합니다.
아래 요구사항대로 리액트 컴포넌트 하나를 작성해 주세요. 코드를 먼저 주고, 설명은 코드 뒤에 다섯 줄 이내로 붙여 주세요.

[사용 기술] React 함수형 컴포넌트, useState, useEffect. 상태 관리 라이브러리를 쓰지 않고 훅만 씁니다. 서버 호출은 fetch를 씁니다.
[서버 주소] http://localhost:8000

[쓸 수 있는 API]
- 세션 목록 조회: GET /sessions
- 세션 생성: POST /sessions
- 세션 이름 변경: PUT /sessions/번호 (본문 {"title": "새 제목"})
- 세션 삭제: DELETE /sessions/번호
- 대화 조회: GET /sessions/번호/messages
- 메시지 전송: POST /sessions/번호/messages (본문 {"text": "질문"})

[화면 구성]
1. 왼쪽: 새 대화 버튼과 대화 목록. 목록 항목마다 제목, 이름 바꾸기, 삭제.
2. 오른쪽: 고른 대화의 메시지와 입력란, 전송 버튼.

[동작]
- 화면을 처음 열 때 목록을 불러오고, 비어 있지 않으면 맨 위 대화를 엽니다.
- 지금 열려 있는 대화를 목록에서 구분해 보여 줍니다.
- 메시지를 보낸 뒤 대화와 목록을 다시 불러옵니다. 첫 질문이면 제목이 바뀌기 때문입니다.
- 이름 바꾸기는 목록 안에서 입력란으로 처리합니다. 한 번에 한 항목만 편집합니다.
- 대화를 지운 뒤에는 남은 대화 중 맨 위 것을 엽니다. 남은 대화가 없으면 빈 화면을 둡니다.
- 답변을 기다리는 동안 '생각 중...' 을 보여 줍니다.

[지킬 것]
- 메시지에는 수정·삭제 기능을 넣지 않습니다.
- 클래스 이름만 붙이고 CSS 파일은 따로 작성합니다.
  1. {프론트 기술} — 쓰는 기술입니다. 예: React 함수형 컴포넌트, useState, useEffect
  2. {서버 주소} — 백엔드 주소입니다. 예: http://localhost:8000
  3. {제목 본문 형식} · {메시지 본문 형식} — 서버로 보내는 본문 모양입니다. 예: {"title": "새 제목"}, {"text": "질문"}
  4. {대기 문구} — 답변을 기다릴 때 띄울 글자입니다. 예: 생각 중...

AI가 대화 목록과 대화창을 각각 다른 파일로 나눠 만들 때가 있습니다. 파일이 여러 개면 붙여 넣을 자리가 늘어나므로, 한 파일로 달라고 요청에 적어 두면 손이 덜 갑니다. 나눠 만들어 준 코드를 쓸 때는 파일 이름과 import 줄을 함께 확인합니다.

6 – 배포한 서버에 반영하기

6.1 – 데이터베이스 파일을 깃허브에서 빼기

  1. 프로젝트 맨 위 폴더(my-chatbot)의 .gitignore 파일을 엽니다.
  2. 파일 맨 아래에 아래 한 줄을 추가합니다.
backend/chat.db
  1. 파일을 저장합니다.
  2. 터미널에서 프로젝트 맨 위 폴더로 이동한 다음 아래 명령을 차례로 입력합니다.
git add .
git commit -m "add chat session CRUD"
git push
  1. 깃허브 저장소 페이지를 새로고침해 db.py 파일이 올라갔는지 확인합니다.

저장소 파일 목록에 db.py 는 보이고 chat.db 는 보이지 않습니다. .gitignore 는 깃이 관리하지 않을 파일을 적어 두는 파일입니다. chat.db 에는 내 컴퓨터에서 주고받은 대화가 담겨 있으므로 공개 저장소에 올리지 않습니다.

.gitignore 에 적기 전에 이미 chat.db 를 올렸다면 이 설정만으로는 빠지지 않습니다. 이때는 git rm --cached backend/chat.db 를 입력해 관리 목록에서 뺀 다음 다시 커밋합니다.

6.2 – 배포한 서버에서 대화가 어떻게 남는지 확인하기

  1. https://render.com 에 접속해 로그인합니다.
  2. 배포해 둔 웹 서비스 이름을 클릭합니다.
  3. 화면 우상단의 [Manual Deploy] 를 클릭하고 [Deploy latest commit] 을 클릭합니다.
  4. 상태 표시가 Live 로 바뀔 때까지 기다립니다. 무료 플랜에서는 약 2분~5분이 걸립니다.
  5. 브라우저 주소창에 배포된 주소 뒤에 /docs 를 붙여 접속하고, 세션을 만들어 메시지를 두어 개 보냅니다.
  6. 같은 주소 뒤에 /sessions 를 붙여 접속해 목록을 확인합니다.
  7. 3번을 다시 실행해 한 번 더 배포한 다음, /sessions 를 다시 확인합니다.

5번과 6번에서는 대화가 목록에 쌓이지만, 다시 배포한 뒤에는 목록이 비어 있습니다. Render 공식 문서는 디스크를 연결하지 않은 서비스의 파일 시스템이 임시(ephemeral)라고 밝히고 있습니다. 서비스를 다시 배포하거나 다시 시작할 때마다 그 안에서 바뀐 파일이 사라진다는 뜻입니다. chat.db 도 서버 안의 파일이므로 함께 사라집니다.

같은 문서는 파일을 계속 남기는 디스크를 유료 서비스에 붙일 수 있다고 밝히고 있습니다. 대화를 오래 남겨야 하면 유료 디스크를 붙이거나, 파일이 아닌 별도 데이터베이스 서비스에 연결하는 방법을 씁니다. 무료 플랜으로 실습할 때는 배포된 챗봇의 대화가 배포할 때마다 초기화된다는 점을 알고 씁니다.

Render 무료 플랜은 15분간 요청이 없으면 절전 모드로 바뀝니다. 절전에서 깨어나며 다시 시작할 때도 파일 시스템이 초기화되므로, 한동안 쓰지 않은 뒤에 접속하면 /sessions 목록이 비어 있을 수 있습니다. (2026-09-07 확인, https://render.com/docs/disks)

코칭 프롬프트

이 부분의 코드를 대화형 AI로 만들어 보려면 아래 프롬프트를 씁니다. 변수 자리를 자기 값으로 채워 붙여 넣습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 앞선 대화가 남아 있으면 그 내용이 섞여 엉뚱한 답이 나옵니다.


아래 프로젝트를 공개 저장소에 올리고 배포하려고 합니다. 요청한 파일만 작성해 주세요. 설명은 파일 뒤에 다섯 줄 이내로 붙여 주세요.

[폴더 구조]
{폴더 구조}

[사용 기술] 백엔드 {백엔드 기술}, 프론트엔드 {프론트 기술}, 배포 {배포 서비스}

[만들 것]
1. 프로젝트 맨 위 폴더에 둘 .gitignore 파일 전문
   - 비밀 키가 든 환경변수 파일
   - 가상환경 폴더
   - 데이터베이스 파일 {DB 파일 이름}
   - 프론트엔드 의존성 폴더와 빌드 결과 폴더
2. 이미 저장소에 올라간 파일을 관리 목록에서 빼는 명령

[함께 알려 줄 것]
- {배포 서비스} 무료 플랜에서 서버 안에 만든 파일이 재배포·재시작 때 어떻게 되는지
- 대화를 계속 남기려면 어떤 선택지가 있는지 (근거가 되는 공식 문서 주소를 함께 적어 주세요)
아래 프로젝트를 공개 저장소에 올리고 배포하려고 합니다. 요청한 파일만 작성해 주세요. 설명은 파일 뒤에 다섯 줄 이내로 붙여 주세요.

[폴더 구조]
my-chatbot/
├── backend/  (main.py, db.py, requirements.txt, .env, .venv/, chat.db)
└── frontend/ (src/, package.json, node_modules/, dist/)

[사용 기술] 백엔드 Python + FastAPI + SQLite, 프론트엔드 React + Vite, 배포 Render

[만들 것]
1. 프로젝트 맨 위 폴더에 둘 .gitignore 파일 전문
   - 비밀 키가 든 환경변수 파일
   - 가상환경 폴더
   - 데이터베이스 파일 chat.db
   - 프론트엔드 의존성 폴더와 빌드 결과 폴더
2. 이미 저장소에 올라간 파일을 관리 목록에서 빼는 명령

[함께 알려 줄 것]
- Render 무료 플랜에서 서버 안에 만든 파일이 재배포·재시작 때 어떻게 되는지
- 대화를 계속 남기려면 어떤 선택지가 있는지 (근거가 되는 공식 문서 주소를 함께 적어 주세요)
  1. {폴더 구조} — 지금 프로젝트의 폴더와 파일입니다.
  2. {백엔드 기술} · {프론트 기술} · {배포 서비스} — 쓰는 기술과 배포처입니다. 예: Python + FastAPI + SQLite, React + Vite, Render

AI가 알려 준 무료 플랜 정책은 그대로 믿지 말고 함께 받은 공식 문서 주소를 열어 확인합니다. 요금제와 정책은 자주 바뀌고, AI가 예전 내용을 답하는 경우가 있습니다.

댓글 남기기