Django와 React를 활용하여 ToDo 웹 애플리케이션 만들기

최병훈·2024년 9월 27일
post-thumbnail
  • 서버 애플리케이션과 클라이언트 애플리케이션을 분리해서 개발하고 통신이 가능하도록 작업
  • 서버 애플리케이션은 Maria DB 와 Python Django 로 개발하고 클라이언트 애플리케이션은 React 로 개발

1. Server Application

1) 사용할 데이터베이스 준비

  • Maria DB 에 접속하여 사용할 데이터베이스 생성
    create database tododatabase;

2) 서버용 애플리케이션을 저장할 디렉토리를 생성

3) 서버용 애플리케이션 생성

  • 가상 환경 생성(별도의 파이썬 환경 - 배포를 할 때는 가상 환경을 만들고 애플리케이션을 제작)

    python -m venv myvenv
  • 가상 환경 활성화

    myvenv\Scripts\activate
    • 활성화가 되면 맨 앞에 (가상환경이름)이 추가됩니다.
    • 이제부터 패키지는 전부 가상환경에 설치됩니다.
  • 필요한 패키지 설치

    pip install django mysqlclient djangorestframework
  • 프로젝트 생성

    django-admin startproject todoproject .
  • 애플리케이션 생성

    python manage.py startapp todoapplication
  • 실행

    python manage.py makemigrations
    python manage.py migrate
    python manage.py runserver 0.0.0.0:80
    • 브라우저에서 "localhost" 실행 확인

4) 데이터베이스 연동을 위한 모델을 생성

  • application 디렉토리의 models.py 파일에 작성

    from django.db import models
    
    class Todo(models.Model):
        # auto increment
        id = models.AutoField(primary_key=True)
        userid=models.CharField(max_length=100)
        title=models.CharField(max_length=100)
        done= models.BooleanField()
        regdate = models.DateTimeField(auto_now_add=True)
        moddate = models.DateTimeField(auto_now_add=True)
  • 변경 내용 적용

    python manage.py makemigrations
    python manage.py migrate

  • 데이터베이스에서 확인
    show tables
    desc todoapplication_todo

5) CRUD 작업

  • URL 설정 : project의 urls.py

    from django.contrib import admin
    from django.urls import path
    
    from todoapplication import views
    
    urlpatterns = [
        path('admin/', admin.site.urls),
    
        # todo 요청은 todoapplication 의 views.py 파일의 TodoView 클래스가 처리
        path('todo', views.TodoView.as_view())
    ]
  • application 의 views.py 파일에 url에 대한 요청을 처리하는 로직을 작성

from django.shortcuts import render

# 클래스로 요청 처리하기 위해서
from django.views import View
# JSON 리턴
from django.http import JsonResponse
# 상태 코드 사용
from rest_framework import status
# 웹 요청 처리 설정을 위한 decorator
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator

from datetime import datetime
import json
# models.py 에 생성한 Todo Model import
from .models import Todo

# Todo 인스턴스를 JSON 형태로 바꿔주는 메서드
def todoToDictionary(todo:Todo) -> dict:
    result = {
        "id" : todo.id,
        "userid" : todo.userid,
        "title" : todo.title,
        "done" : todo.done,
        "regdate" : todo.regdate,
        "moddate" : todo.moddate
    }
    return result

