13. CRUD3-React 화면 붙이기 – 대화방 목록을 브라우저에 그리기

1 – React 프로젝트 만들기

1.1 – Node.js가 설치돼 있는지 확인하기

  1. 새 터미널을 엽니다. Django 서버가 도는 터미널은 그대로 둡니다.
  2. 아래 명령을 입력합니다.
node --version

v22.11.0처럼 버전 번호가 나타나면 설치돼 있습니다. React는 자바스크립트로 화면을 만드는 도구이며, 자바스크립트를 컴퓨터에서 실행하려면 Node.js가 있어야 합니다. Node.js는 브라우저 밖에서 자바스크립트를 실행하는 프로그램입니다. 앞의 숫자가 18보다 낮거나 명령을 찾지 못하면 https://nodejs.org/에서 LTS 표시가 붙은 버전을 내려받아 설치합니다.

1.2 – Vite로 프로젝트 만들기

  1. chatbot-app 폴더로 이동합니다.
cd ~/chatbot-app
  1. React 프로젝트를 만듭니다. 명령 도중 물음이 나오면 Enter로 넘깁니다.
npm create vite@latest frontend -- --template react
  1. 만들어진 폴더로 들어가 도구를 내려받습니다.
cd frontend && npm install

내려받기에는 약 30초에서 2분이 걸립니다.

  1. 개발용 서버를 켭니다.
npm run dev

화면에 Local: http://localhost:5173/이라는 줄이 나타납니다.

  1. 브라우저에서 http://localhost:5173/에 접속합니다.

Vite와 React 로고가 있는 기본 화면이 나타납니다. 이 화면은 뒤에서 통째로 바꿉니다.

Vite(비트)는 React 프로젝트의 뼈대를 만들고 개발용 서버를 띄우는 도구입니다. 파일을 고치면 브라우저 화면이 바로 바뀌므로, 고칠 때마다 새로고침하지 않아도 됩니다. React 폴더를 Django 폴더 안에 겹쳐 두지 않고 나란히 둔 것은 배포할 때 어느 파일을 어디로 올릴지 가르기 위해서입니다.

이제 터미널 창이 두 개 필요합니다. 하나는 Django 서버(8000번), 하나는 React 서버(5173번)를 각각 맡습니다. 둘 다 켜져 있어야 화면이 데이터를 받습니다.

실습 코칭 프롬프트

이 부분을 혼자 해 보다 막히면 아래 프롬프트를 대화형 AI(ChatGPT·Claude·Gemini)에 붙여 넣어 한 단계씩 도움받습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 실습 대화창에는 앞서 시킨 다른 지시가 남아 있어, 그 위에 얹으면 답이 뒤섞입니다.

아래는 Vite로 React 프로젝트 만들기 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: Django 폴더 옆에 frontend 폴더를 만들어 React 개발용 서버를 5173번으로 띄우는 것입니다.
지금까지 한 것: {지금까지 한 것}
막힌 지점: {막힌 지점}
화면에 나온 메시지: {에러 메시지}

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- 명령을 어느 폴더에서 실행했는지, npm install 을 마쳤는지 먼저 확인해 주세요.
아래는 Vite로 React 프로젝트 만들기 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: Django 폴더 옆에 frontend 폴더를 만들어 React 개발용 서버를 5173번으로 띄우는 것입니다.
지금까지 한 것: npm create vite@latest frontend 까지 실행했습니다
막힌 지점: npm run dev 를 입력하면 실행이 안 됩니다
화면에 나온 메시지: Could not read package.json

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- 명령을 어느 폴더에서 실행했는지, npm install 을 마쳤는지 먼저 확인해 주세요.
  1. {지금까지 한 것} — 어느 단계까지 마쳤는지 적습니다. 예: django-admin startproject config . 까지 실행했습니다
  2. {막힌 지점} — 무엇을 하려다 안 됐는지 적습니다. 예: python manage.py runserver 를 실행하면 서버가 바로 꺼집니다
  3. {에러 메시지} — 화면에 나온 글을 그대로 붙여 넣습니다. 없으면 없음이라고 적습니다.

