Django 서버와 CRUD

LEE EUI JOO·2023년 2월 13일
0

Web Programming

목록 보기
16/17

Django Server 와 CRUD

데이터 구조

  • Id, text, checked

클라이언트 프로젝트


프로젝트 생성

패키지 설치

  • sass-loader sass node-sass : 스타일을 sass를 이용해서 설정
  • clanames : 클래스 이름을 편리하게 사용하기 위해서
  • react-icons : 아이콘 사용을 위해서
$ npm install sass-loader sass node-sass  
or
$ yarn add sass-loader sass node-sass  

기본 스타일을 수정 - src 디렉토리의 index.css

body {
  margin: 0;
  padding: 0;
  background: burlywood;
}

❓ 웹 클라이언트 프로그래밍에서 전체 영역에 margin:0; padding:0; 설정 이유

  • 브라우저 별 박스 모델의 계산 방법 차이 때문
    • margin : 바깥쪽 여백
    • border : 경계선
    • padding : 안쪽 여백
    • content : 내용
div{
	width:300px;
}
  • 이전 IE를 제외한 브라우저는 content 의 width 가 300px 가 된다
  • IE 는 border, padding, content를 합친 영역의 크기가 300px가 된다

출력해보기 위해 App.js 수정

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div>
      할일 목록 애플리케이션
    </div>
  );
}

export default App;

UI 구성

  • ToDoTemplate.jsx : 메인 화면

  • ToDoListitem : 하나의 항목 출력

  • ToDoList : 데이터 전체 출력


클라이언트 UI 구성

  • 컴포넌트 들이 저장될 디렉토리를 생성
  • 메인화면 구성을 위한 ToDoTemplate.jsx 파일 생성
import React from 'react';

import './ToDoTemplate.scss';

const ToDoTemplate = ({children}) => {
    return(
        <div className='ToDoTemplate'>
            <div className='app-title'>나의 할일 목록</div>
            <div className='content'>
                {children}
        </div>
    </div>
    )
}
export default ToDoTemplate;
  • ToDo 스타일링을 위한 ToDoTemplate.scss 파일 생성
.ToDoTemplate{
    width: 512px;
    margin-left: auto;
    margin-right: auto;
    margin-top: 6rem;
    border-radius: 4px;
    overflow: hidden;
    .app.title{
        background: aquamarine;
        color: white;
        height: 4rem;
        font-size: 1.5rem;
        align-items: center;
        justify-content: center;
    }
    .content{
        background: white;     
    }
  • App.js
import logo from './logo.svg';
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';

function App() {
  return (
    <ToDoTemplate>
      
    </ToDoTemplate>
  );
}

export default App;

  • 데이터 추가 컴포넌트 생성
  • ToDoInsert.jsx
import React from 'react';
import {MdAdd} from 'react-icons/md';
import './ToDoInsert.scss';

const ToDoInsert = () => {
    return(
        <div>
            <from className = 'ToDoInsert'>
                <input placeholder='할 일을 입력하세요'/>
                <button type='submit'><MdAdd /></button>
            </from>
        </div>
    )
}

export default ToDoInsert;
  • 추가 화면에 디자인을 적용하기 위한 스타일 설정 생성
  • ToDoInsert.scss
.ToDoInsert{
    display: flex;
    outline:none;
    border:none;
    padding:0.5rem;
    font-size:1.125rem;
    line-height:1.5;
    color:white;
    &::placeholder{
        color:#dee2e6;
    }
    flex:1
}

button{
    background: none;
    outline:none;
    border:none;
    background: #868e96;
    color:white;
    padding-left: 1rem;
    padding-right: 1rem;
    font-size:1.5rem;
    display:flex;
    align-items:center;
    cursor:pointer;
    transition:0.1s background ease-in;
    &:hover{
        background: #adb4bd;
    }
}
  • App.js 에 적용
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';

function App() {
  return (
    <ToDoTemplate>
      <ToDoInsert />
    </ToDoTemplate>
  );
}

export default App;

목록 출력

  • 하나의 목록을 출력하기 위한 컴포넌트 생성
  • ToDoListItem.jsx
  • 왼쪽에 체크 박스(완료 여부를 나타낼 UI)와 입력할 문자열
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'

const ToDoListItem = () => {
    return(
        <div className='ToDoListItem'>
            <div className='checkbox'>
                <MdCheckBoxOutlineBlank />
                <div className='text'>할일</div>
            </div>
            <div className='remove'>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default ToDoListItem;
  • ToDoListItem 에 적용할 스타일 파일을 생성하고 작성
  • ToDoListItem.scss
.ToDoListItem{
    padding:1rem;
    display:flex;
    align-items:center;

    &:nth-child(even){
        background: #f8f9fa;
    }

    .checkbox{
        cursor:pointer;
        flex:1;
        display:flex;
        align-items: center;
        svg{
            font-size: 1.5rem;
        }
        .text{
            margin-left: 0.5rem;
            flex:1;
        }
        &.checked{
            svg{
                color:#22b8cf;
            }
            .text{
                color:#adb5bd;
                text-decoration: line-through;
            }
        }
    }

    .remove{
        display:flex;
        align-items: center;
        font-size: 1.5rem;
        color: #ff6b6b;
        cursor:pointer;
        &:hover{
            color:#ff8787;
        }
    }

    & + &{
        border-top:1px solid #dee2e6;
    }
}
  • ToDoListItem 목록을 출력할 ToDoList.jsx 파일생성
  • ToDoList.jsx
import React from 'react';
import ToDoListItem from './ToDoListItem';
import './ToDoList.scss';

const ToDoList = () => {
    return(
        <div className='ToDoList'>
            <ToDoListItem />
            <ToDoListItem />
            <ToDoListItem />
            <ToDoListItem />
        </div>
    )
}

export default ToDoList;
  • ToDoList 의 스타일을 위한 컴포넌트 생성하여 적용
  • ToDoList.scss
.ToDoList{
    min-height: 320px;
    max-height: 513px;
    overflow-y: auto;
    
}
  • App.js 로 출력
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

function App() {
  return (
    <ToDoTemplate>
      <ToDoInsert />
      <ToDoList />
    </ToDoTemplate>
  );
}

export default App;


State 를 이용한 작업

  • state를 생성해서 샘플 데이터를 전송해서 출력
    • 데이터는 App.js가 소유하거나 Context API, Reducer 등을 이용해서 저장한다.
      • App.js에 샘플 데이터를 생성하고 ToDoList에 전달
  • App.js
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  const [todos, setToDos] = useState([
    {id:1,
    text:'쇼핑',
    checked:false},
    {id:2,
    text:'공부',
    checked:false},
    {id:3,
    text:'야식',
    checked:false},
    {id:4,
    text:'간식',
    checked:false}
  ]);

  //변수를 생성
  const nextId = useRef(5);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      setToDos(todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, [todos]
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove}/>
    </ToDoTemplate>
  );
}

export default App;

  • CollectionView - 여러 개의 동일한 뷰를 모아서 출력

  • CollectionView를 가지고 항목을 출력할 때는 ID 설정이 필요

  • CollectionView 내에서 각 항목을 구분하기 위해서


  • App.js 에서 넘어온 데이터를 이용해서 ToDoList.jsx 출력
import React from 'react';
import ToDoListItem from './ToDoListItem';
import './ToDoList.scss';

const ToDoList = ({todos, onRemove}) => {
    return(
        <div className='ToDoList'>
            {todos.map((todo, index) => (
                <ToDoListItem todo={todo} key={index} onRemove={onRemove}/>
            ))}
        </div>
    )
}

export default ToDoList;
  • ToDoListItem.jsx 출력
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';

const ToDoListItem = ({todo, onRemove}) => {
    const {id, text, checked} = todo;

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem'>
            <div className={cn('checkbox', {checked})}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default ToDoListItem;

  • 데이터 추가 구현

    • App.js에서 데이터를 추가하는 함수를 구현하고 ToDoListItem에서 이벤트와 연결
    • reducer를 사용하지 않는다면 데이터가 있는 곳에 함수를 구현하는 것이 편리하기 때문
  • App.js 에 추가 함수 구현

import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  const [todos, setToDos] = useState([
    {id:1,
    text:'쇼핑',
    checked:false},
    {id:2,
    text:'공부',
    checked:false},
    {id:3,
    text:'야식',
    checked:false},
    {id:4,
    text:'간식',
    checked:false}
  ]);

  //변수를 생성
  const nextId = useRef(5);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      setToDos(todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, [todos]
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove}/>
    </ToDoTemplate>
  );
}

