4. 머스테치로 화면 구성하기

WOOK JONG KIM·2023년 2월 15일
0

스프링부트 & AWS

목록 보기
4/8
post-thumbnail

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

일반적으로 탬플릿 엔진이란, 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어
-> 예전엔 JSP, Freemarker 요즘엔 React,vue의 view 파일
-> 둘다 지정된 템플릿과 데이터를 이용하여 HTML을 생성하는 템플릿 엔진
-> 하지만 전자는 서버 템플릿 엔진이고 후자는 클라이언트 템플릿 엔진이라고 불림

자바스크립트에서 JSP나 Freemarker처럼 자바 코드를 사용할 순 없나요?
코드 예시

<script type = "text/javascript">

$(document).ready(function(){
	if(a=="1"){
    	<%
        	System.out.println("test");
        %>
    }
});

위 코드는 if문과 관계 없이 무조건 test를 콘솔에 출력
-> 프론트엔드의 자바 스크립트가 작동하는 영역과 JSP가 작동하는 영역이 다르기 때문!
(JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동)

  1. 서버 템플릿 엔진

서버에서 자바 코드로 문자열을 만든뒤, 이 문자열을 HTML로 변환하여 브라우저로 전달
-> 이때 자바스크립트 코드는 단순한 문자열

  1. 클라이언트 템플릿 엔진

반면 자바 스크립트는 브라우저 위에서 작동(코드가 실행되는 장소가 브라우저)
-> 서버 템플릿 엔진과 다르게 브라우저에서 화면을 생성

머스테치

수많은 언어를 지원하는 가장 심플한 템플릿 엔진으로, JSP와 같이 HTML를 만들어 주는 템플릿 엔진

대부분 언어를 지원하기에, 자바에서 사용될 때는 서버 템플릿 엔진, 자바스크립트에서 사용될 때는 클라이언트 템플릿 엔진으로 모두 사용 가능

자바 진영에는 JSP,Velocity,Freemarker,Thymeleaf등 다양한 서버 템플릿 엔진 존재
-> Freemarker는 템플릿 엔진으로는 너무 과하게 많은 기능을 지원하고, Thymeleaf는 스프링에서 적극적으로 밀지만 문법이 어려움(태그 속성이 익숙하다면 추천)
-> 반면 머스테치는 문법이 다른 템플릿보다 심플하며, 로직 코드를 사용할 수 없어 View 역할과 서버의 역할이 명확하게 분리됨


기본 페이지 만들기

머스태치 파일의 기본 위치는 기본적으로 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 에서 진행)
-> web 패키지 안에 IndexController를 생성

indexController.java

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

머스테치 스타터 덕분에 컨트롤러에서 문자열을 반활할 때
-> 앞의 경로와 뒤의 파일 확장자는 자동으로 지정됨
-> 즉 index 문자열 반환할떄, src/main/resources/templates/index.mustache로 변환되어 View resolver가 처리하게 됨


게시글 등록 화면 만들기

HTML만 사용하기엔 멋이 없어서 오픈 소스인 부트스트랩을 이용해서 화면을 만들 것
-> 부트스트랩이나 제이쿼리 등 프론트엔드 라이브러리를 사용할 수 있는 방법은 2가지

  1. 외부 CDN 사용
  2. 직접 라이브러리를 받아서 사용

여기선 외부 CDN을 사용할 것이지만 실제 서비스에서는 외부 서비스에 의존하게 돼버려서, 잘 사용하지 않음

부트스트랩과 제이쿼리를 index.mustache에 레이아웃 방식으로 추가
-> 레이아웃 방식이란 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식
-> 매번 해당 라이브러리를 머스태치 파일에 추가하는 것은 귀찮으니, 레이아웃 파일들을 만들어 추가

footer

<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>

header

<!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>

페이지 로딩 속도를 높이기 위해 css는 header에, js는 fotter에 둠
-> HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고 나서 body가 실행
-> 즉 head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출

예를 들어 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋음
-> 반면 css는 화면을 그리는 역할이므로 head에서 불러오는 것이 좋음
-> 또한 bootstrap의 경우 제이 쿼리가 꼭 있어야 하기에 먼저 호출되도록 코드 작성

위와 같이 라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가되니 이제 index.mustache에는 필요한 코드만 남게 됨

{{>layout/header}}

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

{{>layout/footer}}

글 등록 버튼 하나 추가

{{>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>
    </div>
    
{{>layout/footer}}

글 등록 버튼 클릭시 이동할 페이지 주소 /posts/save
-> 해당 컨트롤러 생성

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(){
        return "index";
    }
    
    @GetMapping("/posts/save")
    public String postsSave(){
        // "/posts/save"를 호출 시 posts-save.mustache 를 호출하는 메서드
        return "posts-save";
    }
}

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}}

