이번 목표
- Axios 라이브러리를 이용한 서버와의 통신
- useEffect() 를 활용한 비동기 처리와 상태 변경
- 커스텀 훅을 이용한 공통 코드 재사용하기
- 컴포넌트에서 모달창을 이용해서 결과 보여주기
4.1 Ajax 통신 처리
- Axios 라이브러리를 추가해 준다.
- npm install axios
- Axios 의 경우 기본적으로 데이터 형식이 JSON 을 사용한다.
todoApi.js
import axios from "axios";
export const API_SERVER_HOST = 'http://localhost:8080';
const prefix = `${API_SERVER_HOST}/api/todo`;
export const getOne = async(tno) =>{
const res = await axios.get(`${prefix}/${tno}`);
return res.data;
}
export const getList = async (pageParam) =>{
const {page, size} = pageParam;
const res = await axios.get(`${prefix}/list`,{params: {page:page, size:size}});
return res.data;
}
4.2 useEffect()
- 비동기 처리는 함수를 호출하고 호출 결과를 기다려서 다음 처리가 진행되는 방식이 아니고 나중에 결과를 처리하는 방식임
- 리액트의 경우는 컴포넌트에서 비동기 방식으로 호출했다면 호출 결과를 처리한 후에 상태를 변경해서 처리
- 문제점 : 컴포넌트의 상태가 변경되었기 때문에 컴포넌트는 다시 렌더링을 호출
- 다시 랜더링이 되면서 비동기 호출을 하게 되고 잠시 후 다시 상태가 갱신되는 악순한이 계속 반복됨!!
- 리액트에서는 useEffect() 로 컴포넌트 내에 특정한 상황을 만족하는 경우에만 특정한 동작을 수행하는 방법을 제공함
- ex) 컴포넌트 실행 과정에서 한 번만 실행해야 하는 비동기 철
- ex) 컴포넌트의 여러 상태 중에서 특정한 상태만 변경되었을 경우에 비동기 처리
조회를 위한 컴포넌트
- pages 폴더는 주로 URL 처리를 위한 컴포넌트이고, 실제 작업은 components 폴더를 이용해서 구성
- useEffect() 를 사용해서 번호가 변경 되었을 때만 Axios를 이용하는 getOne() 을 호출하도록 구성됨
ReadComponent.js
import React, { useEffect, useState } from 'react';
import { getOne } from '../../api/todoApi';
const initState = {
tno:0,
title:'',
writer:'',
dueDate:null,
complete:false
}
const ReadComponent = ({tno}) => {
const [todo, setTodo] = useState(initState);
useEffect(()=>{
getOne(tno).then(data =>{
console.log(data);
setTodo(data);
})
}, [tno])
return (
<div>
</div>
);
};
export default ReadComponent;
ReadPage.js
import React, { useCallback } from 'react';
import { createSearchParams, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import ReadComponent from '../../components/todo/ReadComponent';
const ReadPage = () => {
const { tno } = useParams();
const navigate = useNavigate();
const [queryParams] = useSearchParams();
const page = queryParams.get("page") ? parseInt(queryParams.get("page")) : 1;
const size = queryParams.get("size") ? parseInt(queryParams.get("size")) : 10;
const queryStr = createSearchParams({ page, size }).toString();
return (
<div className='font-extrabold w-full bg-white mt-6'>
Todo Read Page Component {tno}
<div className='text-2xl'>
Todo Read Page Component {tno}
</div>
<ReadComponent tno={tno}></ReadComponent>
</div>
);
};
export default ReadPage;
<StrictMode> 설정
- 아래 그림을 보면 Axios 호출이 두번 된것을 확인할 수 있다 (33 두번 나옴)
- 비동기 호출은 두번 호출되는 경우가 발생한다.
- index.js 에서 strictMode 를 빼주면 해결 !!
변경전
<React.StrictMode>
<App />
</React.StrictMode>
변경 후
root.render(
<App />
);
ReadComponent.js
import React, { useEffect, useState } from 'react';
import { getOne } from '../../api/todoApi';
const initState = {
tno:0,
title:'',
writer:'',
dueDate:null,
complete:false
}
const ReadComponent = ({tno}) => {
const [todo, setTodo] = useState(initState);
useEffect(()=>{
getOne(tno).then(data =>{
console.log(data);
setTodo(data);
})
}, [tno])
return (
<div className='torder-2 border-sky-200 mt-10 m-2 p-4'>
{makeDiv('Tno', todo.tno)}
{makeDiv('Writer', todo.writer)}
{makeDiv('Title', todo.title)}
{makeDiv('Due Date', todo.dueDate)}
{makeDiv('Complete', todo.complete ? 'Completed' : 'Not Yet')}
</div>
);
};
const makeDiv = (title, value)=>{
return (
<div className='flex justify-center'>
<div className='relative mb-4 flex w-full flex-warp items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
{title}
</div>
<div className='w-4/5 p-6 rounded-r border border-solid shadow-md'>
{value}
</div>
</div>
</div>
);
}
export default ReadComponent;
화면
4.3 네비게이션 관련 커스텀 훅
- 컴포넌트들 내부에서 만들어지는 공통적인 코드의 경우 커스텀 훅을 이용해서 작성함
- 커스텀 훅은 반드시 use 로 시작해야됨!!!
목록 페이지로 이동
useCustomMove.js
import { createSearchParams, useNavigate, useSearchParams } from 'react-router-dom';
const getNum = (param, defaultValue)=>{
if(!param){
return defaultValue;
}
return parseInt(param);
}
const useCustomMove = () => {
const navigate = useNavigate();
const[queryParams] = useSearchParams();
const page = getNum(queryParams.get('page'),1);
const size = getNum(queryParams.get('size'),10);
const queryDefault = createSearchParams({page, size}).toString();
const moveToList = (pageParam) =>{
let queryStr = "";
if(pageParam){
const pageNum = getNum(pageParam.page,1);
const sizeNum = getNum(pageParam.size, 10);
queryStr = createSearchParams({page: pageNum, size: sizeNum}).toString();
}else{
queryStr = queryDefault;
}
navigate({pathname: `../list`, search:queryStr});
}
return {moveToList, page, size}
};
export default useCustomMove;
ReadComponent.js
import React, { useEffect, useState } from 'react';
import { getOne } from '../../api/todoApi';
import useCustomMove from '../../hooks/useCustomMove';
const initState = {
tno:0,
title:'',
writer:'',
dueDate:null,
complete:false
}
const ReadComponent = ({tno}) => {
const [todo, setTodo] = useState(initState);
const {moveToList} = useCustomMove();
useEffect(()=>{
getOne(tno).then(data =>{
console.log(data);
setTodo(data);
})
}, [tno])
return (
<div className='border-2 border-sky-200 mt-10 m-2 p-4'>
{makeDiv('Tno', todo.tno)}
{makeDiv('Writer', todo.writer)}
{makeDiv('Title', todo.title)}
{makeDiv('Due Date', todo.dueDate)}
{makeDiv('Complete', todo.complete ? 'Completed' : 'Not Yet')}
<div className='flex justofy-end p-4'>
<button type='button'
className='rounded p-4 text-xl w-32 text-white bg-blue-500'
onClick={()=>moveToList()}>
List
</button>
</div>
</div>
);
};
const makeDiv = (title, value)=>{
return (
<div className='flex justify-center'>
<div className='relative mb-4 flex w-full flex-warp items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
{title}
</div>
<div className='w-4/5 p-6 rounded-r border border-solid shadow-md'>
{value}
</div>
</div>
</div>
);
}
export default ReadComponent;
수정/삭제 페이지로 이동
useCustomMove.js
const moveToModify=(num)=>{
console.log(queryDefault);
navigate({pathname:`../modify/${num}`, search: queryDefault})
}
ReadComponent.js
<button type='button'
className='rounded p-4 m-2 text-xl w-32 text-white bg-blue-500'
onClick={()=>moveToModify(tno)}>
Modify
</button>
4.4 목록 데이터 처리
목록 데이터 가져오기
ListComponent.js
import React, { useEffect, useState } from 'react';
import useCustomMove from '../../hooks/useCustomMove';
import { getList } from '../../api/todoApi';
const initState ={
dtoList:[],
pageNumList:[],
pageRequestDTO:null,
prev:false,
next:false,
totalCount:0,
prevPage:0,
nextPage:0,
totalPage:0,
current:0
}
const ListComponent = () => {
const {page, size} = useCustomMove();
const [serverData, setServerData] = useState(initState);
useEffect(()=>{
getList({page,size}).then(data=>{
console.log(data);
setServerData(data);
})
},[page,size])
return (
<div>
Todo List Component
</div>
);
};
export default ListComponent;
ListComponent.js
import React, { useEffect, useState } from 'react';
import useCustomMove from '../../hooks/useCustomMove';
import { getList } from '../../api/todoApi';
const initState ={
dtoList:[],
pageNumList:[],
pageRequestDTO:null,
prev:false,
next:false,
totalCount:0,
prevPage:0,
nextPage:0,
totalPage:0,
current:0
}
const ListComponent = () => {
const {page, size} = useCustomMove();
const [serverData, setServerData] = useState(initState);
useEffect(()=>{
getList({page,size}).then(data=>{
console.log(data);
setServerData(data);
})
},[page,size])
return (
<div className='border-2 border-blue-100 mt-10 mr-2 ml-2'>
<div className='flex flex-wrap mx-auto justify-center p-6'>
{serverData.dtoList.map(todo => {
return(
<div key={todo.tno} className='w-full min-w-[400px] p-2 m-2 rounded shadow-md'>
<div className='flex'>
<div className='font-extrabold text-2xl p-2 w-1/12'>
{todo.tno}
</div>
<div className='font-extrabold text-1xl m-1 p-2 w-8/12'>
{todo.title}
</div>
<div className='font-medium text-1xl m-1 p-2 w-2/10'>
{todo.dueDate}
</div>
</div>
</div>
)
})}
</div>
</div>
);
};
export default ListComponent;
페이징 처리
PageComponent.js
const PageComponent = ({serverData, movePage})=>{
return(
<div className="m-6 flex justify-center">
{serverData.prev ?
<div className="m-2 p-2 w-16 text-center font-bold text-blue-400"
onClick={()=>movePage({page:serverData.prevPage})}>
Prev
</div> : <></>}
{serverData.pageNumList.map(pageNum =>
<div key={pageNum}
className={`m-2 p-2 w-12 text-center rounded shadow-md text-white${serverData.current === pageNum ?
'bg-gray-500' : 'bg-blue-400'}`}
onClick={()=>movePage({page:pageNum})}>
{pageNum}
</div>)}
{serverData.next ?
<div className="m-2 p-2 w-16 text-center font-bold text-blue=400"
onClick={()=>movePage({page:serverData.nextPage})}>
Next
</div> :<></> }
</div>
)
}
export default PageComponent;
ListComponent.js
import React, { useEffect, useState } from 'react';
import useCustomMove from '../../hooks/useCustomMove';
import { getList } from '../../api/todoApi';
import PageComponent from '../common/PageComponent';
const initState ={
dtoList:[],
pageNumList:[],
pageRequestDTO:null,
prev:false,
next:false,
totalCount:0,
prevPage:0,
nextPage:0,
totalPage:0,
current:0
}
const ListComponent = () => {
const {page, size, moveToList} = useCustomMove();
const [serverData, setServerData] = useState(initState);
useEffect(()=>{
getList({page,size}).then(data=>{
console.log(data);
setServerData(data);
})
},[page,size])
return (
<div className='border-2 border-blue-100 mt-10 mr-2 ml-2'>
<div className='flex flex-wrap mx-auto justify-center p-6'>
{serverData.dtoList.map(todo => {
return(
<div key={todo.tno} className='w-full min-w-[400px] p-2 m-2 rounded shadow-md'>
<div className='flex'>
<div className='font-extrabold text-2xl p-2 w-1/12'>
{todo.tno}
</div>
<div className='font-extrabold text-1xl m-1 p-2 w-8/12'>
{todo.title}
</div>
<div className='font-medium text-1xl m-1 p-2 w-2/10'>
{todo.dueDate}
</div>
</div>
</div>
)
})}
</div>
<PageComponent serverData={serverData} movePage={moveToList}></PageComponent>
</div>
);
};
export default ListComponent;
화면
동일 페이지 클릭시에도 서버 호출 하고 싶을 경우 처리 방법
- refresh 설정값을 false로 선언 후 refresh 가 false 인 경우 계속 호출되도록 수정했다.
useCustomMove.js
const [refresh, setRefresh] = useState(false);
const moveToList = (pageParam) =>{
let queryStr = "";
if(pageParam){
const pageNum = getNum(pageParam.page,1);
const sizeNum = getNum(pageParam.size, 10);
queryStr = createSearchParams({page: pageNum, size: sizeNum}).toString();
}else{
queryStr = queryDefault;
}
setRefresh(!refresh); // 추가
navigate({pathname: `../list`, search:queryStr});
}
ListComponent.js
useEffect(()=>{
getList({page,size}).then(data=>{
console.log(data);
setServerData(data);
})
},[page,size,refresh])
조회 페이지 이동
- 번호 클릭시 상세 페이지로 이동가능하게 구현
useCustomMove.js
const moveToRead = (num) =>{
console.log(queryDefault);
navigate({pathname: `../read/${num}`, search:queryDefault});
}
ListComponent.js
- onClick 이벤트 추가
<div key={todo.tno} className='w-full min-w-[400px] p-2 m-2 rounded shadow-md'
onClick={()=>moveToRead(todo.tno)}>
<div className='flex'>
<div className='font-extrabold text-2xl p-2 w-1/12'>
{todo.tno}
</div>
<div className='font-extrabold text-1xl m-1 p-2 w-8/12'>
{todo.title}
</div>
<div className='font-medium text-1xl m-1 p-2 w-2/10'>
{todo.dueDate}
</div>
</div>
</div>
4.5 등록 컴포넌트와 모달창 처리
AddComponent.js
import React, { useState } from 'react';
const initState = {
title: '',
writer: '',
dueDate: ''
}
const AddComponent = () => {
const[todo, setTodo] = useState({...initState});
const handleChangeTodo = (e) =>{
todo[e.target.name] = e.target.value;
setTodo({...todo});
}
const handleClickAdd = () =>{
console.log(todo);
}
return (
<div className='border-2 border-sky-200 mt-10 m-2 p-4'>
<div className='flex justify-center'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
TITLE
</div>
<input className='w-4/5 p-6 rounded-r border border-solid border-neutral-500 shadow-md'
name='title'
type={'text'}
value={todo.title}
onChange={handleChangeTodo}>
</input>
</div>
</div>
<div className='flex justify-center'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
WRITER
</div>
<input className='w-4/5 p-6 rounded-r border border-solid border-neutral-500 shadow-md'
name='writer'
type={'text'}
value={todo.writer}
onChange={handleChangeTodo}>
</input>
</div>
</div>
<div className='flex justify-center'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
DUEDATE
</div>
<input className='w-4/5 p-6 rounded-r border border-solid border-neutral-500 shadow-md'
name='dueDate'
type={'date'}
value={todo.dueDate}
onChange={handleChangeTodo}>
</input>
</div>
</div>
<div className='flex justify-center'>
<div className='relative mb-4 flex p-4 flex-wrap items-stretch'>
<button type='button'
className='rounded p-4 w-36 bg-blue-500 text-xl text-white'
onClick={handleClickAdd}>
ADD
</button>
</div>
</div>
</div>
);
};
export default AddComponent;
AddPage.js
import React from 'react';
import AddComponent from '../../components/todo/AddComponent';
const AddPage = () => {
return (
<div className='p-4 w-full bg-white'>
<div className='text-3xl font-extrabold'>
Todo Add Page
</div>
<AddComponent/>
</div>
);
};
export default AddPage;
화면
서버 호출 확인
todoApi.js
export const postAdd = async (todoObj) =>{
const res = await axios.post(`${prefix}/`, todoObj);
return res.data;
}
AddComponent.js
const handleClickAdd = () =>{
postAdd(todo).then(result =>{
console.log(result);
setTodo({...initState});
}).catch(e=>{
console.error(e);
})
}
결과 확인
모달 컴포넌트 구현
- 서버와의 통신이 끝나고 나면 result 상태를 변경해서 보이게 한다!!
- 모달 영역을 클릭하거나 close modal 버튼을 클릭시 result 상태를 다시 null로 변경시켜 닫도록 구현되었다.
ResultModal.js
const ResultModal = ({title, content, callbackFn}) =>{
return(
<div className={`fixed top-0 left-0 z-[1055] flex h-full w-full justify-center bg-black bg-opacity-20`}
onClick={()=>{
if(callbackFn){
callbackFn();
}
}}>
<div className="absolute bg-white shadow dark:bg-gray-700 opacity-100 w-1/4 rounded
mt-10 mb-10 px-6 min-w-[600px]">
<div className="justify-center bg-warning-400 mt-6 mb-6 text-2xl border-b-4 border-gray-500">
{title}
</div>
<div className="text-4xl border-orange-400 border-b-4 pt-4 pb-4">
{content}
</div>
<div className="justify-end flex">
<button className="rounded bg-blue-500 mt-4 mb-4 px-6 pt-4 pb-4 text-lg text-white"
onClick={()=>{
if(callbackFn){
callbackFn();
}
}}>
Close Modal
</button>
</div>
</div>
</div>
)
}
export default ResultModal;
AddComponent.js
const closeModal = () =>{
setResult(null);
}
{/*모달 */}
{result ? <ResultModal title={'Add Result'} content={`New ${result} Added`}
callbackFn={closeModal}/> : <></>}
화면
페이지 이동
AddComponent.js
const {moveToList} = useCustomMove();
const closeModal = () =>{
setResult(null);
moveToList();
}
4.6 수정 / 삭제 처리
- 삭제 버튼 : 삭제 결과를 모달창으로 보여주고 'todo/list' 로 이동
- 수정 버튼 : 수정 결과를 모달창으로 보여주고 '/todo/read/번호' 로 이동
ModifyPage.js
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import ModifyComponent from '../../components/todo/ModifyComponent';
const ModifyPage = () => {
const {tno} = useParams();
return (
<div className='p-4 w-full bg-white'>
<div className='text-3xl font-extrabold'>
Todo Modify Page
</div>
<ModifyComponent tno={tno}/>
</div>
);
};
export default ModifyPage;
ModifyComponent.js
import React, { useEffect, useState } from 'react';
import { deleteOne, getOne, putOne } from '../../api/todoApi';
import useCustomMove from '../../hooks/useCustomMove';
import ResultModal from '../common/ResultModal';
const initState ={
tno:0,
title:'',
writer:'',
dueDate:'null',
complete:false
}
const ModifyComponent = ({tno, moveList, moveRead}) => {
const [todo, setTodo] = useState({...initState});
// 모달 창을 위한 상태
const [result, setResult] = useState(null);
// 이동
const {moveToList, moveToRead} = useCustomMove();
useEffect(()=>{
getOne(tno).then(data => setTodo(data));
}, [tno])
const handleChangeTodo = (e) =>{
todo[e.target.name] = e.target.value;
setTodo({...todo});
}
const handleChangeTodoComplete = (e) =>{
const value = e.target.value;
todo.complete = (value === 'Y');
setTodo({...todo});
}
const handleClickModify = () =>{
putOne(todo).then(data=>{
console.log(`modify result : ${data}`);
setResult('Modified');
})
}
const handleClickDelete = () =>{
deleteOne(tno).then(data =>{
console.log(`delete result: ` + data);
setResult("Deleted");
})
}
// 모달 창 닫기
const closeModal = () =>{
if(result === 'Deleted'){
moveToList();
}else{
moveToRead(tno);
}
}
return (
<div className='border-2 border-sky-200 mt-10 m-2 p-4'>
{/* 모달 창 */}
{result ? <ResultModal title={'처리결과'} content={result} callbackFn={closeModal}/> : <></>}
<div className='flex justify-center mt-10'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
TNO
</div>
<div className='w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100'>
{todo.tno}
</div>
</div>
</div>
<div className='flex justify-center mt-10'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
WRITER
</div>
<div className='w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100'>
{todo.writer}
</div>
</div>
</div>
<div className='flex justify-center mt-10'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
TITLE
</div>
<input className='w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md'
name='title'
type={'text'}
value={todo.title}
onChange={handleChangeTodo}
/>
</div>
</div>
<div className='flex justify-center mt-10'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
DUEDATE
</div>
<input className='w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md'
name='dueDate'
type={'date'}
value={todo.dueDate}
onChange={handleChangeTodo}
/>
</div>
</div>
<div className='flex justify-center mt-10'>
<div className='relative mb-4 flex w-full flex-wrap items-stretch'>
<div className='w-1/5 p-6 text-right font-bold'>
COMPLETE
</div>
<select
name='status'
className='border-solid border-2 rounded m-1 p-2'
onChange={handleChangeTodoComplete}
value={todo.complete ? 'Y' : 'N'}>
<option value='Y'>Completed</option>
<option value='N'>Not Tet</option>
</select>
</div>
</div>
<div className='flex justify-end p-4'>
<button type='button' className='rounded p-4 m-2 text-xl w-32 text-white bg-red-500'
onClick={handleClickDelete}>
DELETE
</button>
<button type='button' className='rounded p-4 m-2 text-xl w-32 text-white bg-blue-500'
onClick={handleClickModify}>
MODIFY
</button>
</div>
</div>
);
};
export default ModifyComponent;
'개발 > 연습프로젝트기록' 카테고리의 다른 글
6. 리액트와 상품 API 서버 연동 (0) | 2024.04.01 |
---|---|
5. 상품 API 서버 구성하기 (0) | 2024.04.01 |
3. API 서버 (0) | 2024.03.24 |
2. React-Router (0) | 2024.03.23 |
1. 개발 환경설정 (0) | 2024.03.23 |