SpringBoot #6 SpringBoot+React 기반 간단한 게시판 생성하기 #7 게시물 상세 조회

2021. 10. 18. 14:51JAVA/Spring

이전글

https://yonghwankim-dev.tistory.com/149

 

SpringBoot #6 SpringBoot+React 기반 간단한 게시판 생성하기 #6 게시물 검색 조건 및 등록 처리하기

이전글 https://yonghwankim-dev.tistory.com/147 SpringBoot #6 SpringBoot+React 기반 간단한 게시판 생성하기 #5 웹 페이지에 페이지 번호 추가 및 기능 이전 글 https://yonghwankim-dev.tistory.com/144 Spri..

yonghwankim-dev.tistory.com

 

개요

이전글에서는 게시물의 검색과 등록 처리를 구현하였습니다. 이번글에서는 상세 게시물 조회를 구현하도록 하겠습니다. 그리고 상세 게시물 조회 처리를 구현하기 전에 이전글에서 게시물의 리스트를 출력하면서 발생한 문제점을 해결하도록 하겠습니다.

 

0. 페이지 재로딩 문제

페이지 재로딩 문제는 페이지에서 다른 페이지로 이동하고 뒤로가기 또는 페이지를 새로고침 할때 기존의 데이터를 유지하지 못하고 초기화되는 문제를 의미합니다. 

 

예를 들어 아래 그림은 게시물 리스트에서 2페이지 번호를 클릭하여 이동한 상태입니다. (before)

위 상태에서 새로고침을 누르면 아래와 같은 결과가 됩니다. (after)

위 결과를 보면 새로고침을 수행시 2페이지 번호를 유지하는게 아닌 1페이지로 초기화된 것을 볼 수 있습니다. 이는 WebBoardListComponent.jsx 파일의 컴포넌트의 page 추출방식이 get 방식의 url 파라미터를 추출하는 것이 아닌 페이지 번호를 클릭하였을때 page 변수 변경시키고 그에 따른 게시물 리스트만 재 랜더링하는 방식이었습니다.

 

하지만 위와 같이 2페이지로 이동한 상태에서 새로고침을 클릭하게 되면 page를 변경하는 것이 아닌 get방식의 url 파라미터 없이 페이지 자체를 새로 렌더링하기 때문에 page가 1로 초기화가 되고 게시물 리스트가 1페이지로 초기화되는 문제가 발생한 것입니다. 제가 원하던 의도는 2페이지로 이동하고 새로고침을 하더라도 게시물 리스트의 2페이지를 유지하는 것을 의도로 하고 있습니다.

 

그리고 새로고침하여 초기화되는 문제뿐만 아니라 동일하게 A 페이지에서 B 페이지로 이동하고 B페이지에서 브라우저 뒤로가기 버튼을 통해서 A 페이지로 되돌아갈 때 A 페이지의 페이지 관련 데이터가 유지되지 않아서 초기화되어 1페이지로 이동되는 문제점이 있습니다.

 

페이지 재로딩 문제 요약

  • 동일한 페이지에서 새로고침을 할때 해당 페이지의 데이터가 유지되지 않음
  • 페이지에서 또 다른 페이지로 이동하고 다시 뒤로가기 할 때 동일하게 기존 페이지의 데이터가 유지되지 않음

페이지 재로딩 문제 해결방법

  1. localStroage를 활용한 방법
  2. sessionStroage를 활용한 방법

 

페이지 재로딩 문제 해결방법에 대한 저 같은 경우 localStroage를 활용한 방법으로 문제를 해결하였습니다. 아래의 소스코드는 이전글의 WebBoardListComponent.jsx의 내용을 수정한 내용입니다.

 

src/component/webBoard/WebBoardListComponent.jsx

import React, {Component} from 'react';
import ApiService from "../../ApiService";
import change_date from '../../function/change_date';
class WebBoardListComponent extends Component{
    constructor(props){
        super(props);

        const {page,size,type,keyword} = localStorage;
    
        this.state = {
            result : null,      
            prevPage : null,
            nextPage : null,
            pageList : [],
            boards : [],
            page : page===null || page===undefined ? 1 : page,
            size : size===null || size===undefined ? 10 : size,
            type : type===null || type===undefined ? "" : type,
            keyword : keyword===null || type===undefined  ? "" : keyword
        }
    }

    handleWebBoardSubmit = ()=>{
        const {page, size, type, keyword} = this.state;
        localStorage.setItem('page',page);
        localStorage.setItem('size',size);
        localStorage.setItem('type',type);
        localStorage.setItem('keyword',keyword);
    }

    componentDidMount(){
        this.reloadWebBoardList(this.state.page, this.state.size, this.state.type, this.state.keyword);
    }

    componentDidUpdate(){
        this.handleWebBoardSubmit();
    }