export default App;
  • ToDoInsert.jsx 파일에 데이터 추가 함수를 이벤트와 연결
import React, {useState, useEffect, useCallback} from 'react';
import {MdAdd} from 'react-icons/md';
import './ToDoInsert.scss';

const ToDoInsert = ({onInsert}) => {
    //Input에 입력된 내용을 저장하기 위한 state
    const [value, setValue] = useState('');

    //Input의 내용이 변경될 때 호출될 이벤트 처리 함수
    const onChange = useCallback((e) => {
        setValue(e.target.value);
    }, [])

    //form 안에서 submit 버튼을 누를 때
    const onSubmit = useCallback((e) => {
        onInsert(value);
        setValue('');
        e.preventDefault(); //내장된 이벤트 처리 구문을 수행하지 않음
    }, [onInsert, value]);

    return(
        <div>
            <form className = 'ToDoInsert' onSubmit={onSubmit}>
                <input placeholder='할 일을 입력하세요'
                value={value} onChange={onChange}/>
                <button type='submit'><MdAdd /></button>
            </form>
        </div>
    )
}

export default ToDoInsert;

데이터 삭제 구현

  • ToDoListItem에서 - 아이콘을 누르면 삭제하도록 작성
    • 삭제하기 전에 대화상자를 출력해서 삭제할 것인지 여부를 묻고 삭제
    • App.js 수정
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  const [todos, setToDos] = useState([
    {id:1,
    text:'쇼핑',
    checked:false},
    {id:2,
    text:'공부',
    checked:false},
    {id:3,
    text:'야식',
    checked:false},
    {id:4,
    text:'간식',
    checked:false}
  ]);

  //변수를 생성
  const nextId = useRef(5);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      setToDos(todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, [todos]
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove}/>
    </ToDoTemplate>
  );
}

export default App;
  • ToDoListItem.jsx 수정
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';

