1. 타임리프
1) 템플릿 엔진 개념
- 스프링 서버에서 데이터를 받아 우리가 보는 웹 페이지, 즉 HTML 상에 그 데이터를 넣어 보여주는 도구
ex)
<h1 text=${이름}>
<p text=${나이}>
- h1 태그에는 ${이름}이 text 어트리뷰트로 할당되어 있고, p 태그에는 ${나이}가 text 어트리뷰트로 할당되어 있다.
- 이렇게 해두면 서버에서 이름,나이라는 키로 데이터를 템플릿 엔진에 넘겨주고 템플릿 엔진은 이를 받아 HTML에 값을 적용한다.
ex) 서버에서 보여준 데이터 예
{
이름:"홍길동"
나이:11
}
- 값이 달라지면 그때그때 화면에 반영하여 동적인 웹 페이지를 만들 수 있다.
- 대표적인 템플릿 엔진으로는 JSP, 타임리프, 프리마커 등이 있다.
타임리프 표현식과 문법
- 표현식은 전달받은 데이터를 사용자들이 볼 수 있는 뷰로 만들기 위해 사용되는 표현식
타임리프 표현식
표현식 | 설명 |
${...} | 변수의 값 표현식 |
#{...} | 속성 파일 값 표현식 |
@{...} | URL 표현식 |
*{...} | 선택한 변수의 표현식. th:object에서 선택한 객체에 접근 |
타임리프 문법
표현식 | 설명 | 예제 |
th:text | 텍스트를 표현할 때 사용 | th:text=${person.name} |
th:each | 컬렉션을 반복할 때 사용 | th:each="person : ${persons}" |
th:if | 조건이 true일 때만 표시 | th:if="${person.age}>=20" |
th:unless | 조건이 false일 때만 표시 | th:unless="${person.age}>=20" |
th:href | 이동 경로 | th:href="@{/persons{id=${person.id})}" |
th:with | 변숫값으로 지정 | th:with="name=${person.name}" |
th:object | 선택한 객체로 지정 | th:object="${person}" |
2) 타임리프 사용을 위한 의존성 추가하기
- 타임리프를 사용하기 위해 build.gradle 파일에 의존성 추가
build.gradle
...
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
..
3) 타임리프 문법 익히기용 컨트롤러 작성
ExampleController.java
- /thymeleaf/example GET 요청이 오면 특정 데이터를 뷰, 즉 HTML로 넘겨주는 모델 객체에 추가하는 컨트롤러 메서드 작성
@Controller //컨트롤러라는 것을 명시적으로 표시
public class ExampleController {
@GetMapping("/thymeleaf/example")
public String thymeleafExample(Model model) { //뷰로 데이터를 넘겨주는 모델 객체
Person examplePerson = new Person();
examplePerson.setId(1L);
examplePerson.setName("홍길동");
examplePerson.setAge(11);
examplePerson.setHobbies(List.of("운동","도서"));
model.addAttribute("person", examplePerson); //Person 객체 저장
model.addAttribute("today", LocalDate.now());
return "example"; //example.html라는 뷰 조회
}
@Setter
@Getter
class Person {
private Long id;
private String name;
private int age;
private List<String> hobbies;
}
}
- 모델 객체는 뷰, 즉 HTML 쪽으로 값을 넘겨주는 객체, 모델 객체는 따로 생성할 필요 없이 코드처럼 인자로 선언하기만 하면 스프링이 알아서 만들어주므로 편리하게 사용 가능
- addAttribute() 메서드로 모델에 값을 저장. 여기서는 "person"이라는 키에 사람 정보를, "today"라는 키에 날짜 정보를 저장
- thymeleafExample() 메서드가 반환하는 값은 "example"이다. 이 값은 클래스에 붙은 애너테이션인 @Controller이므로 뷰의 이름을 반환하는 것이다. 즉, 스프링 부트는 컨트롤러의 @Controller 애너테이션을 보고 반환하는 값의 이름을 가진 파일을 찾으라는 것이라고 이해하고 resource/templates 디렉터리에서 example.html을 찾은 다음 웹 브라우저에서 해당 파일을 보여준다.
4) 뷰 작성하기
resources/templates/example.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>타임리프 익히기</h1>
<p th:text="${#temporals.format(today, 'yyyy-MM-dd')}"></p>
<div th:object="${person}">
<p th:text="|이름 : *{name}|"></p>
<p th:text="|나이 : *{age}|"></p>
<p>취미</p>
<ul th:each="hobby : *{hobbies}">
<li th:text="${hobby}"></li>
<span th:if="${hobby == '운동'}">(대표 취미)</span>
</ul>
</div>
<a th:href="@{/api/articles/{id}(id=${person.id})}">글 보기</a>
</body>
</html>
- #temporals.format() 함수로 LocalDate 타입인 오늘 날짜를 yyyy-MM-dd 형식의 String 타입으로 포매팅한다.
- th:object를 사용해 모델에서 받은 객체 중 "person"이라는 키를 가진 객체의 데이터를 하위 태그에 지정한다.
- 하위 태그에서는 *{...}를 사용해 부모 태그에 적용한 객체 값에 접근할 수 있다.
- th:text는 텍스트를 표현한다.
- th:each는 객체의 hobbies 개수만큼 반복하는 반복자이다.
2. 블로그 글 목록 뷰 구현하기
1) 컨트롤러 메서드 작성하기
- 요청을 받아 사용자에게 뷰를 보여주려면 뷰 컨트롤러가 필요하다.
- 뷰 컨트롤러 메서드는 뷰의 이름을 반환하고, 모델 객체에 값을 답는다.
dto/ArticleListViewResponse.java
@Getter
public class ArticleListViewResponse {
private final Long id;
private final String title;
private final String content;
public ArticleListViewResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
}
}
controller/BlogViewController.java
- /articles GET 요청을 처리할 코드를 작성
- 블로그 글 전체 리스트를 담은 뷰를 반환
@RequiredArgsConstructor
@Controller
public class BlogViewController {
private final BlogService blogService;
@GetMapping("/articles")
public String getArticles(Model model) {
List<ArticleListViewResponse> articles = blogService.findAll()
.stream()
.map(ArticleListViewResponse::new)
.toList();
model.addAttribute("articles", articles); //1.블로그 글 리스트 저장
return "articleList"; //2. articleList.html라는 뷰 조회
}
}
- addAttribute() 메서드를 사용해 모델에 값을 저장한다. 여기서는 "articles" 키에 블로그 글 리스트를 저장한다.
- 반환값인 "articleList"는 resource/templates/articleList.html을 찾도록 뷰의 이름을 적은 것이다.
2) HTML 뷰 만들고 테스트 하기
templates/articleList.html
- 모델에 전달한 블로그 글 리스트 개수만큼 반복해 글 보여주기
- th:each로 "articles"키에 담긴 데이터 개수만큼 반복
- th:text는 반복 대상 객체의 id, "text"를 출력
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글 목록</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container">
<div class="row-6" th:each="item:${articles}">
<div class="card">
<div class="card-header" th:text="${item.id}">
</div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-text" th:text="${item.content}"></p>
<a href="#" class="btn btn-primary">보러가기</a>
</div>
</div>
</div>
</div>
</body>
3. 블로그 글 뷰 구현하기
1) 엔티티에 생성, 수정 시간 추가하기
Article.java
...
@CreatedDate //엔티티가 생성될 때 생성시간 저장
@Column(name="created_at")
private LocalDateTime createdAt;
@LastModifiedDate //엔티티가 수정될 때 수정 시간 저장
@Column(name="updated_at")
private LocalDateTime updatedAt;
- @CreatedDate 애너테이션을 사용하면 엔티티가 생성될 때 생성 시간을 created_at 컬럼에 저장한다.
- @LastModifiedDate 애너테이션을 사용하면 엔티티가 수정될 때 마지막으로 수정된 시간을 updated_at 컬럼에 저장한다.
- 또한 엔티티의 생성 및 수정 시간으로 자동으로 감시하고 기록하기 위해@EntityListeners(AuditingEntityListener.class)에너테이션을 추가해준다.
- SpringBootDeveloperApplication.java 파일을 열어 엔티티의 created_at, updated_at을 자동으로 업데이트하기 위한 애너테이션을 추가
@EnableJpaAuditing
@SpringBootApplication
public class SpringBootDeveloperApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDeveloperApplication.class, args);
}
}
2) 컨트롤러 메서드 작성하기
dto/ArticleViewResponse.java
@NoArgsConstructor
@Getter
public class ArticleViewResponse {
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;
public ArticleViewResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
this.createdAt = article.getCreatedAt();
}
}
- 블로그 글을 반환할 컨트롤러의 메서드 작성
- BlogViewController.java 파일을 열어 getArticle() 메서드를 추가
BlogViewController.java
...
@GetMapping("/articles/{id}")
public String getArticle(@PathVariable Long id, Model model) {
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
return "article";
}
- getArticle() 메서드는 인자 id에 URL로 넘어온 값을 받아 findById() 메서드로 넘겨 글을 조회하고, 화면에서 사용할 모델에 데이터를 저장한 다음, 보여줄 화면의 템플릿 이름을 반환
3) HTML 뷰 만들기
resource/templates/article.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<input type="hidden" id="article-id" th:value="${article.id}">
<header class="mb-4">
<h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
<div class="text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyyy-MM-dd HH:mm')}|"></div>
</header>
<section class="mb-5">
<p class="fs-5 mb-4" th:text="${article.content}"></p>
</section>
<button type="button" id="modify-btn"
th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
class="btn btn-primary btn-sm">수정</button>
<button type="button" id="delete-btn"
class="btn btn-secondary btn-sm">삭제</button>
</article>
</div>
</div>
</div>
</body>
articleList.html
- 글 리스트 화면에 있는 [보러 가기] 버튼을 수정
- href 속성을 th:href 속성으로 변경하고 URL 표현식 @{...}을 사용해 [보러가기]를 눌렀을 때 주소창의 값을 /articles/{item.id}로 변경해 글 상세 화면으로 이동
...
<a th:href="@{/articles/{id}(id=${item.id})}"
class="btn btn-primary">보러가기</a>
...
4. 삭제 기능 추가하기
1) 삭제 기능 코드 작성하기
resource/static/article.js
//삭제 기능
const deleteButton = document.getElementById('delete-btn');
if(deleteButton) {
deleteButton.addEventListener('click', event => {
let id = document.getElementById('article-id').value;
fetch(`/api/articles/${id}`, {
method:'DELETE'
})
.then(() => {
alert('삭제가 완료되었습니다.');
location.replace('/articles');
});
});
}
- HTML에서 id를 delete-btn으로 설정한 엘리먼트를 찾아 그 엘리먼트에서 클릭 이벤트가 발생하면 fetch() 메서드를 통해 /api/articles/DELETE 요청을 보내는 역할을 한다.
- then() 메서드는 fetch()가 잘 완료되면 연이어 실행되는 메서드이다.
- alert() 메서드는 then() 메서드가 실행되는 시점에 웹 브라우저 화면으로 삭제가 완료되었음을 알리는 팝업을 띄어주는 메서드이다.
- location.replace() 메서드는 실행 시 사용자의 웹 브라우저 화면을 현재 주소를 기반해 옮겨주는 역할을 한다.
5. 수정/생성 기능 추가하기
- 블로그 글 수정과 생성은 같은 화면에서 벌어진다.
1) 수정/생성 뷰 컨트롤러 작성하기
- 글을 생성할 때는 URL에 별도 쿼리 파라미터가 없다. 하지만 수정할 때는 URL에 ?id=123과 같이 수정할 글의 id를 쿼리 파라미터에 추가해 요청한다.
- 쿼리 파라미터란 HTTP 요청에서 URL 끝에 '?'로 시작하는 키 값으로 이루어진 문자열이며 '&'로 구분한다.
- 쿼리 파라미터가 있는 경우 컨트롤러 메서드를 수정해야 하므로 엔티티를 조회해 기존 글 데이터를 모델에 넣어 화면에 보여줘야 한다. 쿼리 파라미터가 없을 때에는 새 글이므로 화면에 아무것도 보여줄 필요 없다.
- 또한 뷰에서는 쿼리 파라미터의 id 여부에 따라 [수정]과 [생성] 적절한 버튼을 보여줘야 한다.
BlogViewController.java
- newArticle() 메서드 추가
...
@GetMapping("/new-article")
//id 키를 가진 쿼리 파라미터의 값을 id 변수에 매핑(id는 없을 수도 있음)
public String newArticle(@RequestParam(required=false) Long id, Model model) {
if(id==null) {
//id가 없으면 생성
model.addAttribute("article", new ArticleViewResponse());
}else {
//id가 있으면 수정
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
}
return "newArticle";
}
- 쿼리 파라미터로 넘어온 id값은 newArticle() 메서드의 Long 타입 id 인자에 매핑한다.
- id가 있으면 수정, 없으면 생성이므로 id가 없는 경우 기본 생성자를 이용해 빈 ArticleViewResponse 객체를 만들고, id가 있으면 기존 값을 가져오는 findById() 메서드를 호출
2) 수정/생성 뷰 만들기
newArticle.html
- 수정할 때는 id가 필요하므로 input 엘리먼트의 type을 hidden으로 설정해 엘리먼트를 숨긴다.
- th:value로 글의 id를 저장한다.
- th:if로는 id가 있을 때 [수정] 버튼, 없을 때 [등록] 버튼이 나타나도록 한다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>블로그 글 </title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h1 class="mb-3">My Blog</h1>
<h4 class="mb-3">블로그에 오신 것을 환영합니다.</h4>
</div>
<div class="container mt-5">
<div class="row">
<article>
<!--아이디 정보 저장 -->
<input type="hidden" id="article-id" th:value="{article.id}">
<header class="mb-4">
<input type="text" class="form-control" placeholder="제목" id="title" th:value="${article.title}">
</header>
<section class="mb-5">
<textarea class="form-control h-25" rows="10" placeholder="내용" id="content" th:text="${article.content}"></textarea>
</section>
<button th:if="${article.id} != null" type="button" id="modify-btn" class="btn btn-primary btn-sm">수정</button>
<button th:if="${article.id} == null" type="button" id="create-btn" class="btn btn-primary btn-sm">등록</button>
</article>
</div>
</div>
<script src="js/article.js"></script>
</body>
</html>
article.js
- 실제 수정, 생성 기능을 위한 API 구현
// 수정 기능
const modifyButton = document.getElementById('modify-btn');
if (modifyButton) {
modifyButton.addEventListener('click', event => {
let params = new URLSearchParams(location.search);
let id = params.get('id');
fetch(`/api/articles/${id}`, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('수정이 완료되었습니다.');
location.replace(`/articles/${id}`);
});
});
}
- id가 modify-btn인 엘리먼트를 찾고 그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져와 fetch() 메서드를 통해 수정 API로 /api/articles/ PUT 요청을 보낸다.
- 요청을 보낼 때는 headers에 요청 형식을 지정하고, body에 HTML에 입력한 데이터를 JSON 형식으로 바꿔 보낸다.
article.html
- [수정] 버튼에 id값과 클릭 이벤트 추가
...
<button type="button" id="modify-btn"
th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|"
class="btn btn-primary btn-sm">수정</button>
...
6. 생성 기능 마무리하기
1) 생성 기능 작성하기
article.js
- [등록] 버튼을 누르면 입력 칸에 있는 데이터를 가져와 게시글 생성 API에 글 생성 관련 요청을 보내주는 코드를 추가
// 생성 기능
const createButton = document.getElementById('create-btn');
if (createButton) {
createButton.addEventListener('click', event => {
fetch('/api/articles', {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value
})
})
.then(() => {
alert('등록 완료되었습니다.');
location.replace('/articles');
});
});
- id가 create-btn인 엘리먼트를 찾아 그 엘리먼트에서 클릭 이벤트가 발생하면 id가 title, content인 엘리먼트의 값을 가져와 fetch() 메서드를 통해 생성 API로 /api/articles/POST 요청을 보내준다.
articleList.html
- id가 create-btn인 [생성] 버튼을 추가
...
<button type="button" id="create-btn"
th:onclick="|location.href='@{/new-article}'|"
class="btn btn-secondary btn-sm mb-3">글 등록</button>
...
'SpringBoot' 카테고리의 다른 글
[스프링 부트 3 백엔드 개발자 되기] ch 9. JWT로 로그인/로그아웃 구현하기 (1) | 2024.07.24 |
---|---|
[스프링 부트 3 백엔드 개발자 되기] ch 8. 스프링 시큐리티로 로그인/로그아웃, 회원가입 구현하기 (28) | 2024.07.19 |
[스프링 부트 3 백엔드 개발자 되기] ch 6. 블로그 기획하고 API 만들기 (0) | 2024.07.17 |
[스프링 부트 3 백엔드 개발자 되기] ch 5. 데이터베이스 조작이 편해지는 ORM (0) | 2024.07.12 |
[스프링 부트 3 백엔드 개발자 되기] ch 4. 스프링 부트 3과 테스트 (0) | 2024.07.11 |