    reloadWebBoardList = (page=1, size=10, type="", keyword="")=>{
        ApiService.fetchWebBoards(page, size, type, keyword)
                    .then(res=>{
                        this.setState({
                                        result : res.data,
                                        prevPage : res.data.prevPage,
                                        nextPage : res.data.nextPage,
                                        pageList : res.data.pageList,
                                        boards:res.data.result.content
                                    })
                    })
                    .catch(err=>{console.log("reloadWebBoardList() Error!",err);});
    }

    onChangePage = (p)=>{
        this.setState({page : p});
        this.reloadWebBoardList(p,this.state.size, this.state.type, this.state.keyword);
    }
    onChangeType = (e)=>{
        this.setState({type : e.target.value});
    }
    onChangeKeyword = (e)=>{
        this.setState({keyword : e.target.value});
    }
    onClickSearch = ()=>{
        this.reloadWebBoardList(this.state.page, this.state.size, this.state.type, this.state.keyword);
    }

    onClickRegister = ()=>{
        this.props.history.push("/boards/register");
    }

    onClickView = (bno)=>{
        this.props.history.push("/boards/view?bno="+bno);
    }

    render(){
        return(
            <>
            <div>
                <h2>WebBoard List</h2>
                <div>
                    <button onClick={()=>{this.onClickRegister()}}>Register</button>
                </div>
                <table>
                    <thead>
                        <tr>
                            <th>Bno</th>
                            <th>Title</th>
                            <th>Wirter</th>
                            <th>Content</th>
                            <th>Regdate</th>
                            <th>Updatedate</th>
                        </tr>
                    </thead>
                    <tbody>
                        {
                            this.state.boards.map( board=>
                                    <tr key={board.bno}>
                                        <td>{board.bno}</td>
                                        <td onClick={()=>{this.onClickView(board.bno)}} style={{cursor:'pointer'}}>{board.title}</td>
                                        <td>{board.writer}</td>
                                        <td>{board.content}</td>
                                        <td>{change_date(board.regdate)}</td>
                                        <td>{change_date(board.updatedate)}</td>
                                    </tr>
                                )
                        }
                    </tbody>
                </table>

                {/* search */}
                <div>
                    <select name="searchType" onChange={this.onChangeType} value={this.state.type}>
                        <option value="">--</option>
                        <option value='t'>Title</option>
                        <option value='w'>Writer</option>
                        <option value='c'>Content</option>
                    </select>
                    <input type="text" onChange={this.onChangeKeyword} value={this.state.keyword}/>
                    <button onClick={this.onClickSearch}>Search</button>
                </div>
            </div>
            <div>
                {/* pagination */}
                <ul>                 
                    {
                        /* PREV 버튼 */
                        this.state.prevPage===null ? null : <li><button onClick={()=>{this.onChangePage(this.state.prevPage.pageNumber+1)}}>PREV {this.state.prevPage.pageNumber+1}</button></li>
                    }
                    {
                        /* 페이지 번호 버튼 */
                        this.state.pageList.map(page =>
                                                        <li key={page.pageNumber+1}>
                                                            {
                                                                this.state.result.currentPageNum-1===page.pageNumber ? 
                                                                    <button onClick={()=>{this.onChangePage(page.pageNumber+1)}} style={{color:"red"}}>{page.pageNumber+1}</button>
                                                                    :
                                                                    <button onClick={()=>{this.onChangePage(page.pageNumber+1)}}>{page.pageNumber+1}</button>
                                                                    
                                                            }
                                                        </li> 
                                                        
                                                )
                    }
                    {
                        /* NEXT 버튼 */
                        this.state.nextPage===null ? null : <li><button onClick={()=>{this.onChangePage(this.state.nextPage.pageNumber+1)}}>NEXT {this.state.nextPage.pageNumber+1}</button></li>
                    }
                </ul>
            </div>

            </>
        );
    }
}

export default WebBoardListComponent;

위와 같이 수정하면 아래와 같이 다른 페이지에서 새로고침을 하거나 다른 페이지로 이동하였다가 뒤로 돌아가도 페이징 데이터를 유지하는 것을 확인할 수 있습니다. 그리고 검색 조건을 설정하고 게시물 검색을 수행하고 새로고침을 하여도 페이지를 유지하는 것을 볼 수 있습니다.

 

1. 게시물의 조회

게시물의 리스트 화면에서는 게시물의 상세 내용을 볼 수 있도록 처리되어야 합니다. 게시물을 조회하는 경우에는 반드시 다음의 경우를 고려해야 합니다.

  • 검색 조건이 없는 경우의 조회 : 페이지 번호를 유지한 상태에서 조회로 이동
  • 검색 조건이 있는 경우의 조회 : 페이지 번호 + 기타 검색 조건을 모두 유지한 상태에서 이동

WebBoardViewComponent 구현

src/component/webBoard/WebBoardViewComponent.jsx

import { Component } from "react";
import ApiService from "../../ApiService";
import queryString from 'query-string';
import change_date from "../../function/change_date";

