
이 글은 2026년 04월 30일 작성된 글입니다.
오늘은 답변 등록 기능, Bootstrap 화면 구성,
질문 등록 기능과 validation 처리까지 정리했다.
질문 상세 페이지에서 답변을 입력할 수 있도록 폼을 추가했다.
<form action="/answer/create/" method="post">
<textarea name="content" id="content" rows="15"></textarea>
<input type="submit" value="답변등록">
</form>
처음에는 답변 내용만 서버로 보내는 구조로 시작했다.
답변은 특정 질문에 달려야 하므로,
질문 id를 URL에 포함해서 전달하도록 수정했다.
<form th:action="@{|/answer/create/${question.id}|}" method="POST">
<textarea name="content" cols="30" rows="10"></textarea>
<input type="submit" value="답변등록">
</form>
이렇게 하면 서버는 어떤 질문에 대한 답변인지 알 수 있다.
질문 상세 페이지에서 해당 질문에 달린 답변 목록을 출력했다.
<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<ul>
<li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
</ul>
question.answerList를 사용하면
질문에 연결된 답변들을 화면에 출력할 수 있다.
기본 HTML 화면에 Bootstrap을 적용해서
화면을 더 보기 좋게 구성했다.
<input type="submit" value="답변등록" class="btn btn-primary">
Bootstrap을 사용하면 직접 CSS를 많이 작성하지 않아도
버튼, 폼, 여백 등을 빠르게 정리할 수 있다.
페이지 구조를 더 명확하게 만들기 위해
표준 HTML 구조로 변경했다.
<html>
<head>
<title>질문 게시판</title>
</head>
<body>
<main>
<!-- content -->
</main>
</body>
</html>
기본 구조를 잡아두면
이후 공통 레이아웃을 적용하기도 쉬워진다.
새 질문을 작성할 수 있도록 질문 등록 폼을 만들었다.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form th:action="@{/question/create}" method="post">
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" name="subject" id="subject" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
질문 등록 폼에서는 제목과 내용을 입력받는다.
폼에서 전달된 제목과 내용을 받아
질문 데이터를 저장하도록 구현했다.
흐름은 다음과 같다.
질문 등록 시 빈 값이 들어오지 않도록 validation을 적용했다.
implementation("org.springframework.boot:spring-boot-starter-validation")
validation을 적용하면
폼 데이터가 서버에 들어온 뒤 조건에 맞는지 검사할 수 있다.
폼에서 에러가 발생했을 때
사용자에게 에러 메시지를 보여주도록 처리했다.
<form th:action="@{/question/create}" th:object="${questionForm}" method="post">
<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"></div>
</div>
<input type="text" th:field="*{subject}" class="form-control">
<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
th:object와 th:field를 사용하면
폼 객체와 입력 필드를 자연스럽게 연결할 수 있다.
답변 등록 폼에도 validation을 적용했다.
<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"></div>
</div>
<textarea th:field="*{content}" rows="10" class="form-control"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
질문 등록과 답변 등록 모두
폼 객체를 기준으로 유효성 검사를 처리하는 구조가 되었다.
여러 화면에서 반복되는 레이아웃을 공통 템플릿으로 분리했다.
<html layout:decorate="~{layout}">
<div layout:fragment="content">
<!-- 페이지별 내용 -->
</div>
</html>
공통 레이아웃을 사용하면
전체 페이지 구조를 일관성 있게 유지할 수 있다.
th:action, th:object, th:field를 사용하면 폼 처리가 훨씬 깔끔해진다.