# 요청 처리 클래스
@method_decorator(csrf_exempt, name='dispatch')
class TodoView(View):
    # 삽입 요청 처리
    def post(self, request):
        # 클라이언트가 전송한 데이터 가져오기
        # 클라이언트가 전송한 데이터가 dict로 만들어집니다.
        request = json.loads(request.body)

        # 데이터에서 userid 와 title 읽기
        userid = request["userid"]
        title = request["title"]

        # 삽입할 데이터 생성
        todo = Todo()
        todo.userid = userid
        todo.title = title
        todo.done = False

        # 데이터 삽입
        todo.save()

        # userid 에 해당하는 데이터 가져오기
        if userid != None:
            todos = Todo.objects.filter(userid=userid)
        else:
            todos = Todo.objects.all()

        return JsonResponse({'list':list(todos.values())}, status=status.HTTP_200_OK)


    # 조회 요청 처리
    def get(self, request):
        # userid 라는 파라미터 읽어오기
        userid = request.GET.get("userid", None)
                # userid 에 해당하는 데이터 가져오기
        if userid != None:
            todos = Todo.objects.filter(userid=userid)
        else:
            todos = Todo.objects.all()

        return JsonResponse({'list':list(todos.values())}, status=status.HTTP_200_OK)


    # 수정 처리
    def put(self, request):
        # 필요한 파라미터 읽기 - userid, id, done
        request = json.loads(request.body)

        userid = request["userid"]
        id = request["id"]
        done = request["done"]

        # 수정할 데이터 찾아오기
        todo = Todo.objects.get(id=id)

        if todo.userid == userid:
            todo.done = done
            todo.moddate = datetime.today()
            # 데이터 수정
            todo.save()
            return JsonResponse({'result':True,
                                 'data': todoToDictionary(todo)},
                                 status=status.HTTP_200_OK)
        else:
            return JsonResponse({'result':False,
                                 'data': "수정 권한이 없음"},
                                 status=status.HTTP_200_OK)


    # 삭제 처리
    def delete(self, request):
        # 필요한 파라미터 읽기 - userid, id, done
        request = json.loads(request.body)
        
        userid = request["userid"]
        id = request["id"]

        # 수정할 데이터 찾아오기
        todo = Todo.objects.get(id=id)

        if todo.userid == userid:
            # 데이터 삭제
            todo.delete()
            return JsonResponse({'result':True},
                                 status=status.HTTP_200_OK)
        else:
            return JsonResponse({'result':False,
                                 'data': "수정 권한이 없음"},
                                 status=status.HTTP_200_OK)
  • POSTMAN 을 통해 POST 요청

  • POSTMAN 을 통해 GET 요청

  • POSTMAN 을 통해 PUT 요청

    • 데이터베이스에서 확인
  • POSTMAN 을 통해 DELETE 요청

6) 패키지 외부로 내보내기

  • 파일명은 관습적으로 requirements.txt 를 사용
    pip freeze > requirements.txt
  • 생성된 requirements.txt 파일
    asgiref==3.8.1
    Django==5.1.1
    djangorestframework==3.15.2
    mysqlclient==2.2.4
    sqlparse==0.5.1
    tzdata==2024.2

2. Client Application

1) 프로젝트 생성

yarn create react-app client

2) 아이콘 사용을 위한 패키지 설치

npm install --save --legacy-peer-deps @material-ui/core
npm install --save --legacy-peer-deps @material-ui/icons

3) 상단에 배치될 컴포넌트를 생성

  • src/ToDo.jsx(확장자는 일반적으로 js, jsx, ts, tsx 등을 사용)
import React from "react";

class ToDo extends React.Component {
    render() {
        return (
            <div className="ToDo">
	            <input type="checkbox" id="todo0" name="todo0" value="todo0" />
    	        <lable for="todo0">ToDo 컴포넌트 만들기</lable>
            </div>
        );
    }
}

export default ToDo;

4) App.js 파일을 수정해서 앞에서 생성한 컴포넌트를 출력

import React from "react";
import ToDo from "./ToDo";
import './App.css';

function App() {
  return (
    <div className="App">
      <ToDo/>
    </div>
  );
}

export default App;

5) ToDo.jsx 파일을 수정해서 Props와 State 사용

  • Props : 상위 컴포넌트로부터 데이터를 받을 때 사용
  • State : 현재 컴포넌트의 상태를 저장할 때 사용
  • React 는 Props 나 State 가 변경되면 컴포넌트를 자동으로 재출력
import React from "react";

class ToDo extends React.Component {
  constructor(props) {
    super(props);
    // 상위 컴포넌트로부터 넘겨받은 item 속성의 값을 item 이라는 이름으로 저장
    this.state = { item: props.item };
  }

  render() {
    return (
      <div className="ToDo">
        <input
          type="checkbox"
          id={this.state.item.id}
          name={this.state.item.id}
          checked={this.state.item.done}
        />
        <lable id={this.state.item.id}>{this.state.item.title}</lable>
      </div>
    );
  }
}

export default ToDo;
  • App.js 파일을 수정해서 데이터를 생성해서 ToDo 컴포넌트에 넘겨주기
import React from "react";
import ToDo from "./ToDo";
import './App.css';

class App extends React.Component {
  constructor(props){
    super(props)
    this.state = {item:{id:0, title:"안녕 React", done:true}}
  }

  render(){
    return (
      <div className="App">
        <ToDo item={this.state.item}/>
      </div>
    );
  }
}