AI가 고친 코드 전체를 한 번에 내놓으면 “코드 전체 말고 어느 줄을 왜 고쳐야 하는지 한 단계만 알려 주세요”라고 되물으십시오. 에러 메시지를 요약해 적으면 원인을 못 짚습니다. 화면에 나온 글을 그대로 붙여 넣습니다.

2 – 두 서버를 잇기

2.1 – CORS 설정으로 차단 풀기

  1. Django 쪽 터미널에서 서버를 끄고 아래 도구를 설치합니다.
pip install django-cors-headers
  1. 편집기로 config/settings.py 파일을 엽니다.
  2. INSTALLED_APPS 목록에 한 줄을 더합니다.
INSTALLED_APPS = [
    # ... 앞의 줄은 그대로 둡니다
    "rest_framework",
    "corsheaders",   # 추가
    "chat",
]
  1. MIDDLEWARE 목록을 찾아 맨 위에 한 줄을 더합니다.
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",   # 반드시 맨 위
    "django.middleware.security.SecurityMiddleware",
    # ... 나머지는 그대로 둡니다
]
  1. 파일 맨 아래에 허용할 주소를 적습니다.
# 이 주소에서 오는 요청만 브라우저가 통과시킵니다.
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
]
  1. 파일을 저장하고 서버를 다시 켭니다.

CORS(교차 출처 리소스 공유)는 브라우저가 다른 주소의 서버에 보낸 요청을 어디까지 허용할지 정하는 규칙입니다. React는 5173번, Django는 8000번을 쓰므로 브라우저는 둘을 다른 사이트로 봅니다.

막는 쪽이 브라우저라는 점이 중요합니다. Django는 요청을 정상으로 처리하고 응답까지 보내지만, 브라우저가 그 응답을 화면 코드에 넘기지 않습니다. 그래서 Django 터미널에는 오류가 없는데 화면에서만 실패하는 모양이 됩니다. 방금 넣은 설정은 Django가 “이 주소에서 오는 요청은 받아도 된다”고 응답에 적게 하는 것입니다.

CorsMiddleware를 맨 위에 두는 이유가 있습니다. 미들웨어는 위에서부터 차례로 요청을 거칩니다. 허용 표시를 붙이는 코드가 아래에 있으면, 위에 있는 다른 코드가 먼저 응답을 만들어 버려 표시가 붙지 않습니다.

CORS_ALLOW_ALL_ORIGINS = True로 모든 주소를 열어 두는 방법도 있습니다. 실습에서는 동작하지만 배포할 때는 쓰지 않습니다. 아무 사이트나 이 서버를 부를 수 있게 되기 때문입니다.

2.2 – API 주소를 환경변수로 빼기

  1. 편집기에서 새 파일을 만듭니다.
  2. 아래 한 줄을 적습니다.
VITE_API_BASE=http://127.0.0.1:8000/api
  1. frontend 폴더 안에 ".env"라는 이름으로 저장합니다.
  2. React 서버가 켜져 있으면 Ctrl+C로 끄고 다시 켭니다.
npm run dev

환경변수(environment variable)는 코드 밖에 값을 적어 두고 코드가 그 값을 읽어 쓰게 하는 방법입니다. React 코드에 서버 주소를 직접 적으면 배포할 때 그 주소를 찾아 고쳐야 하지만, 이렇게 빼 두면 주소가 바뀌어도 코드는 그대로 둡니다.

이름을 VITE_로 시작하는 것은 Vite의 규칙입니다. 이 앞부분이 붙은 값만 React 코드에서 읽을 수 있습니다. 앞부분을 빼면 값이 있어도 코드에서 undefined로 읽힙니다.

.env 파일을 고치면 React 서버를 다시 켜야 반영됩니다. 파일을 저장했는데 주소가 그대로면 대개 서버를 다시 켜지 않은 경우입니다.

VITE_가 붙은 값은 빌드할 때 화면 코드 안에 그대로 박힙니다. 브라우저에서 열어 보면 보이는 값이므로, 비밀번호나 토큰은 이 파일에 적지 않습니다.

