아래 내용은 코드로 배우는 React with 스프링부트 API서버 강의와 함께 설명이 부족한 부분에 대해서 조사하여 추가한 내용입니다.
Ajax 통신을 위해 npm install axios
를 통해 설치가 필요하며, Axios 는 자동 JSON 데이터 변환을 지원합니다. fetch()
의 경우 json()
을 이용하여 변환이 필요합니다.
async
는 promise
가 반환되도록 합니다. Promise 란 프로미스가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있습니다.
Promise 를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 약속을 반환합니다. ( 참고 )
자바스크립트는 await
키워드를 만나면 promise
가 처리될 때까지 기다립니다. promise
가 처리되면 그 결과가 함께 실행이 재개되며, promise
를 기다리는 동안 다른 스크립트를 실행하는 등의 역할을 할 수 있기 때문에 CPU 리소스가 낭비되지 않습니다. await
는 async
함수 안에서만 동작합니다.
export const getList = async(pageParam) => {
const {page, size} = pageParam;
// config 의 인자로 params 와 값을 지정하면 ? 로 시작하는 쿼리 스트링으로 변환된다
const res = await axios.get(`${prefix}/list`, {params:{
page: page,
size: size
}});
return res.data;
}
GET 요청을 수행하기 위해 axios.get()
을 사용하며, 비동기로 처리하기 위해 await 키워드를 사용합니다. config 의 인자로 params 라는 키 값에 page 와 size 를 전달하면 ?page=X&size=X
형태로 쿼리 스트링이 완성되게 됩니다.
const initState = {
toDoNo: 0,
title: '',
content: '',
dueDate: '',
complete: false
}
function ReadComponent({ tno }) {
const [toDo, setToDo] = useState(initState);
useEffect(() => {
getSingleData(tno).then(data => {
setToDo(data)
})
}, [tno]);
return (
{makeDiv('No', toDo.toDoNo)}
...
)
}
initState 라는 객체를 만들고, useState()
Hook 을 통해 ToDo 객체의 초기 상태를 지정합니다. 특정 데이터를 조회할 때 필요한 번호를 useEffect()
의 의존성 배열에 전달하고, 이 값이 변경되면 getSingleData() 를 호출해 다시 API 통신을 해서 데이터를 받아온 후 state 를 수정하여 다시 렌더링 될 수 있도록 합니다.
/todo/read/11
을 호출했을 때 스프링 서버와의 통신이 정상적으로 이루어진 것을 확인할 수 있습니다.
페이징 처리를 할 때 page 와 size 가 필요한데 만약 page 와 size 가 지정된 상태로 페이지를 이동한다고 했을 때, 쿼리 스트링으로 지정된 page 와 size 가 없어진다면 default 값으로 다시 변하게 됩니다. Custom Hook 을 만들어 페이지 이동 시, 해당 값들을 계속 유지해줄 수 있습니다.
const useMove = () => {
...
const page = getValue(searchParam.get('page'), 1);
const size = getValue(searchParam.get('size'), 10);
const queryDefault = createSearchParams({page, size}).toString()
const moveToList = (pageParam) => {
let queryStr = '';
if(pageParam) {
const pageNum = getValue(pageParam.page, 1);
const sizeNum = getValue(pageParam.size, 10);
queryStr = createSearchParams({page: pageNum, size:sizeNum}).toString();
} else {
queryStr = queryDefault;
}
navigate({
pathname: '../list',
search: queryStr
})
}
return {moveToList}
}
위의 예시가 앞서 말한 쿼리 파라미터를 유지해주기 위해 만든 Custom Hook 입니다. moveToList()
를 호출할 때 전달된 pageParam 이 있다면 해당 값들을 쿼리 파라미터를 자동으로 생성해주는 createSearchParams()
로 전달하고. 없다면 기본으로 생성된 값을 반환해주는 역할을 수행합니다.
위의 Hook 에는 List 로 이동하는 것 뿐만 아니라 상세 조회화면, 수정 화면과 같은 이동 함수들을 함께 정의하여 수정이 끝났을 때, 삭제가 끝났을 때 페이지 이동을 쉽게 구현할 수 있습니다.
function ListComponent() {
const {page, size} = useMove();
const [serverData, setServerData] = useState(initState);
useEffect(() => {
getList({page, size}).then(data => {
setServerData(data);
});
}, [page, size])
return (
{serverData.dtoList.map(todo =>
<div key={todo.toDoNo} className="w-full min-w-[400px] p-2 m-2 rounded shadow-md" onClick={() => moveToRead(todo.toDoNo)}>
<div className="flex ">
<div className="p-2 w-1/12 text-lg"> {todo.toDoNo} </div>
...
</div>task_nameto
</div>
)}
)
...
}
export default ListComponent
useMove()
을 통해 page 와 size 를 가져옵니다. page 와 size 값이 변경된 경우, List 를 반환하는 API( getList ) 를 호출하여 다시 서버로부터 데이터를 받아옵니다. 받아온 데이터는 useState()
를 통해 상태로 관리할 수 있습니다.
반복되는 부분 상위의 최상위 div 태그에서 onClick 이벤트를 설정하여 어떠한 글씨를 클릭해도 해당 데이터를 상세하게 볼 수 있는 페이지로 이동할 수 있도록 moveToPage 를 호출하도록 설정합니다. 페이지를 이동하는 함수는 위에서 만든 useMove()
에 정의됩니다.
ReadComponent 에서 moveToList()
를 호출할 때, 이는 useMove()
내부의 navigate 함수를 호출하게 됩니다. 이 때, navigate 함수가 list 페이지로 이동하면서 page와 size 정보를 쿼리 파라미터로 전달합니다.
따라서 ListComponent 에서 다시 useMove 를 사용하면 해당 페이지로 이동한 정보를 가져올 수 있습니다. 이러한 방식으로 컴포넌트 간에 useMove()
에서 관리하는 정보를 공유할 수 있습니다.
import React from 'react'
import { useState, useEffect } from 'react'
import useMove from '../../hooks/useMove';
import ResultModal from '../common/ResultModal';
import { getSingleData, modifyData } from '../../api/toDoApi';
// 1. ToDo 의 상태 초기값
const initState = {
toDoNo: 0,
title: '',
content: '',
dueDate: '',
complete: false
}
function ModifyComponent({ tno }) {
const { moveToRead } = useMove(); // 2. 페이지 이동을 위해 Custom Hook 인 useMove 에서 상세보기화면으로 이동하는 moveToRead 함수를 꺼낸다
const [toDo, setToDo] = useState(initState); //3. ToDo 객체를 상태로 관리한다
const [result, setResult] = useState(null); // 4. ResultModal 을 띄우는 경우를 판단하기 위한 state
// tno 값이 변경되었을 때 tno 값으로 다시 데이터를 조회한다
useEffect(() => {
getSingleData(tno).then(data => {
setToDo(data)
})
}, [tno]);
// input 태그의 값이 변했을 때 컴포넌트에서 state 로 관리하는 ToDo 의 데이터를 변경해준다
const handleChange = (event) => {
toDo[event.target.name] = event.target.value;
setToDo({ ...toDo });
}
// 수정 후 저장을 눌렀을 때, ResultModal 이 나와야 하기 때문에 판단 기준이 되는 result 의 상태를 변경
const handleClickSave = () => {
if(window.confirm("저장하시겠습니까?")) {
modifyData(toDo).then(data => {
setResult(tno);
setToDo({...initState}) // toDo 데이터 초기화
})
}
}
// ResultModal 의 Closed 버튼을 클릭한 경우 다시 result 를 초기화하고 상세보기 페이지로 이동한다
const handleClickClose = () => {
setResult(null); // null 로 변경하면 결과 Modal 이 사라진다
moveToRead(tno);
}
return (
<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-3/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="title" type={'text'} value={toDo.title} onChange={handleChange} ></input>
</div>
...
<button type="button"
onClick={() => moveToRead(toDo.toDoNo)}
className="rounded p-3 m-2 w-36 bg-gray-500 text-xl text-white" > Back </button>
<button type="button"
onClick={handleClickSave}
className="rounded p-3 m-2 w-36 bg-blue-500 text-xl text-white" > Save </button>
...
{result ? <ResultModal title={'Success'} content={`${result} ToDo Modified`} callbackFn={handleClickClose} />
: <></>}
</div>
);
}
export default ModifyComponent