백엔드/Spring Boot

7. 블로그 화면 구성하기 (1) - 타임리프

두개의 문 2023. 8. 2. 08:43

◉  사전지식 : 타임리프 

 

 - 타임리프 : 템플릿 엔진

 - 템플릿 엔진 : 스프링 서버에서 데이터를 받아 우리가 보는 웹 페이지 즉, HTML 상에 그 데이터를 넣어 보여주는 도구 

    단, 템플릿 엔진의 경우, HTML과 함께 템플릿 엔진을 위한 문법을 섞어 사용해야 함 

 

 

 - 템플릿 엔진(Template Engine)

  • 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어로, 웹 사이트를 어떤 형태로 만들지 도와주는 양식 

  • 서버 사이드 템플릿 엔진과 클라이언트 사이드 템플릿 엔진으로 분류됨 

 

   ① 서버 사이드 템플릿 엔진 (Server Side Template Engine)

      - 서버에서 DB 혹은 API에서 가져온 데이터를 미리 정의된 Template에 넣어 HTML를 그려서 클라이언트에 전달해주는 역할 

      - 대표적인 서버 사이드 템플릿 엔진 : Thymeleaf, JSP, Freemarker

 

   ② 클라이언트 사이드 템플릿 엔진 (Client Side Template)

      - HTML 형태로 코드를 작성할 수 있으며, 동적으로 DOM를 그리게 해주는 역할 

      - 데이터를 받아서 DOM 객체에 동적으로 그려주는 프로세스를 담당 

      - 대표적인 클라이언트 사이드 템플릿 엔진 : Mustache, Squirrelly, Handlebars

 

    • 보통 Java에서 웹 개발 시 JSP를 이용하여 진행 

       JSP 사용 시, <% %> 형태의 스크립트릿을 사용하여 개발함 → 스크립트릿의 경우, HTML과 혼재되어 유지 보수의 어려움이 있음

       따라서, Spring boot에서는 JSP가 아닌 Thymeleaf 사용을 권장함  

 

 


1. 템플릿 엔진 개념 잡기 

 

 - 간단한 템플릿 문법을 위한 예

 <h1 text=${ 이름 } >
 <p text=${ 나이 } >

 

 

 - 서버에서 보내준 데이터 예

    : 서버에서 이름, 나이라는 키로 데이터를 템플릿 엔진에 넘겨주면, 템플릿 엔진은 HTML에 값을 적용함 

 {
	이름 : “구름”
	나이 : 10
 }

  • 값이 달라질 경우, 그때 그때 화면에 반영되므로 동적인 페이지가 생성 가능 

 

 

 - 타임리프 표현식과 문법 

  ▪︎ 표현식 : 전달받은 데이터를 사용자들이 볼 수 있는 뷰로 만들기 위해 사용됨 

표현식 설명
${ ... } 변수의 값 표현식
#{ ... } 속성 파일 값 표현식 
@{ ... } 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="@{ /person(id=${ person.id })}"
th:with 변수값으로 지정  th:with="name=${ person.name }"
th:object 선택한 객체로 지정  th:object="${ person }" 

 

 


 ※ 뷰 페이지를 작업할 때 사용하면 좋은 확장프로그램 : LiveReload++

    - 이클립스에서 작업한 내용 저장 시, 웹 브라우저가 자동으로 갱신되도록 해주는 플러그인 

    - 서버 실행 후, 확장 프로그램 클릭하여 새로고침해주면 자동으로 실행됨 

 


2. 타임리프 사용을 위한 의존성 추가하기 

 

 1) 새로운 Spring Starter Project 생성 : HiThymeleaf

 

 

 - 프로젝트 생성 시, 의존성 추가 

 

 

 

 

 2) 타임리프 사용을 위한 의존성 추가하기 

  - build.gradle 파일에 의존성 추가 → Snippets에 업데이트해놓기 

     ※ build.gradle 파일 수정 후, 반드시 Refresh Gradle Project 해주어야 반영됨 

dependencies {
	// * MVC 개발 라이브러리 
	// MVC 환경 개발에 필요한 필수 라이브러리 
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// 타임리프를 뷰 템플릿으로 사용 시 필요 
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

	// * 데이터베이스를 다루는 기술 ( JPA / MyBatis / JDBC TEMPLATE )
	// JDBC Template 방식 기술로 개발할 때 필요 
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	// JPA 방식 기술로 개발 시 필요 
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	// 순수 JDBC 기술로 개발 시 필요 
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	// 한국에서 가장 많이 사용하고 있는 데이터베이스 기술 
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
	
	// * 실행 시 필요한 데이터베이스 JDBC 드라이버 
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'com.mysql:mysql-connector-j'
	runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

	// * 개발할 때 소스 자동 생성 실행 시 필요한 라이브러리 
	annotationProcessor 'org.projectlombok:lombok'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	
	// * 테스트 라이브러리 
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2'
}

 

 

 

 

 3) 타임리프 문법 익히기용 컨트롤러 작성하기 

  - '/thymeleaf/example' GET 요청이 오면, 특정 데이터를 HTML로 넘겨주는 모델 객체에 추가하는 컨트롤러 메서드 작성 

  

 

 ▪︎ ExampleController.java ( controller패키지 )

package org.choongang.ewha.hithymeleaf.controller;

import java.time.LocalDate;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.Getter;
import lombok.Setter;

