ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Study 1주차 ~ 4장
    BackEnd/Spring 2022. 3. 31. 17:03

    "머스테치로 화면 구성하기"

    - 서버 템플릿 엔진과 클라이언트 템플릿 엔진의 차이는 무엇인가?

    - 왜 JSP말고 머스테치?

    - 머스테치를 이용한 CRUD화면 개발 방법


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

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

    서버 템플릿 엔진 : JSP, Freemaker 등

    클라이언트 템플릿 엔진 : React, Vue의 view파일 등

     

    ✅ 서버 템플릿 엔진을 이용한 화면생성은 서버에서 JAVA코드로 문자열을 만든뒤, 이 문자열을 HTML로 변환하여 브라우저로 전달. 클라이언트 템플릿 엔진은 브라우저 위에서 화면을 생성하기에 이미 서버에서 코드가 벗어난 경우임. 

     

    ✅ 머스테치란?

    다양한 언어를 지원하는 템플릿 엔진, 문법이 다른 엔진에 비해 심플하고 로직 코드를 사용할 수 없기에 서버와 View가 완전히 분리. (화면 역할에만 충실할 수 있음)

     


    4.2 기본 페이지 만들기

    우선 머스테치 플러그인을 설치하고, 의존성을 build.gradle에 등록한다. 

    implementation('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>

    - web/IndexController : mustache는 앞의 경로와 뒤의 파일 확장자를 자동으로 지정해준다. 

    @Controller
    public class IndexController {
        @GetMapping("/")
        public String index(Model model) {
            return "index"; // 문자열을 반환할 때 앞의 경로, 뒤 확장자는 자동지정됨.
            // 즉, src/main/resources/templates 까지는 자동, return 문의 index, 그 뒤 다시 자동으로 .mustache 확장자
        }
    }

    즉 위 코드는, src/main/resources/templates가 지정이 되어 있고, return되는 index가 경로에 붙은다음 확장자 .mustache는 다시 자동으로 붙는다. 이 경로를 View Resolver가 처리하게 된다. 

     

    - IndexControllerTest

    package firstSpring.practice.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 메인페이지_로딩() {
            String body = this.restTemplate.getForObject("/", String.class);
            assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
        }
    }

    메인 메소드를 실행하고 localhost:8080 접속하면 확인해볼 수 있다.

     

     


    4.3 게시글 등록 화면 만들기

     

    부트스트랩을 레이아웃 방식으로 추가하여 사용. 

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

     

    이런 디렉토리 구조

    - header.mustache

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

    - footer.mustache

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

     

    ✅ 페이지 로딩 속도를 높이기 위해 css는 header, js는 footer에 둔다. HTML은 head가 다 실행된 후에 body가 실행되기 때문이다. 

     

    이렇게 레이아웃 틀을 만들어두었기 때문에 index.mustache코드는 간결해진다. 

    <!DOCTYPE HTML>
    <html>
    {{>layout/header}}
    <h1> 스프링 부트로 시작하는 웹 서비스</h1>
    {{>layout/footer}}

    그리고 글을 등록하기 위한 버튼도 추가해보자. <h1>태그와 footer사이에 추가. 

    <div class="row">
            <div class="col-md-6">
                <a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
            </div>
        </div>

    <a> 태그를 이용해서 글 등록 버튼을 누르면 등록 페이지로 이동할 수 있다. 페이지에 관련된 작업들은 모두 IndexController를 사용한다. 

    ...
        @GetMapping("/posts/save")
        public String postsSave() {
            return "posts-save"; // posts/save 호출하면 posts-save.mustache 호출하는 메소드 추가
        }
    ...

    ✅ /posts/save호출하면 posts-save.mustache호출하는 메소드 추가. posts-save.mustache파일도 index.mustache파일과 같은 위치에 생성해준다. 

    {{>layout/header}}
    <h1> 게시글 등록 </h1>
    <div class="col-md-12">
        <div class="col-md-4">
            <form>
                <div class="form-group">
                    <label for="title"> 제목 </label>
                    <input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
                </div>
                <div class="form-group">
                    <label for="author"> 작성자 </label>
                    <input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요">
                </div>
                <div class="form-group">
                    <label for="content"> 내용 </label>
                    <textarea type="text" class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
                </div>
            </form>
            <a href="/" role="button" class="btn btn-secondary">취소</a>
            <button type="button" class="btn btn-primary" id="btn-save">등록</button>
        </div>
    </div>
    
    {{>layout/footer}}

    여기까지 작성하고 다시 프로젝트를 실행해보면 아래처럼 글 등록 화면도 완성된다. 

    이제 등록 버튼에 실제로 등록할 수 있게끔 API를 호출하는 코드를 작성한다. 

     

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

    var main = {
        init : function () {
            var _this = this;
            $('#btn-save').on('click', function() {
                _this.save();
            });
            $('#btn-update').on('click', function() {
                _this.update();
            });
            $('#btn-delete').on('click', function() {
                _this.delete();
            });
        },
        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();

    ✅ window.location.href = '/'; : 성공시 메인 페이지(/)로 돌아간다

    ✅ 함수의 이름이 중복되는 경우가 자주 발생할 수 있으므로, index라는 변수의 속성으로 function을 추가함으로써 유효범위를 만들어 사용할 수 있다. 

    ✅ 머스테치 파일이 index.js를 사용할 수 있게끔, footer.mustache에도 추가해준다. 

    <script src="/js/app/index.js"></script>

     

     

    테스트를 해보면 위처럼 글이 등록되었다는 alert가 뜨고, database에도 저장이 되어 있다. 

     


    4.4 전체 조회 화면 만들기

    - index.mustache를 다시 수정해보자

    <!DOCTYPE HTML>
    <html>
    {{>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">
            <thread class="thread-strong">
                <tr>
                    <th>게시글 번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>최종수정일</th>
                </tr>
            </thread>
            <tbody id="tbody">
            {{#posts}}
                <tr>
                    <td>{{id}}</td>
                    <td><a href="/posts/update/{{id}}">{{title}}</a></td>
                    <td>{{author}}</td>
                    <td>{{modifiedDate}}</td>
                </tr>
            {{/posts}}
            </tbody>
        </table>
    </div>
    {{>layout/footer}}
    </html>

    ✅ {{#posts}} : posts라는 List를 순회한다. for문의 기능을 한다고 생각하면 된다. 

    ✅ {{id}} : List에서 뽑아낸 객체의 필드. 

     

    - PostsRepository

    package firstSpring.practice.practice.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();
    }

    ✅ SpringDataJpa에서 제공하지 않는 메소드는 쿼리로 작성가능. Entity만으로 해결하기 어려운 복잡한 쿼리의 경우 직접 쿼리문을 작성하는 방안이 있다.

     

    - PostsService에 추가

    @Transactional(readOnly = true) // 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 속도 향상. 등록, 수정, 삭제 전혀 없는 메소드에서 사용 추천.
    public List<PostsListResponseDto> findAllDesc() {
    	return postsRepository.findAllDesc().stream()
        	.map(PostsListResponseDto::new) // posts -> new PostsListResponseDto(posts))의 람다식.
            .collect(Collectors.toList());
    }

    - PostsResponseDto

    package firstSpring.practice.practice.web.dto;
    import firstSpring.practice.practice.domain.posts.Posts;
    import lombok.Getter;
    
    @Getter
    public class PostsResponseDto {
        private Long id;
        private String title;
        private String content;
        private String author;
    
        public PostsResponseDto(Posts entity) {
            this.id = entity.getId();
            this.title = entity.getTitle();
            this.content = entity.getContent();
            this.author = entity.getAuthor();
        }
    }

    - IndexController

    @RequiredArgsConstructor
    @Controller
    public class IndexController {
    
        private final PostsService postsService;
        @GetMapping("/")
        public String index(Model model) {
            model.addAttribute("posts", postsService.findAllDesc());
            return "index"; // 문자열을 반환할 때 앞의 경로, 뒤 확장자는 자동지정됨.
            // 즉, src/main/resources/templates 까지는 자동, return 문의 index, 그 뒤 다시 자동으로 .mustache 확장자
        }
    }

    여기까지 코드를 완성하면 글 작성, 글 목록 2가지 기능이 구현되어 있다. 

     


    4.5 게시글 수정, 삭제 화면 만들기

    마지막으로, 만들어 두었던 게시글 수정 API를 위한 화면 개발, 삭제 화면 및 삭제 API를 작성하면 기본 기능 구현이 끝난다. 

     

    - 수정을 위한 posts-update.mustache

    {{>layout/header}}
    <h1>게시글 수정</h1>
    <div class="col-md-12">
        <div class="col-md-4">
            <form>
                <div class="form-group">
                    <label for="id">글 번호</label>
                    <input type="text" class="form-control" id="id" value="{{post.id}}" readonly>
                </div>
                <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 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>
            <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
        </div>
    </div>
    {{>layout/footer}}

    ✅ readonly : Input태그에서 값을 수정하지 못하도록 만들려면 readonly를 추가하면 된다. id와 author은 수정할 수 없도록 readonly 속성을 부여

     

    - index.js에 update function 추가

    $('#btn-update').on('click', function() {
    	_this.update();
    });
    
    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));
            });
        },

    ✅ $('#btn-update').on('click') : btn-update라는 id를 가진 HTML 요소에 click이벤트 발생하면 update함수 실행

    ✅ 수정을 위해 PUT 메소드 이용, url에는 게시물 구분을 위해 경로에 id가 붙는다. 

     

    - 수정 페이지로 이동하기 위한 index.mustache코드

    {{#posts}}
    <tr>
        <td>{{id}}</td>
        <td><a href="/posts/update/{{id}}">{{title}}</a></td>
        <td>{{author}}</td>
        <td>{{modifiedDate}}</td>
    </tr>
    {{/posts}}

    - indexController

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

     

    아래 화면처럼 확인할 수 있다. 

     

     


    마지막으로 게시글 삭제! 

    - posts-update.mustache : 삭제 버튼 추가

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

    - index.js에 삭제 이벤트 추가

     $('#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));
            });
    }

    - PostsService추가

    @Transactional
    public void delete (Long id) {
    	Posts posts = postsRepository.findById(id) // 존재하는지 먼저 확인
    	.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
    	postsRepository.delete(posts); // deleteById(id)로 바로 삭제도 가능. 엔티티를 파라미터로 주는것도 가능.
    }

    - 마지막으로 PostsApiController에 삭제 API를 구현해주면 끝난다! 

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

     

    - 삭제 버튼이 추가된 것을 확인할 수 있다. 

     


    참고 책 : http://www.yes24.com/Product/Goods/83849117

     

    스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

    가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

    www.yes24.com

     

    'BackEnd > Spring' 카테고리의 다른 글

    Spring 입문 강의 섹션 0 ~ 섹션 3  (0) 2022.07.29
    Spring Study 3주차 8장  (0) 2022.05.01
    Spring Study 2주차 ~ 6장, 7장  (0) 2022.04.07
    Spring Study 2주차 ~ 5장  (0) 2022.04.06
    Spring Study 1주차 ~ 2,3장  (0) 2022.03.30
Designed by Tistory.