export default App;
  • 화면에 내용이 출력되는지 확인

5) App.js 파일을 ToDo 배열을 출력하도록 수정

  • 배열을 출력할 때는 상위 컴포넌트에서 반복문을 돌려서 하위 컴포넌트를 여러 개 만들면 됩니다.
  • key를 설정하지 않으면 화면에서는 아무런 문제가 되지 않는데, 자바스크립트 검사창에는 에러가 발생
import React from "react";

class ToDo extends React.Component {
  constructor(props) {
    super(props);
    // 상위 컴포넌트로부터 넘겨받은 item 속성의 값을 item 이라는 이름으로 저장
    this.state = { item: props.item };
  }

  render() {
    return (
      <div className="ToDo">
        <input
          type="checkbox"
          id={this.state.item.id}
          name={this.state.item.id}
          checked={this.state.item.done}
        />
        <lable id={this.state.item.id}>{this.state.item.title}</lable>
      </div>
    );
  }
}

export default ToDo;
  • 브라우저에서 확인

6) 디자인 변경

  • ToDo.jsx 파일 수정
import React from "react";

import { ListItem, ListItemText, InputBase, Checkbox } from "@material-ui/core";

class ToDo extends React.Component {
  constructor(props) {
    super(props);
    // 상위 컴포넌트로부터 넘겨받은 item 속성의 값을 item 이라는 이름으로 저장
    this.state = { item: props.item };
  }

  render() {
    const item = this.state.item;
    return (
      <ListItem>
        <Checkbox checked={item.done} />
        <ListItemText>
          <InputBase
            inputProps={{ "aria-label": "naked" }}
            type="text"
            id={item.id}
            name={item.id}
            value={item.title}
            multiline={true}
            fullWidth={true}
          />
        </ListItemText>
      </ListItem>
    );
  }
}

export default ToDo;
  • 브라우저에서 확인
  • App.js 파일 수정
import React from "react";
import ToDo from "./ToDo";
import "./App.css";

import { Paper, List } from "@material-ui/core";

class App extends React.Component {
  constructor(props) {
    super(props);
    // 데이터 배열 생성
    this.state = {
      items: [
        { id: 0, title: "안녕 React", done: true },
        { id: 1, title: "블로그 쓰기", done: false },
        { id: 2, title: "집에 가기", done: true },
      ],
    };
  }

  render() {
    let todoItems = this.state.items.length > 0 && (
      <Paper style={{ margin: 16 }}>
        <List>
          {this.state.items.map((item, idx) => (
            <ToDo item={item} />
          ))}
        </List>
      </Paper>
    );

    return <div className="App">{todoItems}</div>;
  }
}

export default App;
  • 브라우저에서 확인

7) 데이터 추가 구현

  • 데이터를 추가하는 컴포넌트 AddToDo.jsx 파일을 생성하고 작성
import React from "react";
import { TextField, Paper, Button, Grid } from "@material-ui/core";

class AddToDo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { item: { title: "" } };
  }

  render() {
    return (
      <Paper style={{ margin: 16, padding: 16 }}>
        <Grid container>
          <Grid xs={11} md={11} item style={{ paddingRight: 16 }}>
            <TextField placeholder="title 입력" fullWidth />
          </Grid>
          <Grid xs={1} md={1} item style={{ paddingRight: 16 }}>
            <Button fullWidth color="secondary" variant="outlined">
              +
            </Button>
          </Grid>
        </Grid>
      </Paper>
    );
  }
}

export default AddToDo;
  • App.js 파일에서 출력
import React from "react";
import ToDo from "./ToDo";
import "./App.css";

// AddToDo 컴포넌트 추가
import AddToDo from "./AddToDo";

// Container 추가
import { Paper, List, Container } from "@material-ui/core";

class App extends React.Component {
  constructor(props) {
    super(props);
    // 데이터 배열 생성
    this.state = {
      items: [
        { id: 0, title: "안녕 React", done: true },
        { id: 1, title: "블로그 쓰기", done: false },
        { id: 2, title: "집에 가기", done: true },
      ],
    };
  }

  render() {
    let todoItems = this.state.items.length > 0 && (
      <Paper style={{ margin: 16 }}>
        <List>
          {this.state.items.map((item, idx) => (
            <ToDo item={item} />
          ))}
        </List>
      </Paper>
    );

    return (
      <div className="App">
        <Container maxWidth="md">
          <AddToDo />
          <div>{todoItems}</div>
        </Container>
      </div>
    );
  }
}

