-
[4장] 머스테치로 화면 구성하기Spring/스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2022. 11. 21. 22:10
✏️ 서버 템플릿 엔진과 메스테치 소개
- JSP를 비록한 서버 템플릿 엔진은 서버에서 구동됨
- 서버 템플릿 엔진을 이용한 화면 생성
- 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달
- 브라우저에서 자바스크립트가 작동될 때는 서버 템플릿 엔진의 손을 벗어나 제어할 수 없게 됨
📎머스테치
- 장점
- 문법이 다른 템플릿 엔지보다 간단함
- 로직 코드를 사용할 수 없어 View 의 역할과 서버의 역할이 명확하게 분리됨
- 현존하는 대부분의 언어를 지원하기 때문에 서버와 템플릿 엔진으로 모두 사용 가능
머스테치 의존성 추가
IndexController
@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/update/{id}") public String postsUpdate(@PathVariable Long id, Model model) { PostsResponseDto dto = postsService.findById(id); model.addAttribute("post",dto); return "posts-update"; } // @GetMapping("/") // public String index() { // return "index"; //머스테치에서 문자열 반환 시 앞의 경로와 뒤의 파일 확장자 자동으로 지정된다 // } @GetMapping("/posts/save") public String postsSave() { return "posts-save"; } }
- 머스테치 스타터는 컨트롤러가 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정
IndexController
@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); // URL 호출 시 페이지 내용 제대로 호출 되었는지 테스트 -> 코드가 잘 포함되어 있는지 확인하면 된다. assertThat(body).contains("스프링 부트로 시작하는 웹 서비스"); } }
- 실제 URL호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트
- TestRestTemplate를 통해 "/"로 호출했을 때 index.mustache에 포함된 코드들이 있는지 확인
✏️ 게시글 등록 화면 만들기
📌 프론트엔드 라이브러리 사용 방법
- 외부 CDN 사용 ☝️
- 직접 라이브러리를 받아서 사용
레이아웃 방식: 공통 영역을 별도의 파일로 분리하여 필요한 곳에 가져다 쓰는 방식
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> <!--index.js 추가--> <script src="/js/app/index.js"></script> </body> </html>
- header, footer 따로 만들어서 로딩 속도 향상
- HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행됨
- 따라서 body를 하단에 두어야 효율적
index.mustache
{{>layout/header}} <h1>스프링 부트로 시작하는 웹 서비스 Ver.2</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="thead-strong"> <tr> <th>게시글번호</th> <th>제목</th> <th>작성자</th> <th>최종수정일</th> </tr> </thead> <tbody id="tbody"> {{#posts}} <tr> <td>{{id}}</td> <td><a href="/posts/update/{{id}}">{{title}}</td> <td>{{author}}</td> <td>{{modifiedDate}}</td> </tr> {{/posts}} </tbody> </table> </div> {{>layout/footer}}
posts-save.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 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}}
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)); }); }, 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)); }); }, 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();
- $('#btn-update').on('click')
- btn-update란 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하도록 이벤트를 등록
- update : function ()
- 신규로 추가될 update function
- type:'PUT'
- 여러 HTTP Method 중 PUT메소드를 선택
- REST에서 CRUD는 아래와 같이 HTTP Method에 매핑됨
- Create - POST
- Read - GET
- Update - PUT
- Delete - DELETE
상황: index.mustache에서 a.js가 추가되어 a.js도 a.js만의 init과 save function이 있다면?
- 브라우저의 스코프는 공용공간으로 쓰임
- => 나중에 로딩된 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 됨
- 해결법
- var.index라는 객체를 만들어 해당 객체에서 필요한 모든 function을 선언
- index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹칠 위험이 사라짐
- var.index라는 객체를 만들어 해당 객체에서 필요한 모든 function을 선언
머스테치 문법
- <<#posts>>
- posts라는 List를 순회
- Java의 for문과 동일하게 생각!
- <<id>>등의 {{변수명}}
- List에서 뽑아낸 객체의 필드를 사용
PostsRepository
public interface PostsRepository extends JpaRepository<Posts,Long> { @Query("Select p from Posts p order by p.id DESC ") List<Posts> findAllDesc(); }
- SpringDataJpa에서 제공하지 않는 메소드는 쿼리로 작성해도 됨!
PostsListResponseDto
@Getter public class PostsListResponseDto { private Long id; private String title; private String content; private String author; private LocalDateTime modifiedDate; public PostsListResponseDto(Posts entity) { this.id = entity.getId(); this.title = entity.getTitle(); this.content = entity.getContent(); this.author = entity.getAuthor(); this.modifiedDate = entity.getModifiedDate(); } }
IndexController
@RequiredArgsConstructor @Controller public class IndexController { private final PostsService postsService; @GetMapping("/") public String index(Model model) { //Model: 서버 템플릿 엔진에서 사용할 수 있는 객체 저장 model.addAttribute("posts", postsService.findAllDesc()); return "index"; }
- Model
- 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있음
- postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달하는 역할
✏️ 게시글 수정, 삭제 화면 만들기
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> <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}}
- {{posts.id}}
- 머스테치는 객체의 필드 접근 시 점으로 구분
- Posts클래스의 id에 대한 접근은 post.id로 사용 가능
- readonly
- input 태그에 읽기 가능만 허용하는 속성
- id와 author는 수정할 수 없도록 읽기만 허용하도록 추가
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"; }
PostsApiController 추가
@DeleteMapping("/api/v1/posts/{id}") public Long delete(@PathVariable Long id) { postsService.delete(id); return id; }
'Spring > 스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
[3장] 스프링 부트에서 JPA로 데이터베이스 다뤄보자 (0) 2022.11.17 [2장] 스프링 부트에서 테스트 코드를 작성하자 (0) 2022.11.12