Spring Boot/STUDY

[Spring Boot] React연동 프로젝트(1) - 프로젝트 생성 및 리스트 출력

코맹 2024. 7. 2. 17:12

 

프로젝트 실행하기 위해 Spring Boot 웹서버, React 프론트 웹 서버도 함께 실행시킨다.

 

 

1. 리액트 프로젝트 생성

- /spring03/frontboard 폴더 생성
cd spring03
npx create-react-app frontboard

frontboard 폴더 생성

 

 

2. 리액트 라이브러리 설치, npm

  • React용 Bootstrap 설치
npm install react-bootstrap bootstrap

npm audit fix --force는 절대 설치 xxx

 

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">&copy; 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;  
  }
  
}

return list한 값이 json 형태로 나오는 것 확인할 수 있음

 

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;
  • 더미데이터를 지우고 백에서 보낸 데이터로 변경

 

  • 데이터가 잘 들어오는 것 확인!