Spring MVC 게시판 만들기 5편: 페이징과 검색 기능 UI 구현하기
💡 이 글에서 배울 것
화면(JSP)에서 페이징과 검색 UI를 어떻게 구현하는지, 검색 조건을 유지하는 방법까지 알아봅니다.
🎯 한눈에 보기
| 주제 | 핵심 내용 |
|---|---|
| JSTL | JSP에서 반복문, 조건문 사용 |
| Bootstrap | 스타일링된 페이지네이션 |
| JavaScript | 검색 조건 유지하며 페이지 이동 |
📌 이번 편에서 배울 것
이번 편에서는 화면(JSP)에서 페이징과 검색을 어떻게 구현하는지 알아봅니다:
- JSTL로 목록 출력하기
- Bootstrap 페이지네이션
- JavaScript 이벤트 처리
- 검색 조건 유지하기
📋 게시글 목록 화면 (list.jsp)
list.jsp는 크게 3가지 영역으로 구성됩니다:
flowchart TB
subgraph "list.jsp 구성"
A["📊 게시글 테이블"]
B["🔍 검색 필터"]
C["📄 페이지네이션"]
end
A --> B --> C
📊 게시글 테이블 (JSTL 반복문)
Controller에서 전달받은 dto.boardDTOList를 <c:forEach>로 반복합니다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<table class="table table-boardered">
<thead>
<th>Bno</th>
<th>Title</th>
<th>Writer</th>
<th>RegDate</th>
</thead>
<tbody>
<c:forEach var="board" items="${dto.boardDTOList}">
<tr data-bno="${board.bno}">
<td><c:out value="${board.bno}" /></td>
<td>
<!-- 상세보기 링크 (검색 조건 유지) -->
<c:url var="readUrl" value="/board/read/${board.bno}">
<c:param name="page" value="${dto.page}" />
<c:param name="size" value="${dto.size}" />
<c:param name="types" value="${dto.types}" />
<c:param name="keyword" value="${dto.keyword}" />
</c:url>
<a href="${readUrl}">
<c:out value="${board.title}" />
</a>
</td>
<td><c:out value="${board.writer}" /></td>
<td><c:out value="${board.createdDate}" /></td>
</tr>
</c:forEach>
</tbody>
</table>
💡
<c:out>을 쓰는 이유: XSS(크로스 사이트 스크립팅) 공격 방지! HTML 태그가 실행되지 않도록 이스케이프합니다.
🔍 검색 필터 UI
Bootstrap Select와 Input을 사용한 검색 폼:
<div class="d-flex justify-content-end">
<!-- 검색 타입 선택 -->
<select name="typeSelect" class="form-select me-2">
<option value="">--</option>
<option value="T" ${dto.types == 'T' ? 'selected' : ''}>제목</option>
<option value="C" ${dto.types == 'C' ? 'selected' : ''}>내용</option>
<option value="W" ${dto.types == 'W' ? 'selected' : ''}>작성자</option>
<option value="TC" ${dto.types == 'TC' ? 'selected' : ''}>제목 OR 내용</option>
<option value="TW" ${dto.types == 'TW' ? 'selected' : ''}>제목 OR 작성자</option>
<option value="TCW" ${dto.types == 'TCW' ? 'selected' : ''}>제목 OR 내용 OR 작성자</option>
</select>
<!-- 검색어 입력 -->
<input type="text" class="form-control me-2"
name="keywordInput" value="<c:out value='${dto.keyword}'/>" />
<!-- 검색 버튼 -->
<button class="btn btn-outline-info searchBtn">Search</button>
</div>
💡
${dto.types == 'T' ? 'selected' : ''}: 현재 검색 타입이면 선택 상태 유지!
📄 페이지네이션 UI

