다음과 같이 프로젝트를 생성합니다.
이전 프로젝트의 구성을 그대로 유지한채 Spring Boot 프로젝트에 React를 배포해봅시다.
VSCode에서 다음 명령어를 순서대로 입력하여 React 프로젝트를 생성합니다.
create-react-app react14-spring
cd react07-spring
npm install react-router-dom
프로젝트가 생성되었다면 프로젝트를 기본형으로 만듭니다.
다음과 같이 JDBC실습.sql 파일에 작성합니다.
create table board (
num number primary key, --일련번호
title varchar2(200) not null, --제목
content varchar2(2000) not null, --내용
id varchar2(10) not null, --작성자의 아이디
postdate date default sysdate not null, --작성일
visitcount number(6) --조회수
);
Musthave 계정에 연결하고 실행합니다.
다음과 같이 JDBC실습.sql 파일에 작성합니다.
create sequence seq_board_num
increment by 1
start with 1
minvalue 1
nomaxvalue
nocycle
nocache;
Musthave 계정에 연결하고 실행합니다.
Cross-Origin Resource Sharing(CORS)은 출처가 다른 자원 간의 공유를 허용하는 개념입니다. 이는 웹 브라우저에서 하나의 출처(도메인, 프로토콜, 포트)에 있는 자원이 다른 출처에 있는 자원에 접근할 수 있도록 해주는 메커니즘을 말합니다. CORS는 기본적으로 보안상의 이유로 브라우저가 다른 출처의 자원에 접근하는 것을 제한하지만, 서버가 특정 출처에서의 접근을 허용하도록 설정할 수 있습니다.
접속 URL은 일반적으로 아래와 같이 구성되는데, Protocol, Host, Port의 3가지 요소로 구성됩니다. 이 중 세 가지가 모두 동일할 때 동일 출처(Origin)로 간주됩니다. 따라서, Spring 서버가 8586 포트를 사용하고 React가 3000 포트를 사용한다면, 두 서버는 서로 다른 출처로 간주됩니다.

