본문 바로가기
Web/SpringBoot

4. 머스테치로 화면 구성

by SeleniumBindingProtein 2022. 2. 28.
728x90
반응형

1. 서버 템플릿 엔진과 머스테치

 1) 템플릿 엔진 : 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어

  - 서버 템플릿 엔전 : Spring+JSP, Freemarker 등

    => 서버 템플릿 엔진을 이용한 화면 생성은 서버에서 Java 코드로 문자열을  만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달

  - 클라이언트 템플릿 엔진 : 리액트, vue의 view 등

    => vue.js나 react.js를 이용한 SPA는 브라우저에서 화면을 생성하며, 서버에서 이미 코드가 벗어난 경우

 

 2) 머스테치 : 수많은 언어를 지원하는 가장 심플한 템플릿 엔진 

  - 루비, 자바스크립트, 파이썬, PHP, 자바, 펄, Go, ASP 등 현존하는 대부분 언어를 지원하고 있음 

  - 자바에서 사용될 때 서버 템플릿 엔진으로, 자바스크립트에서 사용될 때는 클라이언트 템플릿 엔진으로 모두 사용 가능함

 

3) 템플릿 엔진 장단점

  - JSP, Velocity : 스프링부트에서는 권장하지 않는 템플릿 엔진

  - Freemarker : 템플릿 엔진으로는 너무 과하게 많은 기능을 지원하며,

                            높은 자유도로 인해 숙련도가 낮을수록 Freemarker 안에 비즈니스 로직이 추가될 확률이 높음

  - Thymeleaf : 스프링 진영에서 적극적으로 밀고 있지만 문법이 어려움

  - mustache : 문법이 다른 템플릿 엔진보다 심플하며, 로직 코드를 사용할 수 없어 View의 역할과 서버의 역할이 명확하게 분리됨

                         Mustache.js와 Mustache.java 2가지가 다 있어, 하나의 문법으로 클라이언트/서버 템플릿을 모두 사용 가능함 

                         인텔리제이 커뮤니티 버전을 사용해도 플러그인을 사용할 수 있음

       

2. 머스테치 플러그인 설치

 

3. 기본 페이지 작성

    - build.gradle

implementaion 'org.springframework.boot:spring-boot-starter-mustache'

    - src/main/resources/templates/index.mustache

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹 서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>

</head>
    <body>
        <h1>스프링부트로 시작하는 웹서비스</h1>
    </body>
</html>

   - 머스테치에 URL을 매핑하고, Controller에서 진행함 

   - IndexController.java

   - index을 반환하므로, src/main/resources/templates/index.mustache로 전환되어 View Resolver가 처리하게 됨

   - ViewResolver는 URL 요청의 결과를 전달할 타입과 값을 지정하는 관리자 격으로 볼 수 있음

package com.jojoldu.book.springboot.web;

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

@Controller
public class IndexController {
    @GetMapping("/")
    public String index(){
        return "index";
    }
}

   - IndexControllerTest.java

package com.jojoldu.book.springboot.web;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void 메인페이지_로딩(){
        //when
        String body = this.restTemplate.getForObject("/", String.class);

        //then
        assertThat(body).contains("스프링부트로 시작하는 웹 서비스");
    }
}

 

4. 게시글 등록 화면 생성

 1) 부트스트랩, 제이쿼리 등 프론트엔드 라이브러리를 사용할 수 있는 방법은 크게 2가지가 있음

    - 외부 CDN을 사용하는 것, 직접 라이브러리를 받아서 사용하는 방법

    - 레이아웃 방식 : 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식

    - src/main/resources/templates/layout/header.mustache

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹 서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
    <link rel="stylesheet" href="http://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>

    - src/main/resources/templates/layout/footer.mustache

<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="http://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>

    - src/main/resources/templates/index.mustache

      =>{{>layout/header}} : 현재 머스테치 파일(index.mustache)을 기준으로 다른파일을 가져옴

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹 서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
    <body>
    {{>layout/header}}
        <h1>스프링부트로 시작하는 웹서비스</h1>
        <div class="col-md-12">
            <div class="row">
                <div class="col-md-6">
                    <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
                </div>
            </div>
        </div>
    {{>layout/footer}}
    </body>