class WebBoardViewComponent extends Component{
    constructor(props){
        super(props);
        
        const b = queryString.parse(this.props.location.search).bno

        this.state = {
            bno : b,
            board : {
                bno : 0,
                title : "",
                content : "",
                writer : "",
                regdate : ""
            }
        }
    }

    componentDidMount(){
        this.reloadWebBoardView(this.state.bno);
    }
    
    reloadWebBoardView = (bno)=>{
        ApiService.viewWebBoard(bno)
                    .then(res=>{
                        this.setState({board : res.data});
                    })
                    .catch(err=>{
                        console.log("viewWebBoard() error!",err);
                    });
    }

    onClickModify = ()=>{
        this.props.history.push("/boards/modify");
    }

    onClickGoList = ()=>{
        this.props.history.push("/boards/list");
    }

    render(){
        return (
            <>
            <div>View Page</div>
            <div>
                <div>
                    <label>Bno</label>
                    <input name="bno" value={this.state.board.bno} readOnly/>
                </div>
                <div>
                    <label>Title</label>
                    <input name="title" value={this.state.board.title} readOnly/>
                </div>
                <div>
                    <label>Content</label>
                    <input name="content" value={this.state.board.content} readOnly/>
                </div>
                <div>
                    <label>Writer</label>
                    <input name="writer" value={this.state.board.writer} readOnly/>
                </div>
                <div>
                    <label>Regdate</label>
                    <input name="regdate" value={change_date(this.state.board.regdate)} readOnly/>
                </div>
            </div>
            <div>
                <button onClick={()=>{this.onClickModify()}}>Modify</button>
                <button onClick={()=>{this.onClickGoList()}}>Go List</button>
            </div>
            </>
        ); 
    }
}

export default WebBoardViewComponent;

게시물을 상세 조회할때 필요한 데이터는 게시물의 번호(bno)입니다. 게시물 번호를 전달하는 방식은 get방식의 url 파라미터를 추출하는 방식(/boards/view?bno=01)입니다. get방식을 사용하는 이유는 브라우저의 주소창 입력만으로 상세 게시물 페이지로 이동하기 위함이고 주소창을 봄으로써 몇번째 게시물인지 알기 위해서입니다.

 

reloadWebBoardView 함수를 호출함으로써 특정 게시물 번호(bno)에 따른 게시물 정보를 서버에 요청합니다.

 

src/ApiService.js

import axios from 'axios';

const BOARD_API_BASE_URL = "http://localhost:8080/boards";

class ApiService{

    fetchWebBoards(page, size, type, keyword){
        return axios.get(BOARD_API_BASE_URL + "/list?page="+page+"&size="+size+"&type="+type+"&keyword="+keyword);
    }

    registerWebBoard(title, content, writer){
        return axios.post(BOARD_API_BASE_URL + "/register",null,{params : {title : title, content : content, writer : writer}});
    }

    viewWebBoard(bno){
        return axios.get(BOARD_API_BASE_URL+"/view?bno="+bno);
    }
    
}

export default new ApiService();

 

src/function/change_date.jsx

import moment from 'moment';

function change_date(published_at){

    const published_date = moment(published_at).format('YYYY-MM-DD');
    return published_date;
}

export default change_date;

위 함수는 게시물의 날짜 관련 데이터를 형식화해주는 함수입니다.

 

server : org.zerock.controller.WebBoardController.java

package org.zerock.controller;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/boards/")
@Log
public class WebBoardController {
	... 생략
    
	@GetMapping("/view")
	public Optional<WebBoard> view(Long bno) {
		log.info("BNO : " + bno);
		
		
		Optional<WebBoard> result = repo.findById(bno);
		
		if(result.isPresent())
		{
			return result;
		}
		else
		{
			return null;
		}	
	}	
}

서버는 클라이언트로부터 게시물의 번호(bno)를 받고 repo.findById() 메서드를 통하여 bno번호의 게시물을 데이터베이스에 질의합니다. Optional 객체 타입을 사용한 이유는 bno 번호에 따른 게시물이 없을 수도 있기 때문입니다. 만약 해당 게시물이 존재하면 질의한 결과를 반환합니다.

 

실행결과

위 상태에서 297번 게시물을 클릭합니다.

성공적으로 게시물의 정보를 출력할 수 있었습니다. 이상태에서 뒤로가기 버튼을 클릭하면 결과는 아래와 같습니다.

위 그림과 같이 이전 페이지에서 이동했었던 2페이지 그대로 이동한 것을 볼 수 있습니다.

 

이번 글에서는 페이지 재로딩 문제를 해결하였고 게시물의 상세 페이지 조회를 구현하였습니다.

 

References

boot06 project source code : https://github.com/yonghwankim-dev/SpringBoot-Study 
스타트 스프링 부트, 구멍가게 코딩단 지음