일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- abstract
- 객체 비교
- 집합_SET
- 다형성
- EnhancedFor
- 예외처리
- 사용자예외클래스생성
- 컬렉션 타입
- 예외미루기
- GRANT VIEW
- NestedFor
- 메소드오버로딩
- 자바
- exception
- cursor문
- 추상메서드
- 제네릭
- 정수형타입
- 오라클
- oracle
- 인터페이스
- 어윈 사용법
- 참조형변수
- 대덕인재개발원
- 생성자오버로드
- 한국건설관리시스템
- 환경설정
- Java
- 컬렉션프레임워크
- 자동차수리시스템
- Today
- Total
거니의 velog
(23) 리액트와 상품 API 서버 연동 2 본문
2. 등록 페이지와 컴포넌트 처리
* 등록 작업은 AddPage.js 이름의 페이지 컴포넌트를 추가하고 라우터를 설정해 주는 것으로 시작한다.
import React from "react";
const AddPage = () => {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Add Page</div>
</div>
);
};
export default AddPage;
(1) 라우팅 설정
* router/productsRouter.js에는 AddPage에 대한 라우팅 정보를 설정한다.
import React, { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";
const productsRouter = () => {
const Loading = <div>Loading....</div>;
const ProductsList = lazy(() => import("../pages/products/ListPage"));
const ProductsAdd = lazy(() => import("../pages/products/AddPage"));
return [
{
path: "list",
element: (
<Suspense fallback={Loading}>
<ProductsList />
</Suspense>
),
},
{
path: "",
element: <Navigate replace to="/products/list" />,
},
{
path: "add",
element: (
<Suspense fallback={Loading}>
<ProductsAdd />
</Suspense>
),
},
];
};
export default productsRouter;
* 브라우저에서 /products/add 경로로 접근하거나 /products/ 관련 메뉴에서 ADD 링크에서 이동이 가능해진다.
(2) 상품의 AddComponent와 API 호출
* AddPage 컴포넌트에서 실제 화면의 내용을 구성하는 작업은 components 폴더에 products 폴더를 생성하고 AddComponent.js를 작성해서 진행한다.
import React from "react";
const AddComponent = () => {
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
<div className="flex justify-center">
<h1>Product Add Component</h1>
</div>
</div>
);
};
export default AddComponent;
* /pages/products/AddPage.js 내 AddComponent를 import 한다.
import React from "react";
import AddComponent from "../../components/products/AddComponent";
const AddPage = () => {
return (
<div className="p-4 w-full bg-white">
<div className="text-3xl font-extrabold">Products Add Page</div>
<AddComponent />
</div>
);
};
export default AddPage;
* 브라우저에서 /products/add 경로를 통해서 AddComponent가 추가된 것을 확인한다.
* AddComponent의 내용은 Todo 예제의 AddComponents를 참고해서 첨부파일을 추가할 수 있는 구성으로 작성하는데 우선은 화면 구성부터 완료한다. 첨부파일은 useRef()를 사용해서 처리한다.
import React, { useRef, useState } from "react";
const initState = {
pname: "",
pdesc: "",
price: 0,
files: [],
};
const AddComponent = () => {
const [product, setProduct] = useState({ ...initState });
const uploadRef = useRef();
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
console.log(product);
};
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">Product Name</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="pname"
type={"text"}
value={product.pname}
onChange={handleChangeProduct}
></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">Desc</div>
<textarea
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md resize-y"
name="pdesc"
rows="4"
onChange={handleChangeProduct}
value={product.pdesc}
>
{product.pdesc}
</textarea>
</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">Price</div>
<input
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
name="price"
type={"number"}
value={product.price}
onChange={handleChangeProduct}
></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">Files</div>
<input
ref={uploadRef}
className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
type={"file"}
multiple={true}
></input>
</div>
</div>
<div className="flex justify-end">
<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;
* 먼저, 브라우저에서 화면이 아래와 같이 정상적으로 구성되었는지 확인한다.
[useRef()와 FormData]
* useRef()는 기존의 자바스크립트에서 document.getElementById()와 유사한 역할을 한다. 리액트의 컴포넌트는 태그의 id 속성을 활용하면 나중에 동일한 컴포넌트를 여러 번 사용해서 화면에 문제가 생기기 때문에(웹 표준 위배 : HTML 태그의 id는 중복되어서는 안된다는 규칙) useRef()를 이용해서 처리한다.
* 예제에서는 ADD 버튼을 클릭했을 때 첨부파일에 선택된 정보를 읽어내서 첨부파일의 정보를 파악하고 이를 Ajax 전송에 사용하는 FormData 객체로 구성해야 한다. FormData 타입으로 처리된 모든 내용은 Axios를 이용해서 서버를 호출할 때 사용하게 된다.
* AddComponent의 ADD 버튼의 처리는 아래와 같이 변경된다.
const AddComponent = () => {
(...)
const handleClickAdd = (e) => {
const files = uploadRef.current.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
//other data
formData.append("pname", product.pname);
formData.append("pdesc", product.pdesc);
formData.append("price", product.price);
console.log(formData);
};
(...)
}
* useRef를 이용할 때는 current 라는 속성을 활용해야 현재 DOM 객체를 참조하게 된다. Ajax를 전송할 때는 FormData 객체를 통해서 모든 내용을 담아서 전송하게 된다.
[productsAPI의 개발]
* Axios와의 통신은 api 폴더 내에 productsApi.js 파일을 작성해서 처리한다. 이전의 todo의 경우 단순한 JSON 데이터를 처리했지만 이번에는 multipart/form-data 방식으로 서버를 호출해야만 한다.
* api 폴더에 productsApi.js 파일을 추가하고 아래와 같은 내용으로 작성한다.
import axios from "axios";
import { API_SERVER_HOST } from "./todoApi";
const host = `${API_SERVER_HOST}/api/products`;
export const postAdd = async (product) => {
const header = { headers: { "Content-Type": "multipart/form-data" } };
// 경로 뒤 '/' 주의
const res = await axios.post(`${host}/`, product, header);
return res.data;
};
* postAdd() 에서 주의해야 하는 점은 Axios가 기본적으로 Content-Type을 application/json 을 이용하기 때문에 파일 업로드를 같이할 때는 multipart/form-data 헤더 설정을 추가해 주어야 한다는 점이다.
* AddComponent에서는 버튼을 클릭했을 때 postAdd()를 호출하도록 코드를 수정한다. 전송해야 하는 모든 데이터는 FormData 객체로 처리한다.
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
const initState = {
(...)
};
const AddComponent = () => {
(...)
const handleClickAdd = (e) => {
const files = uploadRef.current.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
//other data
formData.append("pname", product.pname);
formData.append("pdesc", product.pdesc);
formData.append("price", product.price);
console.log(formData);
postAdd(formData);
};
(...)
};
export default AddComponent;
* 작성된 AddComponent는 /products/add 링크에서 확인하고 이전 장에서 스프링 부트로 구성된 서버를 실행한 후에 상품 추가를 시도해 본다.
* ADD 버튼을 클릭한 후에 브라우저의 개발자 도구에서 전달되는 내용(payload)을 확인했을 때 아래 오른쪽 그림과 같이 파일 데이터들은 binary 데이터로 전달되는 것을 확인해야 한다(아래 files 항목은 여러 개의 파일을 전송했을 경우이다).
* 브라우저에서는 서버가 정상적으로 상품을 추가했는지 응답(response) 데이터로 확인할 수 있다.
* 서버에서는 정상적으로 파일 업로드가 되었는지 업로드 폴더를 확인하고 데이터베이스에서 해당 번호의 상품 이미지들이 정상적으로 처리되었는지 확인한다. 위의 테스트에서는 2개의 이미지를 전송했으므로 썸네일 이미지를 포함해서 4개의 파일이 생성된다.
[진행 모달창과 결과 모달창]
* API 서버와 통신이 필요한 모든 기능은 서버에서 데이터를 가져오는(fetch) 시간을 고려해야 한다. 흔히 '처리중입니다...' 혹은 '로딩중...' 과 같은 메시지가 보이는 모달창을 통해서 이를 처리하는 경우가 많은데 파일 업로드와 같은 경우 단순한 텍스트를 이용할 때 보다 많은 시간이 걸리기 때문에 진행에 대한 모달창을 보여주고 처리가 끝나면 결과를 모달창으로 보여주는 것이 바람직하다.
* 진행 모달창을 작성하기 위해서 components의 commons 폴더에 FetchingModal을 추가한다.
import React from "react";
const FetchingModal = () => {
return (
<div
className={`fixed top-0 left-0 z-[1055] flex h-full w-full place-items-center justify-center bg-black bg-opacity-20`}
>
<div className=" bg-white rounded-3xl opacity-100 min-w-min h-1/4 min-w-[600px] flex justify-center items-center ">
<div className="text-4xl font-extrabold text-orange-400 m-20">
Loading.....
</div>
</div>
</div>
);
};
export default FetchingModal;
* FetchingModal은 ResultModal과 유사하지만 외부에서 보이거나 사라지도록 제어되기 때문에 속성으로 전달받아야 하는 데이터가 없다는 점이 다르다.
* AddComponent는 서버와의 통신 상태를 fetching이라는 상태를 useState()를 통해서 제어한다. API 서버를 호출할 때 fetching 상태를 true로 해 주고 데이터를 가져온 후 false로 변경해서 화면에서 사라지도록 한다.
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
(...)
const AddComponent = () => {
const [product, setProduct] = useState({ ...initState });
const uploadRef = useRef();
const [fetching, setFetching] = useState(false);
const handleChangeProduct = (e) => {
(...)
};
const handleClickAdd = (e) => {
(...)
setFetching(true);
postAdd(formData).then((data) => {
setFetching(false);
});
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
(...)
</div>
);
};
export default AddComponent;
* 위의 코드가 적용되면 ADD 버튼을 클릭했을 때 FetchingModal이 보였다가 서버 통신이 끝나면 사라지게 된다.
* 일부러 인터넷 속도를 느리게 하여 Loading 모달 창이 잘 뜨는지 확인하고 싶다면 다음의 옵션을 체크하여 테스트 한다.
* 테스트 후에는 다시 원상복귀시킨다.
* 혹은 FetchingModal을 좀 더 오랫동안 보이도록 하고 싶은 경우 API 서버의 ProductController에서는 고의로 등록작업 처리 시에 Thread.sleep()을 이용해서 약간의 시간이 걸리도록 코드를 수정해 줄 수 있다.
// 상품을 등록하는 메서드입니다.
@PostMapping("/")
public Map<String, Long> register(ProductDTO productDTO) {
// 요청된 상품 정보를 로그에 출력합니다.
log.info("register : " + productDTO);
// 상품 DTO에서 업로드된 파일 목록을 가져옵니다.
List<MultipartFile> files = productDTO.getFiles();
// 파일 유틸을 사용하여 파일을 저장하고, 저장된 파일 이름 리스트를 가져옵니다.
List<String> uploadFileNames = fileUtil.saveFiles(files);
// 상품 DTO에 업로드된 파일 이름 리스트를 설정합니다.
productDTO.setUploadFileNames(uploadFileNames);
// 상품 서비스를 호출하여 상품을 등록하고, 상품 번호를 받아옵니다.
Long pno = productService.register(productDTO);
// FetchingModal 테스트를 위한 딜레이 시간 주기
try {
Thread.sleep(2000); // 2초간 살려둬~
} catch (InterruptedException e) {
e.printStackTrace();
}
// 결과를 Map 형태로 반환합니다.
return Map.of("result", pno);
}
* 브라우저를 이용해서 새로운 상품을 등록하면 아래와 같이 FetchingModal 창이 보인 후 사라지게 된다.
[결과 모달창 처리]
* FetchingModal 은 서버와의 통신 과정에서만 보이고, 등록/수정/삭제의 경우 처리 결과를 보여줄 필요가 있다. 이에 대한 처리는 ResultModal을 이용해서 처리한다.
* 현재 API 서버에서 상품이 등록되면 전송되는 JSON 데이터는 result라는 속성을 가지게 된다.
* AddComponent에서는 이를 활용해서 화면에 결과를 보여주도록 구성한다.
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import ResultModal from "../common/ResultModal";
const initState = {
pname: "",
pdesc: "",
price: 0,
files: [],
};
const AddComponent = () => {
const [product, setProduct] = useState({ ...initState });
const uploadRef = useRef();
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(null);
const handleChangeProduct = (e) => {
product[e.target.name] = e.target.value;
setProduct({ ...product });
};
const handleClickAdd = (e) => {
const files = uploadRef.current.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append("files", files[i]);
}
//other data
formData.append("pname", product.pname);
formData.append("pdesc", product.pdesc);
formData.append("price", product.price);
console.log(formData);
setFetching(true);
postAdd(formData).then((data) => {
setFetching(false);
setResult(data.result);
});
};
const closeModal = () => {
//ResultModal 종료
setResult(null);
};
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
{result ? (
<ResultModal
title={"Product Add Result"}
content={`${result}번 등록 완료`}
callbackFn={closeModal}
/>
) : (
<></>
)}
(...)
);
};
export default AddComponent;
* FetchingModal 과 ResultModal이 적용되면 화면에서는 ADD 버튼을 클릭했을 때 FetchingModal이 보여지고, 서버와의 통신이 끝나면 ResultModal이 동작하는 것을 볼 수 있다.
[등록 후 목록 페이지 이동]
* 등록에서 마지막으로 남은 작업은 ResultModal이 닫히면 목록 페이지로 이동하는 것이다. 라우팅과 관련된 작업은 이미 hooks 폴더 내에 useCustomMove()를 작성해 두었기 때문에 이를 활용한다.
* AddComponent의 경우 useCustomMove의 실행 결과에서 moveToList만 필요한 상황이므로 이를 이용해서 ResultModal이 닫히는 closeModal 함수에서 moveToList()를 호출하도록 구성한다.
* AddComponent의 주요 코드는 아래와 같이 작성된다.
import React, { useRef, useState } from "react";
import { postAdd } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import ResultModal from "../common/ResultModal";
import useCustomMove from "../../hooks/useCustomMove";
const initState = {
(...)
};
const AddComponent = () => {
const [product, setProduct] = useState({ ...initState });
const uploadRef = useRef();
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(null);
const { moveToList } = useCustomMove(); //이동을 위한 함수
const handleChangeProduct = (e) => {
(...)
};
const handleClickAdd = (e) => {
(...)
};
const closeModal = () => {
//ResultModal 종료
setResult(null);
moveToList({ page: 1 }); //모달 창이 닫히면 페이지로 이동
};
return (
(...)
);
};
export default AddComponent;
* 최종적으로 브라우저를 통해서 확인해 보면 아래 화면들과 같이 등록된 상품의 번호가 보이고 ResultModal 창을 닫은 후에는 자동으로 /products/list 경로로 이동하는 것을 확인할 수 있다.
'SpringBoot_React 풀스택 프로젝트' 카테고리의 다른 글
(25) 리액트와 상품 API 서버 연동 4 (0) | 2024.03.05 |
---|---|
(24) 리액트와 상품 API 서버 연동 3 (0) | 2024.03.05 |
(22) 리액트와 상품 API 서버 연동 1 (0) | 2024.03.05 |
(21) 상품 API 서버 구성하기 6 (0) | 2024.03.04 |
(20) 상품 API 서버 구성하기 5 (0) | 2024.03.04 |