아직 위 페이지의 등록 버튼은 기능이 없음(API를 호출하는 JS가 전혀 없기 때문)
-> resources에 static/js/app 디렉토리 생성한 후 index.js생성

index.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();

위 코드는 책 참고, index.js만의 유효범위를 만들어 사용 한것

inex.js를 머스테치 파일이 쓸 수 있게 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>

위 경우 호출시 절대 경로(/)로 바로 시작
-> 스프링부트는 기본적으로 src/main/resources/static에 위치한 자바스크립트,CSS, 이미지 등 정적 파일들은 URL에서 /로 설정

이후 웹페이지에서 글 등록 가능

글 등록 시 DB에 저장되는 것을 확인 가능


전체 조회 화면 만들기

index.mustache의 UI 변경

{{>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}}</a></td>
                    <td>{{author}}</td>
                    <td>{{modifiedDate}}</td>
                </tr>
            {{/posts}}
            </tbody>
        </table>
    </div>
{{>layout/footer}}

머스테치 문법이 처음으로 사용됨

{{#posts}} : posts라는 List를 순회(for 문과 동일하다고 생각)

{{id}} 등의 {{변수명}} : List에서 뽑아낸 객체의 필드를 사용


PostRepository 인터페이스에 쿼리 추가

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

SpringDataJpa에서 제공하지 않는 메서드는 위처럼 쿼리로 작성해도 가능


규모가 있는 프로젝트에서의 데이터 조회는 FK의 조인, 복잡한 조건 등으로 인해 이런 Entity 클래스만으로 처리하기 어려워 조회용 프레임워크를 추가로 사용
-> 대표적으로 querydsl, jooq, MyBatis
-> 조회는 위 3가지 프레임웤 중 하나를 사용하고, 등록/수정/삭제 등은 SpringDataJpa를 통해 진행

보통 querydsl을 추천
-> 단순한 문자열로 쿼리를 생성하는 것이 아니라, 메소드를 기반으로 쿼리를 생성하기 때문에 오타나 존재하지 않는 컬럼명을 명시할 경우 IDE에서 자동 검출(Mybatis 지원 X)
-> 또한 국내 많은 회사에서 사용 중이고 레퍼런스가 많음


PostsService에 코드 추가

	...
    
	@Transactional(readOnly = true)
    public List<PostsListResponseDto> findAllDesc(){
        return postsRepository.findAllDesc().stream()
                .map(PostsListResponseDto::new) // .map(posts -> new PostsListResponseDto(posts)) 와 동일
                .collect(Collectors.toList());
    }
    
    ...

위코드는 postRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto 변환 -> List로 변환하는 메서드

PostsListResponseDto.java

@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();
    }
}

컨트롤러 변경

	...
    
	@GetMapping("/")
    public String index(Model model){
        model.addAttribute("posts", postsService.findAllDesc());
        return "index";
    }
    
    ...

Model에는 서버 템플릿 엔진이 사용할 수 있는 객체를 저장 가능
-> 여기에선 postService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달

controller까지 완성 후 정상 동작 확인


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

	@PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id,
                       @RequestBody PostsUpdateRequestDto requestDto){
        return postsService.update(id, requestDto);
    }

위 api로 요청하는 화면 개발

게시글 수정

{{>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}}

{{post.id}} 처럼 머스테치는 객체의 필드 접근시 Dot으로 구분
-> 즉 Post 클래스의 id에 대한 접근은 post.id로 사용 가능

readOnly는 input 태그에 읽기 가능만 허용하는 속성

btn-update 버튼을 클릭하면 update 기능을 호출할수 있게 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));
        });
 	}
};

main.init();

${'#btn-update').on('click') : id를 가진 HTML 엘리먼트에 click이벤트가 발생할 시 update function을 실행하도록 이벤트를 등록

전체 목록에서 수정 페이지로 이동할 수 있게 페이지 이동 기능 추가
-> 타이틀을 클릭하면 해당 게시글의 수정 화면으로 이동하도록

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

화면 작업이 끝났으니 수정 화면 연결할 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";
    }
    ...

게시글 등록 후, 제목을 클릭할 시 수정 창으로 들어가며 변경 또한 가능


게시글 삭제

삭제 버튼은 본문을 확인하고 진행하기 위해, 수정 화면에 추가


	post-update.mustache
	...
    
	<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>
    
    ...

위에서 btn-delete 버튼 클릭시 JS에서 이벤트를 수신

삭제 이벤트를 진행할 JS 코드 추가

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); // 기본으로 제공하는 delete 메서드 사용, 조회 후 그대로 삭제
    }


...

PostsApiController

...

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

화면 개발뿐 아니라 웹 요청에서의 테스트 코드 작성 방법 배웠음

profile
Journey for Backend Developer

0개의 댓글

관련 채용 정보