const ToDoListItem = ({todo, onRemove}) => {
    const {id, text, checked} = todo;

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem'>
            <div className={cn('checkbox', {checked})}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default ToDoListItem;


수정 구현

  • 체크 박스를 누르면 checked 속성의 값을 반전시키도록 작성
    • 처리할 때 넘겨 받아야 하는 데이터는 id 이다
  • 라우터 등을 이용해서 문자열 까지 수정하는 경우라면 모든 데이터를 전부 넘겨 받아야 한다
  • 배열의 일부 데이터 수정을 할 때도 map 함수를 이용
  • App.js 수정
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  const [todos, setToDos] = useState([
    {id:1,
    text:'쇼핑',
    checked:false},
    {id:2,
    text:'공부',
    checked:false},
    {id:3,
    text:'야식',
    checked:false},
    {id:4,
    text:'간식',
    checked:false}
  ]);

  //변수를 생성
  const nextId = useRef(5);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      setToDos(todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, [todos]
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  //데이터 수정 함수
  const onToggle = useCallback((id) => {
    //todos에서 id가 일치하는 데이터를 찾는데
    //id가 일치하는 데이터를 찾으면 스프레드 연산자를 이용해서 복제한 후
    //checked의 값을 반전시키고 그렇지 않은 경우는 todo를 그대로 사용
    setToDos(todos.map(todo => todo.id === id ?
      {...todo, checked:!todo.checked} : todo))
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;
  • ToDoList 컴포넌트에서 수정을 위한 함수를 받아 ToDoListItem컴포넌트에 넘겨주기
  • ToDoList.jsx
import React from 'react';
import ToDoListItem from './ToDoListItem';
import './ToDoList.scss';

const ToDoList = ({todos, onRemove, onToggle}) => {
    return(
        <div className='ToDoList'>
            {todos.map((todo, index) => (
                <ToDoListItem todo={todo} key={index} onRemove={onRemove}
                onToggle={onToggle}/>
            ))}
        </div>
    )
}

export default ToDoList;
  • ToDoListItem 컴포넌트에 수정 이벤트를 연결
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';

const ToDoListItem = ({todo, onRemove, onToggle}) => {
    const {id, text, checked} = todo;

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem'>
            <div className={cn('checkbox', {checked})}
            onClick={()=>{onToggle(id)}}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default ToDoListItem;


최적화

대량의 데이터 출력

  • 컴포넌트가 리랜더링 되는 경우

  • 전달받은 props가 변경되는 경우

  • 자신의 state가 변경되는 경우

  • 상위 컴포넌트가 리랜더링 되는 경우

  • forceUpdate 함수가 실행되는 경우

  • 특정한 props가 변경되지 않으면 리랜더링 되지 않도록 설정

  • Class Component의 경우는 shouldComponentUpdate 메서드를 이용하고 Function Component는 React.memo를 이용하면 된다.


App.js 를 수정해서 대량의 데이터를 생성

import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  //대량의 데이터를 생ㅅ헝해주는 함수
  const createBulkTodos = () => {
    const array = [];
    for(let i=1; i<2000; i=i+1){
      array.push({
        id:i,
        text:`할 일 ${i}`,
        checked:false
      })
    }
    return array;
  }
  const [todos, setToDos] = useState(createBulkTodos);

  //변수를 생성
  const nextId = useRef(2000);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      setToDos(todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, [todos]
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  //데이터 수정 함수
  const onToggle = useCallback((id) => {
    //todos에서 id가 일치하는 데이터를 찾는데
    //id가 일치하는 데이터를 찾으면 스프레드 연산자를 이용해서 복제한 후
    //checked의 값을 반전시키고 그렇지 않은 경우는 todo를 그대로 사용
    setToDos(todos.map(todo => todo.id === id ?
      {...todo, checked:!todo.checked} : todo))
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;

  • ToDoListItem 컴포넌트가 props가 변경되지 않으면 다시 출력되지 않도록 수정
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';

const ToDoListItem = ({todo, onRemove, onToggle}) => {
    const {id, text, checked} = todo;

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem'>
            <div className={cn('checkbox', {checked})}
            onClick={() => onToggle(id)}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default React.memo(ToDoListItem);

함수가 다시 생성되는 문제

  • useCallback을 이용하지 않고 Component 안에 함수를 만들면 함수는 리랜더링 될 때마다 다시 생성

  • useCallback을 이용하면 특정한 state가 변경될 때 수정되도록 할 수 있다.

  • 이런 문제를 해결하고자 할 때는 useState를 변경할 때 함수형 업데이트를 사용

  • useReducer를 사용 - 컴포넌트 외부에 함수를 구현


App.js 파일에서 삽입을 위한 함수 수정

import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  //대량의 데이터를 생ㅅ헝해주는 함수
  const createBulkTodos = () => {
    const array = [];
    for(let i=1; i<2000; i=i+1){
      array.push({
        id:i,
        text:`할 일 ${i}`,
        checked:false
      })
    }
    return array;
  }
  const [todos, setToDos] = useState(createBulkTodos);

  //변수를 생성
  const nextId = useRef(2000);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      //배열에 데이터를 추가하는 구문
      //setToDos(todos.concat(todo));

      //함수형 업데이트로 구현
      setToDos(todos => todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, []
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos.filter(todo => todo.id !== id));
  }, [todos])

  //데이터 수정 함수
  const onToggle = useCallback((id) => {
    //todos에서 id가 일치하는 데이터를 찾는데
    //id가 일치하는 데이터를 찾으면 스프레드 연산자를 이용해서 복제한 후
    //checked의 값을 반전시키고 그렇지 않은 경우는 todo를 그대로 사용
    setToDos(todos.map(todo => todo.id === id ?
      {...todo, checked:!todo.checked} : todo))
  }, [todos])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;
  • 삭제, 수정 함수에도 적용
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useState, useRef, useCallback} from 'react';

function App() {
  //대량의 데이터를 생ㅅ헝해주는 함수
  const createBulkTodos = () => {
    const array = [];
    for(let i=1; i<2000; i=i+1){
      array.push({
        id:i,
        text:`할 일 ${i}`,
        checked:false
      })
    }
    return array;
  }
  const [todos, setToDos] = useState(createBulkTodos);

  //변수를 생성
  const nextId = useRef(2000);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      //배열에 데이터를 추가하는 구문
      //setToDos(todos.concat(todo));

      //함수형 업데이트로 구현
      setToDos(todos => todos.concat(todo));

      nextId.current = nextId.current + 1;
    }, []
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    setToDos(todos => todos.filter(todo => todo.id !== id));
  }, [])

  //데이터 수정 함수
  const onToggle = useCallback((id) => {
    //todos에서 id가 일치하는 데이터를 찾는데
    //id가 일치하는 데이터를 찾으면 스프레드 연산자를 이용해서 복제한 후
    //checked의 값을 반전시키고 그렇지 않은 경우는 todo를 그대로 사용
    setToDos(todos => todos.map(todo => todo.id === id ?
      {...todo, checked:!todo.checked} : todo))
  }, [])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;

리듀서를 이용한 함수처리의 외부구현

  • 이벤트 처리를 위한 함수를 컴포넌트 외부에 구현하게 되면 컴포넌트에 종속되지 않기 때문에 컴포넌트가 리랜더링되더라도 함수를 다시 만들지 않는다.

  • 리듀서로 동작할 함수는 2개의 매개변수를 갖는다.

    • 첫 번째 매개변수는 수정할 데이터이고 두 번째 데이터는 부가 정보를 저장할 action 객체이다.

    • action 객체의 type 속성을 이용해서 분기문으로 수정할 작업을 만드는데 작업은 데이터를 수정해서 리턴해야 한다.

    • 전달하고자 하는 데이터는 action 객체를 이용해서 전달해야 한다.

    • 리듀서를 만들면 리뷰서를 데이터와 연결한다.

const [데이터이름, 함수이름] = useReducer(리듀서 함수 이름, 초기값, 초기화 함수)

  • App.js 파일을 수정해서 리듀서 사용
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';

import React, {useRef, useCallback,useReducer} from 'react';

//컴포넌트 밖에다 만들것
function todoreducer(todos, action){
  switch(action.type){
    case 'INSERT':
      //나중에 호출 방법{type:'INSERT', todo:{}}
      return todos.concat(action.todo);
    case 'REMOVE':
      // {type:'REMOVE', id:?}
      return todos.filter(todo => todo.id !== action.id)
    case 'TOGGLE':
      // {type:'TOGGLE', id:?}
      return todos.map(todo => todo.id === action.id ? {
        ...todo, checked:!todo.checked} : todo)
    default:
      return todos;
  }
}

function App() {
  //대량의 데이터를 생ㅅ헝해주는 함수
  const createBulkTodos = () => {
    const array = [];
    for(let i=1; i<2000; i=i+1){
      array.push({
        id:i,
        text:`할 일 ${i}`,
        checked:false
      })
    }
    return array;
  }

  // 리듀서 연결 //const [데이터이름, 함수이름] = useReducer(리듀서 함수 이름, 초기값, 초기화 함수) 의 형식을 따른다
  const [todos, dispatch] = useReducer(todoreducer, undefined, createBulkTodos);

  //변수를 생성
  const nextId = useRef(2000);

  //데이터 추가 요청을 처리할 함수
  //useCallback을 사용하면 두 번째 매개변수로 설정한 state가 변경될 때만
  //함수를 다시 생성
  const onInsert = useCallback(
    text => {
      const todo = {
        id:nextId.current,
        text,
        checked:false
      }

      //배열에 데이터를 추가하는 구문
      //setToDos(todos.concat(todo));

      //함수형 업데이트로 구현
      //setToDos(todos => todos.concat(todo));
      ///->
      //리듀서 함수 호출로 변경
      dispatch({type:'INSERT', todo});

      nextId.current = nextId.current + 1;
    }, []
  );

  //데이터 삭제 함수
  const onRemove = useCallback((id) => {
    //setToDos(todos.filter(todo => todo.id !== id));
    ///->
    //리듀서 함수 호출로 변경
    dispatch({type:'REMOVE', id})
  }, [])

  //데이터 수정 함수
  const onToggle = useCallback((id) => {
    //todos에서 id가 일치하는 데이터를 찾는데
    //id가 일치하는 데이터를 찾으면 스프레드 연산자를 이용해서 복제한 후
    //checked의 값을 반전시키고 그렇지 않은 경우는 todo를 그대로 사용
    //setToDos(todos.map(todo => todo.id === id ? {...todo, checked:!todo.checked} : todo))
    ///->
    //리듀서 함수 호출로 변경
    dispatch({type:'TOGGLE',id});

  }, [])

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert}/>
      <ToDoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;


  • ToDoInsert.jsx, ToDoList.jsx, ToDoListItem 파일
    • export 구문을 React.memo 로 감싸주기

react-virtualized 라이브러리

  • 개요

    • 보여지는 데이터만 랜더링 할 수 있는 List 라는 컴포넌트를 가진 라이브러리

    • 데이터가 많을 때 리액트는 모든 데이터를 랜더링 하려고 하는데, 버츄얼라이브러리를 이용하면 이 부분을 방지할 수 있다.

    • 이 라이브러리를 사용하기 위해선느 하나의 셀의 높이 그리고 너비를 알아야 한다

  • 설치

$ npm install react-virtualized
OR
$ yarn add react-virtualized
  • ToDoList.jsx 파일 수정
import React, {useCallback} from 'react';
import {List} from 'react-virtualized';

import ToDoListItem from './ToDoListItem';
import './ToDoList.scss';

const ToDoList = ({todos, onRemove, onToggle}) => {
    //출력을 위한 함수
    const rowRenderer = useCallback(({index, key, style})=>{
        const todo = todos[index];
        return(
            <ToDoListItem todo = {todo} key = {key} onRemove={onRemove}
            onToggle={onToggle} style={style}/>
        );
    }, [todos, onRemove, onToggle])
    return(
        <List
            className='ToDoList'
            width={512}
            height={513}
            rowCount={todos.length}
            rowHeight={57}
            rowRenderer={rowRenderer}
            list={todos}
            style={{outline:'none'}}

        />
    )
}

export default React.memo(ToDoList);
  • ToDoListItem.jsx 파일 수정
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';
//ToDoList 에서 style 이 하나 넘어왔음 
const ToDoListItem = ({todo, onRemove, onToggle, style}) => {
    const {id, text, checked} = todo;

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem-virtualized' style={style}>
            <div className={cn('checkbox', {checked})}
            onClick={()=>{onToggle(id)}}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default React.memo(ToDoListItem);
  • ToDoListItem.scss 의 스타일 수정
.ToDoListItem-virtualized{
    & + &{
        border-top: 1px solid #dee2e6;
    }

    &:nth-child(even){
        background:#f8f9fa;
    }
}

.ToDoListItem{
    padding:1rem;
    display:flex;
    align-items:center;

    &:nth-child(even){
        background: #f8f9fa;
    }

    .checkbox{
        cursor:pointer;
        flex:1;
        display:flex;
        align-items: center;
        svg{
            font-size: 1.5rem;
        }
        .text{
            margin-left: 0.5rem;
            flex:1;
        }
        &.checked{
            svg{
                color:#22b8cf;
            }
            .text{
                color:#adb5bd;
                text-decoration: line-through;
            }
        }
    }

    .remove{
        display:flex;
        align-items: center;
        font-size: 1.5rem;
        color: #ff6b6b;
        cursor:pointer;
        &:hover{
            color:#ff8787;
        }
    }

    & + &{
        border-top:1px solid #dee2e6;
    }
}
  • 실행해서 확인 : 스크롤을 빠르게 하단으로 내리면 데이터가 일시적으로 보이지 않는 현상이 발생할 수 있다
    • react-virtualized 의 List 는 현재 화면에 보여져야 하는 데이터만 출력하기 떄문

Django 와 의 연동

  • 클라이언트와 서버 애플리케이션을 분리해서 구현했을 때 웹 클라이언트에서 서버로부터 데이터를 어떻게 가져올 것인지 여부가 중요함

    • Ajax(fetch api, axios 라이브러리 이용 등)
      • 비동기적으로 데이터를 요청해서 응답을 받으면 연결을 해제
      • 이러한 API 들은 도메인이 다르면 데이터를 가져오지 못함(SOP)
    • Web Socket
      • 연결을 유지
    • Web Push(SEE - Server Sent Events)
      • 서버가 클라이언트에게 일방적으로 데이터를 전송

  • Django 프로젝트 생성
    • 원래 가상환경으로 해야 하는데, 로컬로 찍혀있다
    • 일단 로컬로 실습 진행!!

  • 필요한 패키지 설치
    • django, djangorestframework, mysqlclient
$ pip install django
$ pip install djangorestframework
$ pip install mysqlclient
$ pip install pymysql

  • DB 생성
    • DB 이름 : todo

  • 프로젝트와 애플리케이션 생성
// 프로젝트
$ django-admin startproject TodoAPP
// 애플리케이션
$ python manage.py startapp Todo

  • Settings.py 설정
"""
Django settings for TodoAPP project.

Generated by 'django-admin startproject' using Django 4.1.6.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import pymysql
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-s=xx!%0fbs^l+r)346w*&%qhvh+4jm6g_cee30kwf*&)1p4_sm'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'Todo',
    'rest_framework'

]

MIDDLEWARE = [
    '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',
]

ROOT_URLCONF = 'TodoAPP.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'TodoAPP.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
pymysql.install_as_MySQLdb()
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'todo',
        'USER': 'euijoo',
        'PASSWORD': 'euijoo',
        'HOST': 'localhost',
        'PORT' : '3306'

    }
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'Asia/Seoul'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


  • models. py 수정 후 Migration 진행
from django.db import models

# Create your models here.

class Todo(models.Model):
    text = models.CharField(max_length=100)
    checked = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.text
$ python manage.py makemigrations
$ python manage.py migrate

  • ❗️실무에서는 테이블을 자동으로 만들어주는 기능을 사용하지 않는 경우가 많다❗️

  • 관리자 계정 생성
$ python manage.py createsuperuser
  • 테이블을 관리자가 사용할 수 있도록 등록 - admin.py
from django.contrib import admin

# Register your models here.
from .models import Todo

admin.site.register(Todo)
  • Django 서버를 실행하여 관리자 계정으로 접속
$ python manage.py runserver

  • 샘플 데이터 넣기


전체 데이터 조회

  • django 에서는 API 서버를 만들 때 Serializer 를 이용

    • Model 과 전송하고자 하는 json 문자열 그리고 전송되어 온 파라미터 와 Model 사이의 변환을 담당한다
  • 만드는 방법

    • 주의할점은 필드의 이름이 클라이언트에서 전송하는 파라미터 이름과도 일치해야 한다
    • 파라미터를 직접 읽지 않고 Serializer 를 이용해서 자동으로 변환할 때는 이름에 주의를 해야한다.
class 클래스이름(serializers.ModelSerializer):
	class Meta:
    	model = 변환하고자 하는 Model 클래스 이름
        fields = (변환하고자 하는 필드의 이름을 나열)
  • 앱 안에 serializers.py 파일을 생성하고 작성

from rest_framework import serializers
from .models import Todo

class TodoSimpleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'text', 'checked', 'created')
  • 요청 처리 : view 에 함수 혹은 클래스 형 뷰를 만들어서 처리

    • 함수를 사용하는 경우는 어떤 메서드 방식을 처리할 것인지 기재를 해야하고 클래스 형 뷰를 이용할 때는 APIView를 상속받아서 처리하고자 하는 메서드를 재정의(오버라이딩) 하면 된다.

    • 메서드 - 메서드의 매개변수는 request 클라이언트 요청 객체

      • get
      • post
      • delete
      • put
    • API 서버를 만들 때 응답은 Response 객체로 생성한느데 첫번째 매개변수는 데이터이고 두번째 매개변수는 status로 상태 값이다.

  • views.py 파일에 전체 요청 처리를 수행하는 클래스를 생성

from django.shortcuts import render

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)
  • urls.py 파일에서 url 과 요청 처리 클래스를 연결
from django.contrib import admin
from django.urls import path
from django.contrib import admin
from django.urls import path

from Todo.views import TodosAPIView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('todo', TodosAPIView.as_view())
]
  • get 방식의 테스트는 브라우저에 URL을 직접 입력해서 테스트가 가능


  • 클라이언트에서 서버의 데이터를 사용하기 위해서 axios 라이브러리를 설치
  • 클라이언트의 App.js 수정
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';
import React, {useRef, useCallback,useState, useEffect} from 'react';
import axios from 'axios';


function App() {
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);
  //데이터를 저장하는 배열은 null을 가지면 안됨
  //배열은 비어있는 상태로 초기화
  const [todos, setTodos] = useState([]);

  //데이터를 가져오는 함수
  const fetchData = async() =>{
    try{
      // 데이터 요청
      const response = await axios.get('http://localhost:8000/todo');
      // 데이터를 가져오면 todos 에 설정
      setTodos(response.data);
    }catch(e){
      setError(e);
    }
    setLoading(false);
  }

  // 컴포넌트가 랜더링 될 때 1번만 수행
  useEffect(()=>{
    fetchData();
  },[])

  if(loading) return <div>로딩...!</div>
  if(error) return <div>에러....발생</div>
  if(!todos) return null;

  return (
    <ToDoTemplate>
      <ToDoInsert />
      <ToDoList todos={todos}/>
    </ToDoTemplate>
  );
}

export default App;

❗️❗️에러 발생❗️❗️

  • 데이터가 출력되지 않고 콘솔에 에러가 발생함
  • 에러 메시지
Access to XMLHttpRequest at 
'http://localhost:8000/todo' from origin 'http://localhost:3000'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' 
header is present on the requested resource.
  • SOP(Same Origin Policy - 동일 출처 정책)

    • 브라우저는 동일한 출처에 대해서만 데이터 요청이 가능하도록 설정이되어 있다.
    • SOP 에 해당하는 경우는 ajax, fetch api 만 해당함
    • 동일 출처 정책을 판단하는 기준은 포트번호 까지 일치하면 동일 출처로 판단
  • 해결 방법

    • 서버 애플리케이션에서 CORS 설정을 추가해주는 방법

      • 허용할 도메인을 등록
    • 클라이언트 애플리케이션에서 Proxy (내부망에서 외부망으로 요청) 를 이용해서 서버에 데이터를 요청하는 방법

      • 자바스크립트로는 구현 불가능하다고 함!!
      • 별도의 라이브러리를 이용하거나 java 와 같은 프로그래밍 언어의 도움을 받아야함
      • 브라우저를 통하지 않고 데이터를 요청해서 가져온 후 클라이언트에게는 ajax 요청에 대한 응답인 것처럼 구현
    • ✔️ 리액트에서는 package.json 파일에 proxy 설정을 하면 proxy 설정이 가능

Django 에서 CORS 설정

  • 패키지 설치
$ pip install django-cors-headers
  • settings.py 파일 수정

  • 앱 추가

  • 미들웨어 추가

  • CORS 허용 도메인 등록

  • 클라이언트 애플리케이션을 실행시켜 확인

✅ 오류 해결


  • 관리자 계정으로 접속해서 todo 추가하고 클라이언트 애플리케이션에서 확인


데이터 삽입

  • 클라이언트에서 post 방식으로 데이터를 전송해서 처리

  • 클라이언트에서 서버에게 필수 데이터를 정확하게 전송을 해야 한다

    • POST or PUT 방식의 파라미터를 전송할 때는 FormData 를 이용하는 것이 편리함
  • 서버에서 삽입을 처리한 후 성공 여부를 알려주고 클라이언트가 서버에게 데이터를 다시 요청해도 되고 서버가 처리한 후 전체 데이터를 다시 전송해도 된다.

  • 서버 프로젝트의 views.py에 POST 방식 추가

from django.shortcuts import render

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)

    # 데이터 삽입
    def post(self, request):
        # POST 방식으로 전송된 데이터 읽기
        text = request.POST['text']
        checked = request.POST['checked']

        # 삽입할 모델 생성
        todo = Todo()
        todo.text = text
        todo.checked = checked

        # 모델 저장 - 데이터 삽입
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status = status.HTTP_200_OK)
  • POST 방식의 요청은 브라우저에서 테스트 불가
  • API 서버를 만들 때는 API 서버 테스트 도구를 이용
    • POST Man 사용
      • python 에서 bool 은 True 와 False

  • 데이터 삽입을 수행할 함수를 추가 하기 위해 App.js 수정
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';
import React, {useRef, useCallback,useState, useEffect} from 'react';
import axios from 'axios';


function App() {
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);
  //데이터를 저장하는 배열은 null을 가지면 안됨
  //배열은 비어있는 상태로 초기화
  const [todos, setTodos] = useState([]);

  //데이터를 가져오는 함수
  const fetchData = async() =>{
    try{
      // 데이터 요청
      const response = await axios.get('http://localhost:8000/todo');
      // 데이터를 가져오면 todos 에 설정
      setTodos(response.data);
    }catch(e){
      setError(e);
    }
    setLoading(false);
  }

  // 컴포넌트가 랜더링 될 때 1번만 수행
  useEffect(()=>{
    fetchData();
  },[])

  // 삽입을 처리할 함수
  // FormData 데이터를 매개변수로 받고 checked 의 값은 False 로 추가
  const onInsert = useCallback((formData)=>{
    //FormData 에 데이터 추가
    formData.append("checked","False");
    //ajax 객체 생성
    let request = new XMLHttpRequest();
    // 요청 방식 과 URL 과 비동기 여부를 결정
    request.open('POST', 'http://localhost:8000/todo', true);
    // 파라미터와 함께 전송
    request.send(formData);
    // 응답이 온경우 처리
    request.addEventListener('load', function(){
      //axios 는 응답 객체의 data 하게 되면 파싱된 결과가 오지만
      // ajax 객체는 직접 파싱을 해줘야함
      setTodos(JSON.parse(request.responseText));
    })
  },[])

  if(loading) return <div>로딩...!</div>
  if(error) return <div>에러....발생</div>
  if(!todos) return null;

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert} />
      <ToDoList todos={todos}/>
    </ToDoTemplate>
  );
}

export default App;
  • 클라이언트의 ToDoInsert.jsx 파일을 수정
import React, {useState, useEffect, useCallback} from 'react';
import {MdAdd} from 'react-icons/md';
import './ToDoInsert.scss';

const ToDoInsert = ({onInsert}) => {
    //Input에 입력된 내용을 저장하기 위한 state
    const [value, setValue] = useState('');

    //Input의 내용이 변경될 때 호출될 이벤트 처리 함수
    const onChange = useCallback((e) => {
        setValue(e.target.value);
    }, [])

    //form 안에서 submit 버튼을 누를 때
    const onSubmit = useCallback((e) => {
        //form 안에 입력된 데이터를 가지고 FormData 객체 생성
        let formData = new FormData(
            document.getElementById('form'));
        onInsert(formData);

        setValue('');
        e.preventDefault(); //내장된 이벤트 처리 구문을 수행하지 않음
    }, [onInsert]);

    return(
        <div>
            <form className = 'ToDoInsert' onSubmit={onSubmit}
            id='form'>
                <input placeholder='할 일을 입력하세요'
                value={value} onChange={onChange} name='text'/>
                <button type='submit'><MdAdd /></button>
            </form>
        </div>
    )
}

export default React.memo( ToDoInsert);


데이터 수정

  • 처리하는 방식은 삽입과 거의 유사하고 전송 방식은 PUT을 사용

  • PUT과 PATCH의 차이

    • PUT은 데이터의 모든 항목을 수정하는 것이고 PATCH라는 데이터의 일부분만 수정한다.

    • PATCH는 멱등성이 없어서 사용하지 않는 것을 권장

      • 멱등성 : 동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 갖는 것
    • 응답하는 코드는 달라도 되지만 서버의 상태가 동일해야 한다.

    • POST는 멱등성을 가지지 않는다.

    • DELETE는 멱등성을 갖도록 만들어야 한다고 하는데 가장 마지막 데이터를 지우는 형태는 멱등성을 가지지 않으므로 회피하는 것이 좋다.

  • 멱등성을 갖도록 하기

    • 한다면 예를들어 1번 데이터를 지우라고 한다면 처음 요청을 하면 1번 데이터를 지우고 200을 리턴할 것이고, 1번 데이터를 지우라고 한다면 1번 데이터가 없어서 404 응답을 하겠지만 서버의 상태는 동일하다

    • 멱등성을 갖도록 하려고 템플릿 엔진을 이용하는 경우 삽입, 삭제, 수정의 경우는 새로 고침을 해도 요청을 다시 보내지 않도록 redirect를 해야한다

  • 서버의 views.py 파일에 수정을 위한 함수를 추가

from django.shortcuts import render

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)

    # 데이터 삽입
    def post(self, request):
        # POST 방식으로 전송된 데이터 읽기
        text = request.POST['text']
        checked = request.POST['checked']

        # 삽입할 모델 생성
        todo = Todo()
        todo.text = text
        todo.checked = checked

        # 모델 저장 - 데이터 삽입
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
		# 데이터 수정
    def put(self,request):
        id = request.PUT['id']

        todo = Todo()
        todo.id = id
        todo.checked = not todo.checked
        # save 는 upsert 의 역할
        # 기본키의 값이 존재하면
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
  • 클라이언트의 App.js를 수정 - 수정하는 함수 추가
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';
import React, {useRef, useCallback,useState, useEffect} from 'react';
import axios from 'axios';


function App() {
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);
  //데이터를 저장하는 배열은 null을 가지면 안됨
  //배열은 비어있는 상태로 초기화
  const [todos, setTodos] = useState([]);

  //데이터를 가져오는 함수
  const fetchData = async() =>{
    try{
      // 데이터 요청
      const response = await axios.get('http://localhost:8000/todo');
      // 데이터를 가져오면 todos 에 설정
      setTodos(response.data);
    }catch(e){
      setError(e);
    }
    setLoading(false);
  }

  // 컴포넌트가 랜더링 될 때 1번만 수행
  useEffect(()=>{
    fetchData();
  },[])

  // 삽입을 처리할 함수
  // FormData 데이터를 매개변수로 받고 checked 의 값은 False 로 추가
  const onInsert = useCallback((formData)=>{
    //FormData 에 데이터 추가
    formData.append("checked","False");
    //ajax 객체 생성
    let request = new XMLHttpRequest();
    // 요청 방식 과 URL 과 비동기 여부를 결정
    request.open('POST', 'http://localhost:8000/todo', true);
    // 파라미터와 함께 전송
    request.send(formData);
    // 응답이 온경우 처리
    request.addEventListener('load', function(){
      //axios 는 응답 객체의 data 하게 되면 파싱된 결과가 오지만
      // ajax 객체는 직접 파싱을 해줘야함
      setTodos(JSON.parse(request.responseText));
    })
  },[])

  // 수정을 위한 함수
  const onToggle = useCallback((formData)=>{
    let request = new XMLHttpRequest();
    request.open('PUT', 'http://localhost:8000/todo', true);
    request.send(formData);
    request.addEventListener('load', function(){
      //axios 는 응답 객체의 data 하게 되면 파싱된 결과가 오지만
      // ajax 객체는 직접 파싱을 해줘야함
      setTodos(JSON.parse(request.responseText));
    })
  },[])

  if(loading) return <div>로딩...!</div>
  if(error) return <div>에러....발생</div>
  if(!todos) return null;

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert} />
      <ToDoList todos={todos} onToggle={onToggle}/>
    </ToDoTemplate>
  );
}

export default App;
  • ToDoListItem 컴포넌트를 수정해서 수정을 구현
  • 체크박스 클릭시 업데이트 추가
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';
//ToDoList 에서 style 이 하나 넘어왔음 
const ToDoListItem = ({todo, onRemove, onToggle, style}) => {
    const {id, text, checked} = todo;
    // 수정을 처리하는 함수
    const onUpdate = useCallback((id)=>{
        let formData = new FormData();
        formData.append('id',id);
        onToggle(formData);
    },[onToggle])

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((e) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove, id, text]);

    return(
        <div className='ToDoListItem-virtualized' style={style}>
            <div className={cn('checkbox', {checked})}
            onClick={()=>onUpdate(id)}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={onDelete}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default React.memo(ToDoListItem);
  • 서버 확인

❗️❗️500 에러 발생❗️❗️

  • views.py 수정
from django.shortcuts import render, get_object_or_404

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)

    # 데이터 삽입
    def post(self, request):
        # POST 방식으로 전송된 데이터 읽기
        text = request.POST['text']
        checked = request.POST['checked']

        # 삽입할 모델 생성
        todo = Todo()
        todo.text = text
        todo.checked = checked

        # 모델 저장 - 데이터 삽입
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request):
        # 요청은 Put 이지만 파라미터를 읽을 때는 POST 로 읽어야 한다
        id = request.POST['id']
        print(id)
        
        todo = Todo()
        todo.id = id
        todo.checked = not todo.checked
        # save 는 upsert 의 역할
        # 기본키의 값이 존재하면
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
  • 그래도 오류를 뿜음

  • 다시 수정
from django.shortcuts import render, get_object_or_404

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)

    # 데이터 삽입
    def post(self, request):
        # POST 방식으로 전송된 데이터 읽기
        text = request.POST['text']
        checked = request.POST['checked']

        # 삽입할 모델 생성
        todo = Todo()
        todo.text = text
        todo.checked = checked

        # 모델 저장 - 데이터 삽입
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request):
        # 요청은 Put 이지만 파라미터를 읽을 때는 POST 로 읽어야 한다
        id = request.POST['id']
        print(id)
        # 수정을 할 때는 데이터를 먼저 찾아오고 찾아온 데이터 안에서 수정해야함
        # 모델을 직접 생성하면 필수 컬럼의 값이 없어서 에러가 발생할 수 있음
        todo = get_object_or_404(Todo, id=id)
        todo.checked = not todo.checked
        
        # save 는 upsert 의 역할
        # 기본키의 값이 존재하면
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)


데이터 삭제

  • 기본키를 파라미터로 전송해서 처리
    • 삭제는 GET 방식과 동일하게 동작
  • 서버 프로젝트의 views.py 파일에 삭제를 위한 메서드를 추가
from django.shortcuts import render, get_object_or_404

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Todo
from .serializers import TodoSimpleSerializer

class TodosAPIView(APIView):
    # get 방식의 요청을 처리해주는 메서드
    def get(self, request):
        # 테이블의 전체 데이터 요청
        todos = Todo.objects.all()
        # JSON 문자열로 변한
        serializer = TodoSimpleSerializer(todos, many=True)
        # 응답 생성해서 리턴
        return Response(serializer.data, status=status.HTTP_200_OK)

    # 데이터 삽입
    def post(self, request):
        # POST 방식으로 전송된 데이터 읽기
        text = request.POST['text']
        checked = request.POST['checked']

        # 삽입할 모델 생성
        todo = Todo()
        todo.text = text
        todo.checked = checked

        # 모델 저장 - 데이터 삽입
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request):
        # 요청은 Put 이지만 파라미터를 읽을 때는 POST 로 읽어야 한다
        id = request.POST['id']
        print(id)
        # 수정을 할 때는 데이터를 먼저 찾아오고 찾아온 데이터 안에서 수정해야함
        # 모델을 직접 생성하면 필수 컬럼의 값이 없어서 에러가 발생할 수 있음
        todo = get_object_or_404(Todo, id=id)
        todo.checked = not todo.checked

        # save 는 upsert 의 역할
        # 기본키의 값이 존재하면
        todo.save()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def delete(self, request):
        #파라미터 읽어오기
        id = request.GET['id']

        print(id)

        todo = get_object_or_404(Todo, id=id)
        todo.delete()

        # 전체 데이터를 조회해서 리턴
        todos = Todo.objects.all()
        serializer = TodoSimpleSerializer(todos, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
  • 클라이언트 프로젝트의 App.js 를 수정
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
import ToDoList from './components/ToDoList';
import React, {useRef, useCallback, useState, useEffect} from 'react';
import axios from 'axios';


function App() {
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);
  //데이터를 저장하는 배열은 null을 가지면 안됨
  //배열은 비어있는 상태로 초기화
  const [todos, setTodos] = useState([]);

  //데이터를 가져오는 함수
  const fetchData = async() =>{
    try{
      // 데이터 요청
      const response = await axios.get('http://localhost:8000/todo');
      // 데이터를 가져오면 todos 에 설정
      setTodos(response.data);
    }catch(e){
      setError(e);
    }
    setLoading(false);
  }

  // 컴포넌트가 랜더링 될 때 1번만 수행
  useEffect(()=>{
    fetchData();
  },[])

  // 삽입을 처리할 함수
  // FormData 데이터를 매개변수로 받고 checked 의 값은 False 로 추가
  const onInsert = useCallback((formData)=>{
    //FormData 에 데이터 추가
    formData.append("checked","False");
    //ajax 객체 생성
    let request = new XMLHttpRequest();
    // 요청 방식 과 URL 과 비동기 여부를 결정
    request.open('POST', 'http://localhost:8000/todo', true);
    // 파라미터와 함께 전송
    request.send(formData);
    // 응답이 온경우 처리
    request.addEventListener('load', function(){
      //axios 는 응답 객체의 data 하게 되면 파싱된 결과가 오지만
      // ajax 객체는 직접 파싱을 해줘야함
      setTodos(JSON.parse(request.responseText));
    })
  },[])

  // 수정을 위한 함수
  const onToggle = useCallback((formData)=>{
    let request = new XMLHttpRequest();
    request.open('PUT', 'http://localhost:8000/todo', true);
    request.send(formData);
    request.addEventListener('load', function(){
      //axios 는 응답 객체의 data 하게 되면 파싱된 결과가 오지만
      // ajax 객체는 직접 파싱을 해줘야함
      setTodos(JSON.parse(request.responseText));
    })
  },[])

  //삭제를 위한 메서드
  const onRemove = useCallback((id)=>{
    let request = new XMLHttpRequest();
    request.open('DELETE', 'http://localhost:8000/todo?id=' + id, true);
    request.send('');
    request.addEventListener('load', function(){
      setTodos(JSON.parse(request.responseText));
    })

  },[])

  if(loading) return <div>로딩...!</div>
  if(error) return <div>에러....발생</div>
  if(!todos) return null;

  return (
    <ToDoTemplate>
      <ToDoInsert onInsert={onInsert} />
      <ToDoList todos={todos} onToggle={onToggle} onRemove={onRemove}/>
    </ToDoTemplate>
  );
}

export default App;
  • 클라이언트 프로젝트의 ToDoListItem 컴포넌트에서 삭제 구현
import React from 'react';

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline
} from 'react-icons/md';

import './ToDoListItem.scss'
import cn from 'classnames';
import { useCallback } from 'react';
//ToDoList 에서 style 이 하나 넘어왔음 
const ToDoListItem = ({todo, onRemove, onToggle, style}) => {
    const {id, text, checked} = todo;
    // 수정을 처리하는 함수
    const onUpdate = useCallback((id)=>{
        let formData = new FormData();
        formData.append('id',id);
        onToggle(formData);
    },[onToggle])

    //삭제 이벤트 처리 함수
    const onDelete = useCallback((id) => {
        //자바스크립트에서는 window 객체의 멤버를 window.을 생락하고 호출이 가능
        //react 프로젝트에서는 window 객체의 멤버를 호출할 때
        //중복되는 이름이 있을 수 있어서 windw.을 추가해야 하는 경우가 있음
        const result = window.confirm(text + '를 정말로 삭제');
        if(result){
            onRemove(id);
        }
    }, [onRemove,text]);

    return(
        <div className='ToDoListItem-virtualized' style={style}>
            <div className={cn('checkbox', {checked})}
            onClick={()=>onUpdate(id)}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove' onClick={()=>onDelete(id)}>
                <MdRemoveCircleOutline />
            </div>
        </div>
    )
}

export default React.memo(ToDoListItem);

  • 삭제됨


데이터 정렬

  • 서버측 에서 정렬
    todos = Todo.objects.all().order_by('-id').values()
  • 클라이언트측 에서 정렬 - App.js
    • 배열의 reverse() 함수를 호출하면 반대로 만들어줌
    • sort 사용
//데이터를 가져오는 함수
  const fetchData = async() =>{
    try{
      // 데이터 요청
      const response = await axios.get('http://localhost:8000/todo');
      // 서버로부터 데이터를 받아와서 저장
      let ar = response.data;
      // 배열의 데이터를 id 의 내림차순으로 정렬
      // 오름차순은 a와 b의 순서를 변경하면 되고 숫자는 뺄셈의 결과 리턴
      // 문자열의 크기 비교가 가능하므로 앞의 데이터가 크면 양수
      // 같으면 0 작으면 음수를 리턴하도록 만들어주면 된다
      ar.sort((a,b)=>{return b.id - a.id})
      // 데이터를 가져오면 todos 에 설정
      setTodos(ar);
    }catch(e){
      setError(e);
    }
    setLoading(false);
  }

데이터의 CRUD 작업 후 업데이트

  • 삽입, 삭제, 갱신 작업 후 전체 데이터를 전부 가져온다.

  • ToDo처럼 하나의 애플리케이션에서만 사용하는 데이터나 갱신 작업이 자주 발생하지 않는 경우라면 이 경우는 데이터 전체를 다시 가져오는 것보다 현재 로컬에 저장된 데이터에 편집하는 것도 고려할만 하다.

profile
무럭무럭 자라볼까

0개의 댓글