웹 브라우저는 보안상의 이유로 cross-origin HTTP 요청을 제한합니다. Spring 서버에서 별도의 처리가 없으면 요청 시 에러가 발생합니다. 따라서 cross-origin 요청을 허용하려면 웹 서버에서 적절한 설정이 필요합니다.
설정 클래스를 생성합니다. 다음과 같이 com/edu/springboot/WebCorsConfig.java 파일을 작성합니다.
package com.edu.springboot;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("Custom-Header")
.allowCredentials(true)
.maxAge(3600);
}
}
cross-origin 요청을 허용하기 위한 설정이 완료되었습니다.
App 컴포넌트를 생성합니다. 다음과 같이 src\App.js 파일을 작성합니다.
import './App.css';
import React from 'react';
// <BrowserRouter>와 동일한 역할의 컴포넌트로 라우팅 처리를 위한 컴포넌트 랩핑에 사용된다.
import { HashRouter } from 'react-router-dom';
// 라우터 처리를 위한 컴포넌트 임포트
import { Route, Routes } from 'react-router-dom';
import MyList from './components/MyList';
import MyView from './components/MyView';
import MyWrite from './components/MyWrite';
/* 공통링크로 사용할 컴포넌트
차후 Spring Boot 프로젝트로 배포하면 React로 만든 페이지와 Spring에서 만든 페이지를 오갈 수 있다. */
const TopNavi = () => {
return(
<nav>
<table border="1" width="90%">
<tr>
<td style={{ textAlign: 'center' }}>
<a href='/'>Main</a> |
<a href='/crud/index.html'>React CRUD</a> |
<a href='/boardList.do'>Spring Board</a> |
<a href='/rboard/index.html'>React Board</a>
</td>
</tr>
</table>
</nav>
);
}
function App() {
return (
<HashRouter>
<div className="App">
<TopNavi />
<Routes>
<Route path='' element={<MyList />} />
<Route path='/list' element={<MyList />} />
<Route path='/view'>
<Route path=':num' element={<MyView />} />
</Route>
<Route path='/write' element={<MyWrite />} />
</Routes>
</div>
</HashRouter>
);
}
export default App;
목록 컴포넌트를 생성합니다. 다음과 같이 src\components\MyList.jsx 파일을 작성합니다.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
function MyList(props) {
/* State 선언
Spring 서버에서 API를 통해 JSON 배열을 받은 후 저장한다.
따라서 초기값은 빈 배열로 설정한다. */
var [myJSON, setmyJSON] = useState([]);
// 컴포넌트의 Lifecycle에서 사용하는 훅으로 해당 컴포넌트가 렌더링이 완료되면 자동으로 호출된다.
useEffect(function() {
// JavaScript에서 제공하는 비동기 함수로 Spring 서버의 List API를 호출하여 결과를 콜백받는다.
fetch("http://localhost:8586/restBoardList.do?pageNum=1")
.then((result) => {
// console.error("결과1");
// console.log(result);
// 첫 번째 then절로 콜백되면 JSON 형식으로 변환 후 두 번쨰 then절로 넘긴다.
return result.json();
})
.then((json) => {
console.error("결과");
console.log(json);
// 여기서 State를 변경하고 새롭게 랜더링한다.
setmyJSON(json);
});
return () => {
console.log('#Life', 'useEffect 실행 ==> 컴포넌트 언마운트');
}
}, []);
let trTag = [];
/* 수명주기 함수에서 fetch()를 통해 콜백받은 데이터를 여기에서 반복한 후 trTag 배열에 추가한다.
최초에는 빈 배열이므로 반복되지 않는다. */
for (let i=0; i<myJSON.length; i++) {
let data = myJSON[i];
// console.log(data);
trTag.push(
<tr key={ data.num }>
<td>{ data.num }</td>
<td><Link to={ '/view/' + data.num }>{ data.title }</Link></td>
<td>{ data.id }</td>
<td>{ data.postdate }</td>
<td>{ data.visitcount }</td>
</tr>
);
}
return (
<div>
<h2>Spring 게시판 [목록]</h2>
<table border="1">
<thead>
<tr>
<th>num</th>
<th>title</th>
<th>id</th>
<th>postdate</th>
<th>visitcount</th>
</tr>
</thead>
<tbody>{ trTag }</tbody>
</table>
<Link to="/write">작성</Link>
</div>
);
}
export default MyList;
이제 게시판의 목록을 조회할 수 있습니다.
내용조회 컴포넌트를 생성합니다. 다음과 같이 src\components\MyView.jsx 파일을 작성합니다.
import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom';
function MyView(props) {
/* 파라미터로 전달되는 값을 받기 위해 사용하는 훅
열람의 요청명은 "view/일련번호" 형식으로 정의하였고, Router 설정시 num으로 결정되어 있다. */
var params = useParams();
console.log('파라미터', params.num);
// API 통신 후 얻어올 게시물의 레코드를 저장할 State
var [boardRow, setBoardRow] = useState({});
// 컴포넌트 렌더링 후 자동으로 호출되는 수명주기 훅
useEffect(function() {
// 파라미터로 전달된 일련번호를 변경해서 fetch 함수 호출
fetch("http://localhost:8586/restBoardView.do?num=" + params.num)
.then((result) => {
return result.json();
})
.then((json) => {
console.log(json);
// API를 통해 얻어온 값으로 State 변경하여 새롭게 렌더링한다.
setBoardRow(json);
});
return () => {
console.log('#Life', 'useEffect 실행 => 컴포넌트 언마운트');
}
}, []);
// 파싱된 내용을 출력
return (
<div>
<h2>Spring 게시판 [조회]</h2>
<table border='1'>
<tbody>
<tr>
<th>작성자</th>
<td>{ boardRow.id }</td>
</tr>
<tr>
<th>제목</th>
<td>{ boardRow.title }</td>
</tr>
<tr>
<th>작성일</th>
<td>{ boardRow.postdate }</td>
</tr>
<tr>
<th>내용</th>
<td>{ boardRow.content }</td>
</tr>
</tbody>
</table>
<Link to="/list">목록</Link>
</div>
)
}
export default MyView;
이제 게시물의 내용을 조회할 수 있습니다.
게시물 작성 컴포넌트를 생성합니다. 다음과 같이 src\components\MyWrite.jsx 파일을 작성합니다.
import React from 'react'
import { useNavigate } from 'react-router-dom';
function MyWrite(props) {
// 페이지 이동을 위한 훅으로 JSP의 sendRedirect()와 동일하다.
const navigate = useNavigate();
return (
<div>
<h2>Spring 게시판 [작성]</h2>
<form onSubmit={(event) => {
// submit이 되면 화면의 깜빡임이 생기므로 차단
event.preventDefault();
// Event 객체를 통해 폼값 받음
let id = event.target.id.value;
let title = event.target.title.value;
let content = event.target.content.value;
// 파라미터 저장을 위한 JS의 객체로 DTO와 동일한 역할을 한다.
const params = new URLSearchParams();
params.set('id', id);
params.set('title', title);
params.set('content', content);
/* POST 방식으로 데이터 전송하기 위한 JSON 객체 생성
body 프로퍼티에 실제 전송할 폼값을 지정한다. */
const data = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
},
body: params
};
// Spring 서버와 통신 (POST 방식으로 요청)
fetch('http://localhost:8586/restBoardWrite.do', data)
.then((result) => {
return result.json();
})
.then((json) => {
if (json.result === 1) {
console.log('글쓰기 성공');
}
});
// 글쓰기가 완료되면 목록으로 이동한다.
navigate("/list");
}}>
<table border='1'>
<tbody>
<tr>
<th>작성자</th>
<td><input type='text' name='id' value='musthave' /></td>
</tr>
<tr>
<th>제목</th>
<td><input type='text' name='title' /></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name='content' cols='22' rows='3'></textarea></td>
</tr>
</tbody>
</table>
<input type='submit' value='Submit' />
</form>
</div>
);
}
export default MyWrite;
이제 게시판에 게시물을 작성할 수 있습니다.
리액트 프로젝트 실행 시 스프링 부트 프로젝트도 함께 실행해야 합니다. 실행하면 다음과 같이 게시판의 목록이 보입니다.

