Spring/스프링 부트와 AWS로 혼자 구현하는 웹 서비스
[4장] 머스테치로 화면 구성하기
한 면만 쓴 종이
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;
}