export default App;
  • 브라우저에서 확인

8) 데이터 추가 이벤트 핸들러 구현

  • React 와 같은 SPA 에서는 이벤트 처리를 다르게 구현
    • App.js 파일에 이벤트 핸들러가 사용할 함수나 메서드를 만들고 하위 컴포넌트에 넘겨서 연결하도록 합니다.
    • SPA 에서는 전체 화면 출력을 위한 컴포넌트가 존재하고 그 안에 하위 컴포넌트들을 배치해서 사용
    • 각 하위 컴포넌트끼리는 완전 독립적으로 구현을 합니다.
    • 데이터를 최상위 컴포넌트가 가지고 있으면 사용이 쉽지만 데이터를 다른 컴포넌트가 가지고 있으면 이를 넘가는 동작을 구현하는 것이 어려워집니다.
    • 데이터를 조작하는 이벤트 핸들러도 최상위 컴포넌트가 가지고 있는 게 작업이 편리
  • App.js 파일에 데이터 추가 함수 구현하고 AddToDo 컴포넌트에 전달
import React from "react";
import ToDo from "./ToDo";
import "./App.css";

// AddToDo 컴포넌트 추가
import AddToDo from "./AddToDo";

// Container 추가
import { Paper, List, Container } from "@material-ui/core";

class App extends React.Component {
  constructor(props) {
    super(props);
    // 데이터 배열 생성
    this.state = {
      items: [
        { id: 0, title: "안녕 React", done: true },
        { id: 1, title: "블로그 쓰기", done: false },
        { id: 2, title: "집에 가기", done: true },
      ],
    };
  }

  // 데이터 추가를 위한 함수 : item 1개를 넘겨받아서 items에 추가하는 함수
  add = (item) => {
    // react의 state와 props는 불변의 객체
    // 수정 작업을 할 때는 다른 곳에 복사를 한 후 수정하고 다시 state 나 props 에 설정

    // 기존의 state에 있는 items를 thisItems에 복사
    const thisItems = this.state.items;

    // 새로운 item에 데이터를 추가 설정
    item.id = "ID-" + thisItems.length;
    item.done = false;

    // thisItems에 데이터를 추가
    thisItems.push(item);

    // state의 값을 thisItems로 변경
    this.setState({ items: thisItems });
  };

  render() {
    let todoItems = this.state.items.length > 0 && (
      <Paper style={{ margin: 16 }}>
        <List>
          {this.state.items.map((item, idx) => (
            <ToDo item={item} />
          ))}
        </List>
      </Paper>
    );

    return (
      <div className="App">
        <Container maxWidth="md">
          <AddToDo add={this.add} />
          <div>{todoItems}</div>
        </Container>
      </div>
    );
  }
}

export default App;
  • AddToDo.jsx 파일에서 데이터 추가 함수를 넘겨받고, 버튼의 이벤트 핸들러(콜백 함수, 리스너 객체)로 설정
import React from "react";
import { TextField, Paper, Button, Grid } from "@material-ui/core";

class AddToDo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { item: { title: "" } };
    // 상위 컴포넌트로부터 넘겨받은 함수 가져오기
    this.add = props.add;
  }

  /* 이벤트 처리를 위한 함수 */
  // Input의 내용이 변경되었을 때 호출되는 함수
  onInputChange = (e) => {
    // Input 의 내용이 변경되면 데이터의 title 을 Input 으로 수정
    const thisItem = this.state.item;
    thisItem.title = e.target.value;
    this.setState({ item: thisItem });
  };

  // + 버튼을 눌렀을 때 호출되는 함수
  onButtonClick = (e) => {
    this.add(this.state.item);
    this.setState({ item: { title: "" } });
  };

  // Enter 를 눌렀을 때 호출되는 함수
  enterKeyEventHandler = (e) => {
    if (e.key === "Enter") {
      this.onButtonClick();
    }
  };

  render() {
    return (
      <Paper style={{ margin: 16, padding: 16 }}>
        <Grid container>
          <Grid xs={11} md={11} item style={{ paddingRight: 16 }}>
            <TextField
              placeholder="title 입력"
              fullWidth
              value={this.state.item.title}
              onChange={this.onInputChange}
              onKeyPress={this.enterKeyEventHandler}
            />
          </Grid>
          <Grid xs={1} md={1} item style={{ paddingRight: 16 }}>
            <Button
              fullWidth
              color="secondary"
              variant="outlined"
              onClick={this.onButtonClick}
            >
              +
            </Button>
          </Grid>
        </Grid>
      </Paper>
    );
  }
}