실습 코칭 프롬프트

이 부분을 혼자 해 보다 막히면 아래 프롬프트를 대화형 AI(ChatGPT·Claude·Gemini)에 붙여 넣어 한 단계씩 도움받습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 실습 대화창에는 앞서 시킨 다른 지시가 남아 있어, 그 위에 얹으면 답이 뒤섞입니다.

아래는 CORS 설정과 환경변수 분리 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: django-cors-headers 를 설치해 5173번 주소를 허용하고, API 주소를 .env 파일로 빼는 것입니다.
지금까지 한 것: {지금까지 한 것}
막힌 지점: {막힌 지점}
화면에 나온 메시지: {에러 메시지}

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- CorsMiddleware 가 MIDDLEWARE 목록 맨 위에 있는지, 두 서버를 모두 다시 켰는지 먼저 확인해 주세요.
아래는 CORS 설정과 환경변수 분리 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: django-cors-headers 를 설치해 5173번 주소를 허용하고, API 주소를 .env 파일로 빼는 것입니다.
지금까지 한 것: corsheaders 를 설치하고 CORS_ALLOWED_ORIGINS 에 주소를 넣었습니다
막힌 지점: 화면에서 목록을 못 받아 옵니다
화면에 나온 메시지: has been blocked by CORS policy

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- CorsMiddleware 가 MIDDLEWARE 목록 맨 위에 있는지, 두 서버를 모두 다시 켰는지 먼저 확인해 주세요.
  1. {지금까지 한 것} — 어느 단계까지 마쳤는지 적습니다. 예: django-admin startproject config . 까지 실행했습니다
  2. {막힌 지점} — 무엇을 하려다 안 됐는지 적습니다. 예: python manage.py runserver 를 실행하면 서버가 바로 꺼집니다
  3. {에러 메시지} — 화면에 나온 글을 그대로 붙여 넣습니다. 없으면 없음이라고 적습니다.

AI가 고친 코드 전체를 한 번에 내놓으면 “코드 전체 말고 어느 줄을 왜 고쳐야 하는지 한 단계만 알려 주세요”라고 되물으십시오. 에러 메시지를 요약해 적으면 원인을 못 짚습니다. 화면에 나온 글을 그대로 붙여 넣습니다.

3 – 대화방 목록 화면 만들기

3.1 – 서버에서 목록을 받아 화면에 그리기

  1. 편집기로 frontend/src/App.jsx 파일을 엽니다.
  2. 파일 내용을 아래로 바꿉니다.
import { useEffect, useState } from "react";

// .env 에 적어 둔 주소를 읽습니다.
const API = import.meta.env.VITE_API_BASE;

export default function App() {
  // 대화방 목록을 담는 자리. 처음에는 빈 배열입니다.
  const [rooms, setRooms] = useState([]);
  // 받아오는 중인지 표시하는 자리.
  const [loading, setLoading] = useState(true);

  // 서버에서 목록을 받아 화면 값에 넣습니다.
  async function loadRooms() {
    setLoading(true);
    try {
      const res = await fetch(`${API}/conversations/`);
      if (!res.ok) throw new Error(`서버 응답 ${res.status}`);
      const data = await res.json();
      setRooms(data);
    } catch (err) {
      console.error(err);
    } finally {
      setLoading(false);
    }
  }

  // 화면이 처음 그려진 뒤 한 번 실행합니다.
  // 두 번째 값이 빈 배열이라 처음 한 번만 돕니다.
  useEffect(() => {
    loadRooms();
  }, []);

  if (loading) return <p>불러오는 중입니다.</p>;

  return (
    <div>
      <h1>대화방</h1>
      <ul>
        {rooms.map((room) => (
          // key 는 React가 어느 항목이 바뀌었는지 가리는 데 씁니다.
          <li key={room.id}>{room.title}</li>
        ))}
      </ul>
    </div>
  );
}
  1. 파일을 저장합니다.
  2. 브라우저에서 http://localhost:5173/을 봅니다.