</html>

[게시글 등록 화면]

    - 등록 버튼 기능 추가

    - src/main/resources/static/js/app/index.js

     => window.location.href='/' 글 등록이 성공하면 메인페이지(/)로 이동함

     => 중복함수 이름이 발생할 경우가 있으므로, index.js만의 유효범위를 만들어 사용함

     => 방법은 var index란 객체를 만들어 해당 객체에서 필요한 모든 function을 선언하는 것

           (index 객체 안에서만 function이 유요하기 때문에 다른 js와 겹칠 위험이 사라짐)

var main = {
    init : function (){
        var _this = this;
        $('#btn-save').on('click', function (){
            _this.save();
        });
    },
    save : function (){
        var data = {
            title: $('#title').val(),
            author: $('#author').val(),
            content: $('#content').val()
        };

        $.ajax({
            type: 'POST',
            url: '/api/v1/posts',
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify(data)
        }).done(function (){
            alert("글이 등록되었습니다.");
            window.location.href='/';
        }).fail(function (error){
            alert(JSON.stringify(error));
        });
    }
};

main.init();

    - src/main/resources/templates/layout/footer.mustache

 

<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="http://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

<!-- index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>

[등록 버튼 클릭시 결과 화면]

[등록 데이터베이스 확인]

 

5. 전체 조회 화면 생성

    - src/main/resources/templates/index.mustache

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹 서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
    <body>
    {{>layout/header}}
        <h1>스프링부트로 시작하는 웹서비스</h1>
        <div class="col-md-12">
            <div class="row">
                <div class="col-md-6">
                    <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
                </div>
            </div>
            <br>
