$ npm install sass-loader sass node-sass
or
$ yarn add sass-loader sass node-sass
body {
margin: 0;
padding: 0;
background: burlywood;
}
div{
width:300px;
}
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div>
할일 목록 애플리케이션
</div>
);
}
export default App;
ToDoTemplate.jsx : 메인 화면
ToDoListitem : 하나의 항목 출력
ToDoList : 데이터 전체 출력
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;
.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;
}
import logo from './logo.svg';
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
function App() {
return (
<ToDoTemplate>
</ToDoTemplate>
);
}
export default App;
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{
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;
}
}
import './App.css';
import ToDoTemplate from './components/ToDoTemplate';
import ToDoInsert from './components/ToDoInsert';
function App() {
return (
<ToDoTemplate>
<ToDoInsert />
</ToDoTemplate>
);
}
export default App;
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{
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;
}
}
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{
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
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;
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 내에서 각 항목을 구분하기 위해서
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;
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 에 추가 함수 구현
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;
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;
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;
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;
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;
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;
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를 이용하면 된다.
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;
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를 사용 - 컴포넌트 외부에 함수를 구현
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(리듀서 함수 이름, 초기값, 초기화 함수)
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;
개요
보여지는 데이터만 랜더링 할 수 있는 List 라는 컴포넌트를 가진 라이브러리
데이터가 많을 때 리액트는 모든 데이터를 랜더링 하려고 하는데, 버츄얼라이브러리를 이용하면 이 부분을 방지할 수 있다.
이 라이브러리를 사용하기 위해선느 하나의 셀의 높이 그리고 너비를 알아야 한다
설치
$ npm install react-virtualized
OR
$ yarn add react-virtualized
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);
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-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;
}
}
클라이언트와 서버 애플리케이션을 분리해서 구현했을 때 웹 클라이언트에서 서버로부터 데이터를 어떻게 가져올 것인지 여부가 중요함
$ pip install django
$ pip install djangorestframework
$ pip install mysqlclient
$ pip install pymysql
// 프로젝트
$ django-admin startproject TodoAPP
// 애플리케이션
$ python manage.py startapp Todo
"""
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'
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
from django.contrib import admin
# Register your models here.
from .models import Todo
admin.site.register(Todo)
$ python manage.py runserver
django 에서는 API 서버를 만들 때 Serializer 를 이용
만드는 방법
class 클래스이름(serializers.ModelSerializer):
class Meta:
model = 변환하고자 하는 Model 클래스 이름
fields = (변환하고자 하는 필드의 이름을 나열)
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 클라이언트 요청 객체
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)
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())
]
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 - 동일 출처 정책)
해결 방법
서버 애플리케이션에서 CORS 설정을 추가해주는 방법
클라이언트 애플리케이션에서 Proxy (내부망에서 외부망으로 요청) 를 이용해서 서버에 데이터를 요청하는 방법
✔️ 리액트에서는 package.json 파일에 proxy 설정을 하면 proxy 설정이 가능
$ pip install django-cors-headers
settings.py 파일 수정
앱 추가
✅ 오류 해결
클라이언트에서 post 방식으로 데이터를 전송해서 처리
클라이언트에서 서버에게 필수 데이터를 정확하게 전송을 해야 한다
서버에서 삽입을 처리한 후 성공 여부를 알려주고 클라이언트가 서버에게 데이터를 다시 요청해도 되고 서버가 처리한 후 전체 데이터를 다시 전송해도 된다.
서버 프로젝트의 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)
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;
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)
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;
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);
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)
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)
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;
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()
//데이터를 가져오는 함수
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);
}
삽입, 삭제, 갱신 작업 후 전체 데이터를 전부 가져온다.
ToDo처럼 하나의 애플리케이션에서만 사용하는 데이터나 갱신 작업이 자주 발생하지 않는 경우라면 이 경우는 데이터 전체를 다시 가져오는 것보다 현재 로컬에 저장된 데이터에 편집하는 것도 고려할만 하다.