게시물의 제목 링크를 클릭하면 해당 게시물의 내용을 조회할 수 있습니다.

게시판의 목록에서 '작성' 링크를 클릭하면 게시물을 작성할 수 있는 페이지로 이동합니다.

제목과 내용을 입력합니다.

'전송' 버튼을 클릭하면 게시물이 작성되고 게시판 목록으로 이동합니다.

목록에서 게시물이 성공적으로 작성된 것을 확인할 수 있습니다.
React 프로젝트를 배포하려는 Spring Boot 프로젝트의 static 폴더 하위에 새로운 폴더를 생성합니다. 여기서는 rboard 폴더와 crud 폴더를 생성하였습니다.
React 프로젝트는 배포 시 모든 파일 경로가 기본적으로 최상위 절대 경로로 설정되어 루트 디렉토리에서만 실행 가능한 구조를 가지게 됩니다. 따라서 이러한 문제를 해결하기 위해, 모든 경로에 상대 경로(./)를 추가해야 합니다. 수동으로는 Ctrl + F 단축어를 이용하여 일괄적으로 수정할 수 있지만, 이를 자동화하기 위해 빌드 시 해당 경로를 설정할 수 있습니다.
배포할 React 프로젝트의 package.json 파일에 다음의 항목을 추가합니다.
"homepage": "."
차후 배포한 후 build 폴더의 index.html 파일의 내용을 확인해보면, 모든 경로가 ./ 로 시작하는 것을 확인할 수 있습니다.
Terminal 에서 배포할 프로젝트로 이동합니다. node-modules 를 삭제하였다면 다음 명령어로 설치합니다.
npm i
애플리케이션을 배포하기 위한 최적화된 프로덕션 빌드를 생성합니다.
npm run build
빌드가 완료되었다면 Finder 에서 프로젝트의 build 폴더 내의 파일을 앞서 생성했던 static 폴더 하위의 폴더로 복사합니다.
다음과 같이 실행됩니다.

앞서 static 폴더 하위에 생성했던 폴더명을 경로명 뒤에 붙이면 React 페이지로 이동이 가능합니다.
결론적으로 include 를 사용하면 부분적인 페이지 로딩이 가능하고, 이를 통해 React로 만든 페이지와 Spring으로 만든 페이지 간의 전환도 가능합니다.
단독 페이지로 실행하는 경우에는 static 하위에 저장하면 되지만, JSP의 일부분으로 include 할때는 webapp 하위에 저장해야한다.
static 디렉토리는 정적리소스를 저장하는 용도이므로 include 의 대상이 될 수 없기 때문이다.
웹앱 하위에 복붙하기
home : webapp/WEB-INF/views/home.jsp
공통링크 : /B17ReactAndBoot/src/main/webapp/link.jsp
목록 : /B17ReactAndBoot/src/main/webapp/WEB-INF/views/boardList.jsp
열람 : /B17ReactAndBoot/src/main/webapp/WEB-INF/views/boardView.jsp
근데 이거 페이지 소스보기 하면 static 폴더 하위에서 가져오게끔 되어 있음 그래서 어떻게 하나면
webapp 밑에 crud 폴더 지우기
그리고 vscode 열기
그리고 배포하려는거 build 자체를 지우고 다시 빌드할거임
package.json 수정
"homepage": "./crud"
우클릭 > open in integreed 터미널
npm run build
하고나며 ㄴindex.html에서 crud가 다 붙었을 거임
그리고 webapp 하위에 crud 만들어둔거 옮기기
