본문 바로가기
카테고리 없음

스프링 부트(Spring Boot) JPA 게시판 - 게시글 리스트 페이지 구현하기 (With. MySQL, Thymeleaf)

by 도뎡 2021. 9. 28.
반응형

본 JPA 게시판 프로젝트는 단계별(step by step)로 진행됩니다.


이전 글에서는 게시글 생성/수정, 리스트 조회를 처리하는 API를 구현하고, 테스팅까지 진행해 보았습니다.

이번 글부터는 화면(UI)을 담당하는 Presentation Layer(컨트롤러, 뷰)를 처리하는데요.

JPA 게시판을 시작할 때 말씀드렸듯이 JS 영역에서는 jQuery 라이브러리를 이용하지 않고,

순수한 자바스크립트(Vanila JS)만을 사용해서 코드를 작성하며,

화면은 타임리프(Thymeleaf) 템플릿 엔진과 부트스트랩(Bootstrap) 프레임워크를 이용해서 구성합니다.

그럼, 바로 시작해 볼까요?

 

1. 정적 리소스(Static Resources) 추가하기

static.zip
1.02MB

업로드한 static.zip에는 부트스트랩 관련 css, fonts, plugin, scripts 폴더가 압축되어 있습니다.

해당 파일의 압축을 해제하고, src/main/resources의 static 폴더에 추가해 주세요.

리소스가 추가된 static 폴더 구조

 

2. 타임리프 레이아웃(Thymeleaf Layout) 라이브러리 추가하기

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' /* Log4JDBC */
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' /* Thymeleaf Layout */
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

build.gradle 파일, dependencies의 implementation 끝에 thymeleaf-layout-dialect를 추가하고,

저장한 다음에는 프로젝트 마우스 우클릭 - Gradle - Refresh Gradle Project를 실행해 주세요 :)

 

3. 공통 레이아웃(Common Layout) HTML 생성하기

HTML에서 공통적으로 사용하는 header, footer와 css, script를 Import 하는 코드는

웬만해서는 별개의 HTML 파일로 분리한 뒤에 공통 레이아웃으로 참조해서 처리하는 경우가 대다수입니다.

자세한 내용은 뒤에서 다시 설명드리도록 하겠습니다.

우선, src/main/resources의 templates 안에 layout.html을 생성해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
        <meta content="IE=edge" http-equiv="X-UA-Compatible" />
        <meta content="width=device-width, initial-scale=1, user-scalable=no" name="viewport" />
        <meta content="" name="description" />
        <meta content="" name="author" />
        <link rel="stylesheet" th:href="@{/css/style.css}" />
        <link rel="stylesheet" th:href="@{/plugin/mCustomScrollbar/jquery.mCustomScrollbar.min.css}" />
        <title>JPA 게시판</title>
    </head>
    <body>
        <div class="fixed-navbar">
            <div class="pull-left">
                <h1 class="page-title">Board</h1>
            </div>
        </div>
        <div id="wrapper">
            <div class="main-content">
                <div class="row row-inline-block small-spacing">
                    <div class="col-xs-12">
                        <div class="box-content">
                            <div class="clearfix">
                                <h4 class="box-title pull-left"></h4>
                            </div>

                            <!--/* 페이지 Content 영역 */-->
                            <th:block layout:fragment="content"></th:block>

                        </div>
                    </div>
                </div>
                <footer class="footer">
                    <ul class="list-inline">
                        <li>2016 © NinjaAdmin.</li>
                    </ul>
                </footer>
            </div>
        </div>

        <script th:src="@{/scripts/moment.min.js}"></script>
        <script th:src="@{/scripts/jquery.min.js}"></script>
        <script th:src="@{/scripts/main.js}"></script>
        <script th:src="@{/plugin/bootstrap/js/bootstrap.min.js}"></script>
        <script th:src="@{/plugin/mCustomScrollbar/jquery.mCustomScrollbar.concat.min.js}"></script>

        <!--/* JavaScript 영역 */-->
        <th:block layout:fragment="script"></th:block>
    </body>
</html>

 

게시글 리스트, 작성(등록), 상세 페이지에서 공통으로 사용될 레이아웃 HTML입니다.

모든 페이지에서 참조하는 CSS와 JS 파일은 동일하기 때문에 레이아웃 HTML에 선언해야 합니다.

여기서 기억하셔야 할 부분은 다음과 같습니다. (자세한 사용법은 뒤에서 말씀드릴게요!)


1. 타임리프를 이용하기 위해 <html> 태그에 th 네임스페이스 선언

2. 29번 라인, content fragment를 이용해서 각 페이지의 내용(HTML) 렌더링

3. 49번 라인, script fragment를 이용해서 각 페이지의 JS 코드 렌더링


 

4. 게시글 리스트 HTML 생성하기

templates 폴더 안에 board 폴더와 list.html을 생성해 주세요.