export default AddToDo;
  • 브라우저에서 확인 : title을 입력하고 + 버튼을 누르면 리스트에 추가

  • 데이터를 추가하면 목록 부분에 바로 반영이 됩니다.
    • react는 명시적으로 화면을 재출력하지 않더라도, state나 props가 변경되면 화면을 재출력합니다.

3. 연결

1) 클라이언트에서 서버에 데이터 요청을 보내도록 설정

  • App.js 수정
import React from "react";
import ToDo from "./ToDo";
import "./App.css";

// AddToDo 컴포넌트 추가
import AddToDo from "./AddToDo";

// Container 추가
import { Paper, List, Container } from "@material-ui/core";

class App extends React.Component {
  constructor(props) {
    super(props);
    // 데이터 배열 생성
    this.state = { items: [] };
  }

  // 컴포넌트가 메모리에 로드가 되면 호출되는 메서드
  componentDidMount() {
    const requestoptions = {
      method: "GET",
      Headers: { "Content-Type": "application/json" },
    };

    fetch("http://localhost/todo", requestoptions)
      .then((response) => response.json())
      .then(
        (response) => {
          this.setState({ items: response.list });
        },
        (error) => {
          console.log(error);
        }
      );
  }

  // 데이터 추가를 위한 함수 : item 1개를 넘겨받아서 items에 추가하는 함수
  add = (item) => {
    // react의 state와 props는 불변의 객체
    // 수정 작업을 할 때는 다른 곳에 복사를 한 후 수정하고 다시 state 나 props 에 설정

    // 기존의 state에 있는 items를 thisItems에 복사
    const thisItems = this.state.items;

    // 새로운 item에 데이터를 추가 설정
    item.id = "ID-" + thisItems.length;
    item.done = false;

    // thisItems에 데이터를 추가
    thisItems.push(item);

    // state의 값을 thisItems로 변경
    this.setState({ items: thisItems });
  };

  render() {
    let todoItems = this.state.items.length > 0 && (
      <Paper style={{ margin: 16 }}>
        <List>
          {this.state.items.map((item, idx) => (
            <ToDo item={item} />
          ))}
        </List>
      </Paper>
    );

    return (
      <div className="App">
        <Container maxWidth="md">
          <AddToDo add={this.add} />
          <div>{todoItems}</div>
        </Container>
      </div>
    );
  }
}

export default App;
  • 브라우저에서 콘솔의 메시지 확인

    웹 서버에서 CORS 설정을 하지 않아서 웹 클라이언트의 ajax 나 Fetch API 로는 데이터를 가져오지 못해서 에러가 발생

2) Django 프로젝트에서 CORS 설정을 추가해서 클라이언트에서 데이터를 가져갈 수 있도록 수정

  • 가상 환경에 패키지를 설치

    • 패키지 이름 : django-cors-headers
    • 가상 환경을 활성화 한 후, 아래 명령을 수행
      pip install django-cors-headers
  • settings.py 파일을 수정

    • INSTALLED_APPS 에 'corsheaders' 추가

      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
          'rest_framework',
          'todoapplication',
          'corsheaders'
      ]
    • MIDDLEWARE 의 최상단에 'corsheaders.middleware.CorsMiddleware' 추가

      MIDDLEWARE = [
          'corsheaders.middleware.CorsMiddleware',
          'django.middleware.security.SecurityMiddleware',
          'django.contrib.sessions.middleware.SessionMiddleware',
          'django.middleware.common.CommonMiddleware',
          'django.middleware.csrf.CsrfViewMiddleware',
          'django.contrib.auth.middleware.AuthenticationMiddleware',
          'django.contrib.messages.middleware.MessageMiddleware',
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
      ]
    • settings.py 파일에 허용할 URL 을 설정

      CORS_ORIGIN_WHITELIST = ['http://localhost:3000']
      CORS_ALLOW_CREDENTIALS = True
  • 브라우저에서 다시 확인

    • 웹 클라이언트가 서버의 데이터를 받아 출력하였다.
    • 브라우저를 통해 서버의 API 요청한 모습
    • 데이터베이스의 데이터 모습

0개의 댓글