<!--           목록 출력 영역-->
            <table class="table table-horizontal table-bordered">
                <thead class="thread-strong">
                <tr>
                    <th>게시글 번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>최종수정일</th>
                </tr>
                </thead>
                <tbody id="tbody">
                {{#posts}} <!-- posts라는 List를 순회함 (for문과 비슷함) -->
                    <tr>
                        <td>{{id}}</td>  <!-- List에서 뽑아낸 객체의 필드를 사용함함-->
                        <td>{{title}}</td>
                        <td>{{author}}</td>
                        <td>{{modifiedDate}}</td>
                    </tr>
                {{/posts}}
                </tbody>
            </table>
        </div>
    {{>layout/footer}}
    </body>
</html>

    - PostsRepository.java

    - SpringDataJap에서 제공하지 않는 메소드는 아래처럼 쿼리로 작성하기 위해 @Query를 사용하였음

package com.jojoldu.book.springboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface PostsRepository extends JpaRepository<Posts, Long> {
    @Query("SELECT p From Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
}

    - PostsService.java 에서 추가

@Transactional(readOnly = true) //트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선됨
public List<PostsListResponseDto> findAllDesc(){
    return postsRepository.findAllDesc().stream().map(PostsListResponseDto::new).collect(Collectors.toList());
}

 

    - PostsListResponseDto.java

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostsListResponseDto {
    private Long id;
    private String title;
    private String author;
    private LocalDateTime modifiedDate;
    
    public PostsListResponseDto(Posts entity){
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.author = entity.getAuthor();
        this.modifiedDate = entity.getModifiedDate();
    }
}

    - IndexController.java

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@RequiredArgsConstructor
@Controller
public class IndexController {

    private  final PostsService postsService;
    
    @GetMapping("/")
    public String index(Model model){	//Model : 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있음
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
    }

    @GetMapping("/posts/save")
    public String postsSave(){
        return "posts-save";
    }
}

[조회 목록]

 

6. 게시글 수정

    - src/main/resources/templates/posts-update.mustache

{{>layout/header}}
<h1>게시글 등록</h1>
<div class="col-md-12">
    <div class="col-md-4">
        <form>
            <div class="form-group">
                <label for="title">글 번호</label>	//{{post.id}} : 머스테치는 객체 필드 접근 시 dot으로 구분함
                <input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
            </div>																// readonly 읽기만 허용
            <div class="form-group">
                <label for="title">제목</label>
                <input type="text" class="form-control" id="title" value="{{post.title}}">
            </div>
            <div class="form-group">
                <label for="author"> 작성자</label>
                <input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
            </div>
            <div class="form-group">
                <label for="content"> 내용</label>
                <textarea type="text" class="form-control" id="content">{{post.content}}"</textarea>
            </div>
        </form>
        <a href="/" role="button" class="btn btn-secondary">취소</a>
        <button type="button" class="btn btn-primary" id="btn-update">수정완료</button>
    </div>
</div>
{{>layout/footer}}

    - index.js 에서 수정 관련된 자바스크립트 추가

var main = {
    init : function (){
        var _this = this;
        $('#btn-save').on('click', function (){
            _this.save();
        });

        $('#btn-update').on('click', function (){
            _this.update();
        });
    },
    save : function (){
        var data = {
            title: $('#title').val(),
            author: $('#author').val(),
            content: $('#content').val()
        };

        $.ajax({
            type: 'POST',
            url: '/api/v1/posts',
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify(data)
        }).done(function (){
            alert("글이 등록되었습니다.");
            window.location.href='/';
        }).fail(function (error){
            alert(JSON.stringify(error));
        });
    },

    update : function (){
        var data = {
            title: $('#title').val(),
            content: $('#content').val()
        };
        var id = $('#id').val();

        $.ajax({
            type: 'PUT',
            url: '/api/v1/posts/'+id,
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify(data)
        }).done(function (){
            alert("글이 수정되었습니다.");
            window.location.href='/';
        }).fail(function (error){
            alert(JSON.stringify(error));
        });
    }
};

main.init();

    - $('#btn-update').on('click') : btn-update란 id를 가진 HTML 요소에 click 이벤트가 발생할 때,

                                                          update function을 실행하도록 이벤트를 등록함

    - update : function() : 신규로 추가될 update function

    - type:'PUT' : 여러 HTTP Method 중 PUT 메서드를 선택함 

                            PostsApiController에 있는 API에서 이미 @PutMapping으로 선언했기 때문에 Put 사용해야 함

    - create : POST, read : GET, update : PUT, delete : DELETE

    - url: '/api/v1/posts/' + id : 어느 게시글을 수정할 지 URL Path로 구분하기 위해 Path에 id를 추가함 

 

    - index.musache에서 제목 클릭시 수정 페이지로 이동을 위해 a태그 추가

<tbody id="tbody">
{{#posts}} <!-- posts라는 List를 순회함 (for문과 비슷함) -->
    <tr>
        <td>{{id}}</td>  <!-- List에서 뽑아낸 객체의 필드를 사용함함-->
        <td><a href="/posts/update/{{id}}">{{title}}</a></td>
        <td>{{author}}</td>
        <td>{{modifiedDate}}</td>
    </tr>
{{/posts}}
</tbody>

 

    - indexController에 수정화면을 연결할 Controller 코드 작업

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model){
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);
        
        return "posts-update";
    }

[게시글 수정 화면]

 

7. 게시글 삭제

    - posts-update.mustache

<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>

    - index.js

var main = {
    init : function (){

        $('#btn-delete').on('click', function (){
            _this.delete();
        });
    },
delete : function () {
        var id = $('#id').val();

        $.ajax({
            type: 'DELETE',
            url: '/api/v1/posts/' + id,
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
        }).done(function () {
            alert("글이 삭제되었습니다.");
            window.location.href = '/';
        }).fail(function (error) {
            alert(JSON.stringify(error));
        });
    }
};

main.init();

    - PostsService

@Transactional
public void delete(Long id){
    Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException(("해당 게시글이 없습니다. id="+id)));
    postsRepository.delete(posts);
}

    - PostsApiController

//삭제
@DeleteMapping("/api/v1/posts/{id}")
public Long delete(@PathVariable Long id){
    postsService.delete(id);
    return id;
}

728x90
반응형

댓글