Bootstrap의 pagination 컴포넌트를 사용합니다:
<ul class="pagination">
<!-- 이전 버튼 -->
<c:if test="${dto.prev}">
<li class="page-item">
<a class="page-link" href="${dto.start - 1}">Prev</a>
</li>
</c:if>
<!-- 페이지 번호들 -->
<c:forEach var="num" items="${dto.pageNums}">
<li class="page-item ${dto.page == num ? 'active' : ''}">
<a class="page-link" href="${num}">${num}</a>
</li>
</c:forEach>
<!-- 다음 버튼 -->
<c:if test="${dto.next}">
<li class="page-item">
<a class="page-link" href="${dto.end + 1}">Next</a>
</li>
</c:if>
</ul>
화면 모습:
[Prev] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [Next]
↑
현재 페이지
⚡ JavaScript - 페이지 클릭 처리
페이지 링크를 클릭하면 검색 조건을 유지하면서 페이지를 이동합니다.
const pagingDiv = document.querySelector(".pagination");
pagingDiv.addEventListener("click", (e) => {
e.preventDefault(); // 기본 링크 동작 막기
e.stopPropagation(); // 이벤트 버블링 방지
const target = e.target;
const targetPage = target.getAttribute("href"); // 클릭한 페이지 번호
const size = ${dto.size} || 10;
// URL 파라미터 구성
const params = new URLSearchParams({
page: targetPage,
size: size
});
// 현재 검색 조건이 있으면 추가
const types = '${dto.types}';
const keyword = '${dto.keyword}';
if (types && types !== '' && keyword && keyword !== '') {
params.set("types", types);
params.set("keyword", keyword);
}
// 페이지 이동
window.location.href = '/board/list?' + params.toString();
});
flowchart LR
Click["3페이지 클릭"] --> Prevent["기본 동작 방지"]
Prevent --> Build["URL 파라미터 구성"]
Build --> Navigate["/board/list?page=3&size=10&types=T&keyword=스프링"]
🔍 JavaScript - 검색 버튼 처리
document.addEventListener("DOMContentLoaded", function() {
const searchBtn = document.querySelector(".searchBtn");
searchBtn.addEventListener("click", function(e) {
e.preventDefault();
// 입력값 가져오기
const keywordInput = document.querySelector("input[name='keywordInput']");
const typeSelect = document.querySelector("select[name='typeSelect']");
const keyword = keywordInput.value.trim();
const types = typeSelect.value;
// 검색 파라미터 구성
const params = new URLSearchParams({
page: '1', // 검색하면 항상 1페이지부터
size: '10'
});
// 검색 조건이 있을 때만 추가
if (types && types !== '' && keyword && keyword !== '') {
params.set("types", types);
params.set("keyword", keyword);
}
// 검색 실행
window.location.href = '/board/list?' + params.toString();
});
});
💡 검색 시 항상 1페이지: 새로운 검색 결과는 처음부터 보여야 하니까요!
🔄 검색 조건 유지 흐름
전체 흐름을 정리해볼게요:
flowchart TB
A["1. 사용자가 'Spring' 검색"] --> B["2. types=T, keyword=Spring 전송"]
B --> C["3. Controller가 Service 호출"]
C --> D["4. MyBatis 동적 SQL 실행"]
D --> E["5. JSP에 dto 전달"]
E --> F["6. 검색 결과 + 조건 표시"]
F --> G["7. 페이지 클릭 시 조건 유지"]
G --> B
📝 핵심 정리
| 기능 | 사용 기술 | 핵심 포인트 |
|---|---|---|
| 목록 출력 | <c:forEach> | dto.boardDTOList 반복 |
| XSS 방지 | <c:out> | HTML 이스케이프 |
| 검색 조건 유지 | selected 속성 | EL 삼항 연산자 |
| 페이지 이동 | JavaScript | URLSearchParams 사용 |
| 검색 실행 | JavaScript | 항상 page=1로 이동 |
🚀 다음 편 예고
**6편: 댓글 기능과 예외 처리 마스터하기**에서는:
- ReplyController REST API
- ReplyService 예외 처리
- 커스텀 예외 클래스
- AJAX 댓글 기능
을 알아볼 예정입니다!
📚 시리즈 목차
- [1편] 프로젝트 구조와 아키텍처 완벽 정리
- [2편] Spring MVC 컨트롤러와 요청 흐름 이해하기
- [3편] 서비스 계층과 비즈니스 로직 구현하기
- [4편] MyBatis와 데이터베이스 연동 완벽 가이드
- [5편] 페이징과 검색 기능 구현하기 ← 현재 글
- [6편] 댓글 기능과 예외 처리 마스터하기