앞서 만든 대화방 제목이 목록으로 나타납니다.

React는 화면을 그린 직후에 서버를 부르고, 응답이 오면 그 부분만 다시 그립니다. useState는 화면에 쓰는 값을 담아 두는 자리이고, useEffect는 화면이 그려진 뒤 할 일을 적는 자리입니다.

새로고침하지 않아도 바뀌는 것은 Vite가 파일 저장을 지켜보다가 바뀐 부분만 화면에 반영하기 때문입니다.

목록이 나오지 않으면 브라우저에서 F12를 눌러 개발자 도구를 열고 [Console] 탭을 봅니다. CORS policy라는 낱말이 든 붉은 글이 있으면 앞 절의 설정이 반영되지 않은 것이므로, Django 서버를 다시 켰는지 확인합니다.

Failed to fetch만 있고 다른 설명이 없으면 Django 서버가 꺼져 있는 경우가 대부분입니다. 8000번 터미널에 Quit the server with CONTROL-C라는 줄이 떠 있는지 확인합니다.

3.2 – 대화방 만들기와 지우기 붙이기

  1. App.jsxloadRooms 함수 아래에 아래 두 함수를 이어 적습니다.
  // 새 대화방을 만듭니다.
  async function createRoom() {
    const title = window.prompt("대화방 이름을 입력합니다.");
    // 취소를 누르면 null, 빈 칸이면 빈 글자가 들어오므로 둘 다 걸러 냅니다.
    if (!title) return;

    await fetch(`${API}/conversations/`, {
      method: "POST",
      // 보내는 내용이 JSON임을 알립니다. 이 줄이 없으면 서버가 글자로 읽습니다.
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title }),
    });

    // 서버에 저장된 뒤 목록을 다시 받아 화면을 맞춥니다.
    loadRooms();
  }

  // 대화방 하나를 지웁니다.
  async function removeRoom(id) {
    if (!window.confirm("이 대화방을 지웁니다.")) return;

    await fetch(`${API}/conversations/${id}/`, { method: "DELETE" });
    loadRooms();
  }
  1. 화면 부분의 return 안을 아래로 바꿉니다.
  return (
    <div>
      <h1>대화방</h1>
      <button onClick={createRoom}>새 대화방</button>
      <ul>
        {rooms.map((room) => (
          <li key={room.id}>
            {room.title}
            <button onClick={() => removeRoom(room.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
  1. 파일을 저장하고 브라우저에서 [새 대화방]을 클릭합니다.

이름을 묻는 작은 창이 뜨고, 입력한 이름이 목록에 나타납니다. [삭제]를 누르면 그 항목이 사라집니다.

삭제 버튼의 onClick에 화살표 함수를 쓴 이유가 있습니다. onClick={removeRoom(room.id)}로 적으면 화면을 그리는 순간 함수가 실행돼 버려, 누르지도 않았는데 전부 지워집니다. 화살표 함수로 감싸면 누를 때 실행됩니다.

서버에 보낸 뒤 목록을 다시 받아 오는 방식은 요청이 한 번 더 늘지만, 화면 값과 데이터베이스 값이 어긋날 일이 없습니다. 화면 값만 고치는 방식은 빠른 대신 저장이 실패해도 화면에는 성공한 것처럼 보입니다.

3.3 – 대화방 제목 바꾸기 붙이기

  1. App.jsxremoveRoom 함수 아래에 아래 함수를 이어 적습니다.
  // 대화방 제목을 바꿉니다.
  async function renameRoom(id, currentTitle) {
    // 두 번째 값은 입력란에 미리 채워 둘 글자입니다.
    const title = window.prompt("새 이름을 입력합니다.", currentTitle);
    if (!title || title === currentTitle) return;

    await fetch(`${API}/conversations/${id}/`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title }),
    });

    loadRooms();
  }
  1. 목록 안 <li> 부분에 버튼을 하나 더 넣습니다.
          <li key={room.id}>
            {room.title}
            <button onClick={() => renameRoom(room.id, room.title)}>이름 변경</button>
            <button onClick={() => removeRoom(room.id)}>삭제</button>
          </li>
  1. 파일을 저장하고 [이름 변경]을 클릭합니다.

지금 이름이 채워진 입력창이 뜨고, 고쳐서 확인하면 목록의 이름이 바뀝니다. 수정에 PATCH 방식을 쓴 것은 칸 하나만 바꾸기 때문입니다. PUT은 모든 칸을 다시 보내야 하지만 PATCH는 바꿀 칸만 보냅니다. 이름을 고치지 않고 확인을 누르면 아무 요청도 가지 않습니다. 함수 맨 앞에서 값이 같은지 먼저 가리기 때문입니다.

이것으로 등록·조회·수정·삭제 네 동작이 화면에서 모두 동작합니다. 서버 쪽에 만들어 둔 주소를 화면이 요청 방식만 바꿔 가며 부르는 구조입니다.

window.prompt는 실습에서 화면 코드를 짧게 두려고 쓴 것입니다. 실제 서비스에서는 입력란과 버튼을 화면 안에 두는 편이 낫습니다. 브라우저 기본 창은 모양을 바꿀 수 없고 휴대전화에서 어색하게 보입니다.

실습 코칭 프롬프트

이 부분을 혼자 해 보다 막히면 아래 프롬프트를 대화형 AI(ChatGPT·Claude·Gemini)에 붙여 넣어 한 단계씩 도움받습니다.

실습에 쓰던 대화창이 아니라 새 대화창에 붙여 넣습니다. 실습 대화창에는 앞서 시킨 다른 지시가 남아 있어, 그 위에 얹으면 답이 뒤섞입니다.

아래는 화면에서 CRUD 네 동작 붙이기 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: 대화방 목록을 받아 그리고, 만들기·이름 변경·삭제 버튼을 붙이는 것입니다.
지금까지 한 것: {지금까지 한 것}
막힌 지점: {막힌 지점}
화면에 나온 메시지: {에러 메시지}

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- 버튼의 onClick 에 함수를 그대로 넘겼는지 화살표 함수로 감쌌는지 먼저 확인해 주세요.
아래는 화면에서 CRUD 네 동작 붙이기 실습을 혼자 해 보다 막혀서 도움을 받으려는 요청입니다.

지금 하려는 것: 대화방 목록을 받아 그리고, 만들기·이름 변경·삭제 버튼을 붙이는 것입니다.
지금까지 한 것: 목록을 받아 그리는 데까지 됐고 삭제 버튼을 붙였습니다
막힌 지점: 화면을 열자마자 대화방이 전부 지워집니다
화면에 나온 메시지: 없음

다음 규칙을 지켜 주세요.
- 정답 코드를 통째로 주지 마세요. 다음 한 단계만 알려 주세요.
- 먼저 왜 막혔는지 짚어 주고, 제가 직접 해 보게 하세요.
- 제가 해 본 결과를 말하면 그때 확인 질문을 던지고 다음 단계로 넘어가세요.
- 제가 놓치기 쉬운 부분이 보이면 마지막에 한 줄로 짚어 주세요.
- 버튼의 onClick 에 함수를 그대로 넘겼는지 화살표 함수로 감쌌는지 먼저 확인해 주세요.
  1. {지금까지 한 것} — 어느 단계까지 마쳤는지 적습니다. 예: django-admin startproject config . 까지 실행했습니다
  2. {막힌 지점} — 무엇을 하려다 안 됐는지 적습니다. 예: python manage.py runserver 를 실행하면 서버가 바로 꺼집니다
  3. {에러 메시지} — 화면에 나온 글을 그대로 붙여 넣습니다. 없으면 없음이라고 적습니다.

AI가 고친 코드 전체를 한 번에 내놓으면 “코드 전체 말고 어느 줄을 왜 고쳐야 하는지 한 단계만 알려 주세요”라고 되물으십시오. 에러 메시지를 요약해 적으면 원인을 못 짚습니다. 화면에 나온 글을 그대로 붙여 넣습니다.

댓글 남기기