list.html까지 추가된 templates 폴더의 구조는 다음과 같습니다.

templates 폴더 구조

 

다음으로 list.html을 다음과 같이 변경해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">

</html>

 

5. 페이지 처리용 컨트롤러(Controller) 생성하기

우리는 페이지와 API를 처리하는 컨트롤러를 분리하기로 했었습니다.

API 처리용 컨트롤러는 이미 있으니, 페이지 처리용 컨트롤러만 처리해주면 되겠네요.

코드는 따로 설명이 필요 없을 정도로 심플합니다.

package com.study.board.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/board")
public class BoardPageController {

    /**
     * 게시글 리스트 페이지
     */
    @GetMapping("/list")
    public String openBoardList() {
        return "board/list";
    }

}

 

이제, App을 실행하고 게시글 리스트 페이지로 접근해보면 다음과 같은 화면을 보실 수 있습니다.

게시글 리스트 페이지

 

다음은 list.html의 소스 코드입니다.

분명히 <html> 태그 외에는 그 어떠한 코드도 없는데, 화면이 어떻게 보이는 걸까요?

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">

</html>

 

네, 예상하셨듯이 layout:decorator="layout" <<< 이 녀석 덕분인데요.

="layout"의 layout은 templates/layout.html, 즉 layout.html 파일의 경로를 의미합니다.

스프링 부트에서 타임리프는 기본적으로 templates 폴더를 기준으로 HTML 파일을 바라봅니다.

만약, 레이아웃을 참조하는 코드가 제거된 리스트 페이지에는 당연히 아무것도 안 보이겠지요?

layout 참조가 없는 게시글 리스트 페이지

 

6. list.html 수정하기

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">

    <th:block layout:fragment="content">
    <!--/* 검색 영역 */-->
    <div class="input-group" id="adv-search">
        <select id="searchType" class="form-control" style="width: 100px;">
            <option value="">전체</option>
            <option value="title">제목</option>
            <option value="content">내용</option>
            <option value="writer">작성자</option>
        </select>
        <input type="text" id="searchKeyword" class="form-control" placeholder="키워드를 입력해 주세요." style="width: 300px;" />
        <button type="button" class="btn btn-primary">
            <span aria-hidden="true" class="glyphicon glyphicon-search"></span>
        </button>
    </div>

    <!--/* 게시글 영역 */-->
    <div class="table-responsive clearfix">
        <table class="table table-hover">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>등록일</th>
                    <th>조회 수</th>
                </tr>
            </thead>

            <!--/* 게시글 리스트 Rendering 영역 */-->
            <tbody id="list">

            </tbody>
        </table>
        <div class="btn_wrap text-right">
            <a class="btn btn-primary waves-effect waves-light">Write</a>
        </div>

        <!-- 페이지네이션 Rendering 영역 -->
        <nav>

        </nav>
    </div>
    </th:block>


    <th:block layout:fragment="script">
    <script th:inline="javascript">
	/*<![CDATA[*/

		

	/*]]>*/
    </script>
    </th:block>

</html>

 

<th:block layout:fragment="content">

layout.html의 content fragment를 채우기 위한 태그입니다.

쉽게 말씀드리자면, layout.html의 content를 list.html의 content에 선언한 HTML로 렌더링 합니다.

해당 fragment에는 검색 HTML, 게시글 HTML, 페이징 HTML이 포함되어 있는데요.

페이징 영역은 아직은 눈에 보이지 않는 것이 정상입니다.

 

<th:block layout:fragment="script">

layout.html의 script fragment를 채우기 위한 태그입니다.

content와 마찬가지로, layout.html의 script를 list.html의 script에 선언한 JS 코드로 렌더링 합니다.

 

<script th:inline="javascript">와 CDATA

JS 코드에서 타임리프 문법을 사용하기 위해서는 th:inline="javascript"를 선언해야 하며,

스크립트의 시작과 끝 태그를 CDATA로 묶어줘야 합니다.

 

7. 게시글 리스트 조회 API 호출하기

함수 작성에 앞서, 다시 게시글 리스트 페이지로 접근해보면 다음과 같은 화면을 보실 수 있습니다.

content가 채워진 게시글 리스트 페이지

 

이제, 게시글 리스트 데이터를 화면에 렌더링 하는 작업만 해주면 됩니다.

BoardApiController의 findAll( ) 메서드를 호출하는 JS 함수를 작성해 보도록 하겠습니다.

		/**
		 * 페이지 로딩 시점에 실행되는 함수
		 */
		window.onload = () => {

			findAll();
		}

		/**
		 * 게시글 리스트 조회
		 */
		function findAll() {

			fetch('/api/boards').then(response => {
				if (response.ok) {
   					return response.json();
				}
			}).then(json => {
				let html = '';

				if (!json.length) {
					html = '<td colspan="5">등록된 게시글이 없습니다.</td>';
				} else {
					json.forEach((obj, idx) => {
						html += `
							<tr>
    							<td>${json.length - idx}</td>
    							<td class="text-left">
    								<a href="javascript: void(0);">${obj.title}</a>
    							</td>
    							<td>${obj.writer}</td>
    							<td>${moment(obj.createdDate).format('YYYY-MM-DD HH:mm:ss')}</td>
    							<td>${obj.hits}</td>
							</tr>
						`;
					});
				}

				document.getElementById('list').innerHTML = html;
			});
		}

 