@Controller		// 컨트롤러라는 것을 명시적으로 표시 
public class ExampleController {
	@GetMapping("/thymeleaf/example")
	public String thymeleafExample(Model model) {
		// * Model 객체 : HTML 쪽으로 값을 넘겨주는 객체 
		//   -> 따로 생성할 필요 없이 인자로 선언하기만 하면 스프링이 알아서 만들어줌 
		Person examplePerson = new Person();
		examplePerson.setId(1L);
		examplePerson.setName("홍길동");
		examplePerson.setAge(11);
		examplePerson.setHobbies(List.of("운동", "독서"));
		// * List의 of() 메서드 : 변경불가한 list 객체를 생성 
		//   -> List.of()로 생성된 list의 각 요소는 변경 불가능 
		//      변경할 경우, UnsupportedOperationException 발생 
		
		model.addAttribute("person", examplePerson);	// person 객체 저장 
		model.addAttribute("today", LocalDate.now());
		
		return "example";		// example.html 뷰 조회 
	}
	
	// * 내부클래스 : Controller 클래스 내에 Person 클래스 존재 
	//   -> has a 관계 
	@Setter
	@Getter
	class Person{
		private Long id;
		private String name;
		private int age;
		private List<String> hobbies;
	}
}

 

• Model

  - HTML 쪽으로 값을 넘겨주는 객체 
    → 따로 생성할 필요 없이 인자로 선언하기만 하면 스프링이 알아서 만들어줌 

  - org.springframework.ui.Model ( MVC 패턴의 Model 아님 )

  - UI용 화면과 화면 사이 또는 화면과 controller 사이에 임시 데이터를 저장하기 위한 버퍼나 캐쉬같은 임시 메모리 공간 

 

 

• addAttribute( ) 메서드

   - Model 객체에 값을 저장

 

 

• List의 of() 메서드

   - 변경불가한 list 객체를 생성 
      → List.of()로 생성된 list의 각 요소는 변경 불가능 
          변경할 경우, UnsupportedOperationException 발생 

 

 

• @Controller

  - 해당 클래스가 '컨트롤러'임을 명시하는 목적 

  - 스프링 부트가 컨트롤러의 이 애너테이션을 보고, resource/templates 디렉토리에서 '반환하는 값의 이름을 가진 뷰의 파일'을 찾은 다음, 웹 브라우저에서 해당 파일을 보여줌 

     

 

• 모델의 역할 : 컨트롤러와 뷰의 중간다리 역할 

  - 예제에 따르면, 현재 이 모델의 경우 두 종류의 키('person', 'today')를 가진 데이터가 저장됨 

  - 컨트롤러 : model.addAttribute( ) 메서드를 통해 모델에 데이터 설정 

                     → 뷰에서 사용할 수 있게 데이터 전달 

     

 

 

 

 4) 뷰 작성하기 

 

▪︎ Example.html ( src/main/resources/templates )

<!DOCTYPE html>
<html xmlns="http://www.thymeleaf.org">
// * 타임리프 사용 선언 
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>타임리프 익히기</h1>
<!-- LocalDate를 yyyy-MM-dd 포맷으로 변경 -->
<p th:text="${#temporals.format(today,'yyyy-MM-dd')}"></p>
<div th:object="${ person }">	
<!-- 
	# person을 선택한 객체로 지정 
	  - 멤버변수를 여러 개 가진 자료의 경우, 출력 시 객체명을 매번 붙여주어야 함 
	  - 태그 방식에서는 보기 불편함
	  -> 타임리프의 경우, 객체는 한 번만 th:object에 대입하여 하위 태그에 지정 
      	  -> 하위 태그에서는 *{ ... }를 사용해 해당 객체에 접근 
      
	# th:object 속성의 범위 : 시작태그<>와 끝태그</ >의 사이
	  -> 일종의 객체의 루프와 유사한 느낌이라고 생각하면 됨
						       
-->
	<p th:text="|이름 : *{ name }|"></p>
	<p th:text="|나이 : *{ age }|"></p>
	<p>취미</p>
	<!--  List<String> hobbies : th:each(컬렉션을 반복할 때 이용)  -->
	<ul th:each="hobby : *{ hobbies }">
		<li th:text="${ hobby }"></li>
		<!-- 반복 대상이 운동이라면, '대표 취미'라는 표시 추가  -->
		<span th:if="${ hobby == '운동' }">(대표 취미)</span>
	</ul>
</div>
<!--  1번 블로그 글을 보러 이동 : 네비게이션 메뉴(<a>태그) 만들기 -->
<a th:href="@{ /api/articles/{id}(id=${person.id})}">글 보기</a>
</body>
</html>

• #temporals.format( ) 메서드 

  - LocalDate 타입인 오늘 날짜를 'yyyy-MM-dd' 형식의 String 타입으로 포매팅 

 

• th:object - 선택한 객체로 지정 

   *{         }   - 선택한 변수의 표현식
                       th:object에서 선택한 객체에 접근 

th:object를 사용해 모델에서 받은 객체 중 "person"이라는 키를 가진 객체의 데이터를 하위태그에 지정 

→ 하위태그에서는 *{ ... }를 사용해 부모 태그에 적용한 객체 값에 접근 가능 

 : person.name의 경우, *{ name }와 동일한 의미 ( 코드의 반복을 줄여줌 ) 

 

 

 

 

 5) 뷰 테스트하기 

 

 ▪︎ 실행결과