프로젝트 실행하기 위해 Spring Boot 웹서버, React 프론트 웹 서버도 함께 실행시킨다.
1. 리액트 프로젝트 생성
- /spring03/frontboard 폴더 생성
cd spring03
npx create-react-app frontboard
2. 리액트 라이브러리 설치, npm
- React용 Bootstrap 설치
npm install react-bootstrap bootstrap
npm install axios -> REST API 통신 라이브러리
npm install react-router-dom -> 리액트 화면 네비게이션
npm install react-js-pagination -> 리액트 페이징 처리
3. frontBoard 개발 시작
- App.js
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
// 화면 라우팅을 위해서 라이브러리 추가
import { Routes, Route } from 'react-router-dom'
import React from 'react';
function App() {
return (
<Routes>
</Routes>
);
}
export default App;
- logo.svg 삭제, react-router-dom으로 Routes, Route 사용
- index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
// 화면 전환에 필요한 react-router-dom
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div className='App h-full w-full'>
<BrowserRouter>
<div id="wrapper" className='flex flex-col h-screen'>
{/* head */}
{/* main-content */}
<App />
{/* footer */}
</div>
</BrowserRouter>
</div>
);
- reportWebVitals() 삭제
- <React.StrictMode> 삭제
- BrowserRouter react-router-dom 사용
/src/layout/Header.js, Footer.js 생성
- Header.js
import React from 'react';
const Header = () => {
// return은 화면을 그리겠다.
return (
<div className='container'>
<header className='d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom'>
<div id='logo-area' className='col-md-1 mb-2 mb-md-0'>
<a href="/home" className='d-inline-flex link-body-emphasis text-decoration-none'>
<img src={require('../logo.png')} alt="logo" width={40} />
</a>
</div>
<ul className='nav col-12 col-md-6 mb-2 justify-conter'>
<li><a href="#" className='nav-link px-2 link-secondary'>홈</a></li>
<li><a href="#" className='nav-link px-2 link-secondary'>게시판</a></li>
<li><a href="#" className='nav-link px-2 link-secondary'>질문응답</a></li>
</ul>
<div className='col-md-3 text-end me-3'>
로그인
회원가입
</div>
</header>
</div>
);
}
export default Header;
- Footer.js
import React from 'react';
const Footer = () => {
return (
<div className="container footer">
<footer className="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<div className="col-md-4 d-flex align-items-center">
<a href="/" className="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
</a>
<span className="mb-3 mb-md-0 text-body-secondary">© 2024 Company, Inc</span>
</div>
<ul className="nav col-md-4 justify-content-end list-unstyled d-flex">
<li className="ms-3"><a className="text-body-secondary" href="#"></a></li>
<li className="ms-3"><a className="text-body-secondary" href="#"></a></li>
<li className="ms-3"><a className="text-body-secondary" href="#"></a></li>
</ul>
</footer>
</div>
);
}
export default Footer;
- index.js에 Header.js, Footer.js 임포트 시키기
// 만든 페이지 추가
import Header from './layout/Header';
import Footer from './layout/Footer';
{/* head */}
<Header />
{/* footer */}
<Footer />
/src/routes/Home.js, BoardList.js,QnaList.js, Login.js 생성
function Home() {
return (
<div className='container'>
<h1>Home</h1>
</div>
);
}
export default Home;
- Home.js, BoardList.js,QnaList.js, Login.js 모두 동일하게 생성 (이름만 바꿔줌)
/App.js에 Route될 화면 추가
- App.js
// 만든 화면 추가
import Home from './routes/Home';
import BoardList from './routes/BoardList';
import QnaList from './routes/QnaList';
import Login from './routes/Login';
function App() {
return (
<Routes>
{/* a, Link 링크를 누르면 화면 전환될 페이지 */}
<Route path='/home' element={<Home />} />
<Route path='/boardList' element={<BoardList />} />
<Route path='/qnaList' element={<QnaList />} />
<Route path='/login' element={<Login />} />
</Routes>
);
}
- 금방 만든 Home.js, BoardList.js,QnaList.js, Login.js을 App.js에 추가
Header.js에 react-router-dom 추가.
Link, useNavigate 함수 사용
- Header.js 변경된 부분
import { Link, userNavigate } from 'react-router-dom';
<ul className='nav col-12 col-md-6 mb-2 justify-conter'>
<li><Link to="/home" className='nav-link px-2 link-secondary'>홈</Link></li>
<li><Link to="/boardList" className='nav-link px-2 link-secondary'>게시판</Link></li>
<li><Link to="/qnaList" className='nav-link px-2 link-secondary'>질문응답</Link></li>
</ul>
- <a>태그 -> <Link>로 변경
4. backboard RestAPI 추가
- /restcontroller/RestBoardController.java 생성, BoardController에 있는 메서드 복사
- RestBoardController.java
package com.eunji.backboard.restcontroller;
import org.springframework.data.domain.Page;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.eunji.backboard.entity.Board;
import com.eunji.backboard.entity.Category;
import com.eunji.backboard.service.BoardService;
import com.eunji.backboard.service.CategoryService;
import com.eunji.backboard.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.util.List;
@RequiredArgsConstructor
@RequestMapping("/api/board")
@RestController
@Log4j2
public class RestBoardController {
private final BoardService boardService; // 중간 연결책
private final MemberService memberService; // 사용자 정보
private final CategoryService categoryService; // 카테고리 사용
@GetMapping("/list/{category}")
public List<Board> list(@PathVariable(value="category") String category,
@RequestParam(value="page", defaultValue = "0") int page,
@RequestParam(value = "kw", defaultValue = "") String keyword) {
Category cate = this.categoryService.getCategory(category);
Page<Board> paging = this.boardService.getList(page, keyword, cate); // 검색 및 카테고리 추가
List<Board> list = paging.getContent();
log.info(String.format("▶▶▶ list에서 넘긴 게시글 수 %s", list.size()));
return list;
}
}
5. frontboard 개발 계속
backboard에서 RestBoardController를 만들고, 다시 frontboard에서 개발을 진행
/BoardList.js 로직 구현
import axios from 'axios'; // REST API 호출 핵심!!
// Hook함수 사용
import React, { useState, useEffect } from 'react';
// Navigation
import { Link } from 'react-router-dom';
function BoardList() { // 객체를 만드는 함수
// 변수 선언
const [boardList, setBoardList] = useState([]); // 배열값을 받아서 상태를 저장하기 때문에 []
// 함수선언
// 제일 중요!!
const getBoardList = async () => {
var pageString = 'page=0';
const resp = (await axios.get("//localhost:8080/api/board/list/free?" + pageString)).data;
setBoardList(resp); // boardList에 데이터가 들어감
console.log(resp);
}
useEffect(() => {
getBoardList();
}, []); // 값이 없을때는 빈 화면
return (
<div className='container'>
<table className='table'>
<thead className='table-dark'>
<tr className='text-center'>
<th>번호</th>
<th style={{width: '50%'}}>제목</th>
<th>작성자</th>
<th>조회수</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
{/* 반복으로 들어갈 부분 */}
<tr className='text-center'>
<td>게시글 번호</td>
<td className='text-start'>게시글 제목</td>
<td>작성자명</td>
<td>1</td>
<td>작성일</td>
</tr>
</tbody>
</table>
</div>
);
}
export default BoardList;
💥💥💥
Spring Boot에서 만든 Entity는 Board와 Reply 등의 OneToMany / ManyToOne가 JSON으로 변환할 때 문제 발생!
- /Entity를 그대로 사용하지 말고, RestAPI에서는 다시 클래스를 만들어야 함
- /RestBoardController.java getList()를 Board Entity -> BoardDto로 변경
/dto/BoardDto.java 생성
package com.eunji.backboard.dto;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BoardDto {
private Long bno;
private String title;
private String content;
private LocalDateTime createDate;
private LocalDateTime modifyDate;
private Integer hit;
private String writer;
private List<ReplyDto> replyList;
}
- ReplyDto.java 생성
package com.eunji.backboard.dto;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReplyDto {
private Long rno;
private String content;
private LocalDateTime createDate;
private LocalDateTime modifyDate;
private String writer;
}
/restcontroller.RestBoardController.java 수정
package com.eunji.backboard.restcontroller;
import org.springframework.data.domain.Page;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.eunji.backboard.dto.BoardDto;
import com.eunji.backboard.dto.ReplyDto;
import com.eunji.backboard.entity.Board;
import com.eunji.backboard.entity.Category;
import com.eunji.backboard.entity.Reply;
import com.eunji.backboard.service.BoardService;
import com.eunji.backboard.service.CategoryService;
import com.eunji.backboard.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@RequestMapping("/api/board")
@RestController
@Log4j2
public class RestBoardController {
private final BoardService boardService; // 중간 연결책
private final MemberService memberService; // 사용자 정보
private final CategoryService categoryService; // 카테고리 사용
@GetMapping("/list/{category}")
@ResponseBody
public List<BoardDto> list(@PathVariable(value="category") String category,
@RequestParam(value="page", defaultValue = "0") int page,
@RequestParam(value = "kw", defaultValue = "") String keyword) {
Category cate = this.categoryService.getCategory(category); // cate는 Category객체 변수사용x
Page<Board> paging = this.boardService.getList(page, keyword, cate); // 검색 및 카테고리 추가
List<Board> list = paging.getContent();
List<BoardDto> result = new ArrayList<BoardDto>();
for (Board origin : paging) {
List<ReplyDto> subList = new ArrayList<>();
BoardDto bdDto = new BoardDto();
bdDto.setBno(origin.getBno());
bdDto.setTitle(origin.getTitle());
bdDto.setContent(origin.getContent());
bdDto.setCreateDate(origin.getCreateDate());
bdDto.setModifyDate(origin.getModifyDate());
bdDto.setWriter(origin.getWriter().getUsername());
bdDto.setHit(origin.getHit());
if(origin.getReplyList().size() > 0) {
for(Reply reply : origin.getReplyList()) {
ReplyDto replyDto = new ReplyDto();
replyDto.setRno(reply.getRno());
replyDto.setContent(reply.getContent());
replyDto.setCreateDate(reply.getCreateDate());
replyDto.setModifyDate(reply.getModifyDate());
replyDto.setWriter(reply.getWriter().getUsername());
subList.add(replyDto);
}
bdDto.setReplyList(subList);
}
result.add(bdDto);
}
log.info(String.format("▶▶▶ list에서 넘긴 게시글 수 %s", result.size()));
return result;
}
}
💥💥💥
http://localhost:3000/boardList 접속시 오류발생( crossorigin 문제 )
⭕해결하기 위해 backboard에 있는 /security/SecurityConfig.java 몇 가지 추가해줘야 함 ⭕
/security/SecurityConfig.java 수정
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// CORS 타 서버간 접근 권한
.cors(corsConfig -> corsConfig.configurationSource(corsConfigurationSource()))
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
return request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000/")); // 허용할 Origin URL
config.setAllowCredentials(true);
return config;
};
}
- BoardList.js에서 console.log(resp);로 찍은 값 잘 나옴
다시 프론트엔드로 돌아와서 BoardList.js 수정
/BoardList.js RestAPI 호출내용 추가
- BoardList.js
import axios from 'axios'; // REST API 호출 핵심!!
// Hook함수 사용
import React, { useState, useEffect } from 'react';
// Navigation
import { Link } from 'react-router-dom';
function BoardList() { // 객체를 만드는 함수
// 변수 선언
const [boardList, setBoardList] = useState([]); // 배열값을 받아서 상태를 저장하기 때문에 []
// 함수선언
// 제일 중요!!
const getBoardList = async () => {
var pageString = 'page=0';
const resp = (await axios.get("//localhost:8080/api/board/list/free?" + pageString)).data;
setBoardList(resp); // boardList에 데이터가 들어감
console.log(resp);
}
useEffect(() => {
getBoardList();
}, []); // 값이 없을때는 빈 화면
return (
<div className='container'>
<table className='table'>
<thead className='table-dark'>
<tr className='text-center'>
<th>번호</th>
<th style={{width: '50%'}}>제목</th>
<th>작성자</th>
<th>조회수</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
{/* 반복으로 들어갈 부분 */}
{boardList.map((board) => (
<tr className='text-center' key={board.bno}>
<td>{board.bno}</td>
<td className='text-start'>{board.title}</td>
<td>{board.writer}</td>
<td>{board.hit}</td>
<td>{board.createDate}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default BoardList;
- 더미데이터를 지우고 백에서 보낸 데이터로 변경
- 데이터가 잘 들어오는 것 확인!
'Spring Boot > STUDY' 카테고리의 다른 글
[Spring Boot] React연동 프로젝트(3) - 회원가입 및 로그인 기능 구현 (1) | 2024.07.04 |
---|---|
[Spring Boot] React연동 프로젝트(2) - 페이징 기능 구현 (0) | 2024.07.03 |
[Spring Boot] JPA 프로젝트 - 비밀번호 찾기 및 변경 기능 구현(2) (0) | 2024.06.28 |
[Spring Boot] JPA 프로젝트 - 비밀번호 찾기 및 변경 기능 구현(1) (0) | 2024.06.27 |
[Spring Boot] JPA 프로젝트 - 에러페이지 작업 (0) | 2024.06.27 |