window.onload( ) 함수

해당 함수는 페이지가 로딩되는 시점에 한 번만 실행되는 함수입니다.

페이지에 접근했을 때 게시글 리스트를 조회하는 findAll( ) 메서드를 실행합니다.

 

findAll( ) 함수

게시글 리스트를 조회하는 API를 호출하는 용도의 함수입니다.

jQuery의 Ajax를 사용하지 않고, fetch( ) 함수를 사용했는데요.

Fetch API는 jQuery Ajax보다 API를 간편하게 호출할 수 있도록 브라우저에서 제공해주는 API입니다.

fetch( ) 함수에서 꼭 기억해야 할 사항은 다음과 같습니다.


1. 첫 번째 인자로 요청(Request) URL을 전달한다.

2. 두 번째 인자로 요청(Request) Method, Headers, Body(Data) 등을 전달한다.

3. 두 번째 인자가 비어있는 경우, 기본적으로 GET Method로 요청한다.

4. fetch( )는 Promise라는 이름의 객체(Object)를 반환한다.

* (Fetch API를 더욱 자세히 알아보고 싶으시다면, 여기와 여기를 참고해 주세요 :)


findAll( ) 함수

 

66~70번 라인

게시글 리스트를 조회하는 BoardApiContorller의 findAll( ) 메서드를 호출합니다.

첫 번째 then( ) 안의 response는 Promise 객체이며, Promise의 ok 상태가 true인 경우,

즉 정상적으로 API가 호출된 경우에만 게시글 리스트(JSON)를 리턴합니다.

리턴된 JSON은 70번 라인에 있는 then( ) 안의 json에 담기게 됩니다.

 

html 변수

게시글 HTML을 저장할 변수입니다.

 

73~75번 라인

데이터가 없는 경우에 실행되는 로직입니다.

 

75~91번 라인

다음의 이미지는 70번 라인 then( ) 안의 json을 console로 출력한 게시글 데이터입니다.

게시글 리스트 JSON

 

데이터가 있는 경우, json에 담긴 객체의 길이(Lnegth)만큼 forEach를 돌아 HTML을 그리는데요.

77~87번 라인을 보시면, 따옴표( ' ) 대신, 키보드 Tab키 위에 있는 이름 모를( ` ) 기호가 사용됩니다.

JS에서는 해당 기호를 백틱이라 부르며, 백틱을 사용하면 ${ } 표현식으로 데이터에 접근할 수 있습니다.

즉, 데이터나 문자열을 연결하기에 훨씬 수월해졌다는 이야기입니다 :)

// ( + ) 기호를 사용하던 기존 방식
html += '<td>'+ obj.title +'</td>';
html += '<td>'+ obj.writer +'</td>';
html += '<td>'+ obj.hits +'</td>';

 

마지막으로 84번 라인의 moment( ).format( ) 함수는 Moment.js 라이브러리에서 제공해주는 함수입니다.

Moment.js는 날짜 데이터를 더욱 쉽게 컨트롤할 수 있도록 도와주는 JS 라이브러리이며,

여러분의 편의를 생각해서, static.zip 파일 안에 moment.min.js 파일을 포함시켜 두었습니다 :)

 

8. 애플리케이션 실행해보기

데이터가 없는 경우의 게시글 리스트 결과 화면입니다.

데이터가 없는 게시글 리스트 페이지

 

데이터가 있는 경우의 게시글 리스트 결과 화면입니다.

데이터가 있는 게시글 리스트 페이지

 

마무리

여기까지, 게시글 리스트 페이지를 구현해 보았습니다.

물론, 리스트 조회 기능에 검색과 페이징이 빠져서는 안되겠지요?

검색, 페이징 기능은 게시글 등록, 상세 페이지 구현이 완료되면 따로 포스팅 하려고 합니다.

다음 글에서는 게시글의 생성과 수정을 담당하는 게시글 등록 페이지를 구현해 보도록 할게요.

오늘도 따라오시느라 고생 많으셨고, 다음 글에서 뵙겠습니다.

방문해 주셔서 감사합니다 :)

 


진행에 어려움을 겪으시는 분들이 계실 수 있으니, 프로젝트를 첨부해 드리도록 하겠습니다.

application.properties의 데이터베이스 정보만 내 PC 환경과 일치하도록 변경해서 사용해 주세요 :)


Board.zip
2.14MB

반응형

댓글