스프링 부트와 AWS로 혼자 구현하는 웹 서비스[4]

uuuu.jini·2022년 3월 28일
0
post-thumbnail

🐯 04장. 머스테치로 화면 구성하기 🐯

  1. 서버 템플릿 엔진과 머스테치 소개
  2. 기본 페이지 만들기
  3. 게시글 등록 화면 만들기
  4. 전체 조회 화면 만들기
  5. 게시글 수정, 삭제 화면 만들기

머스테치를 통해 화면 영역을 개발하는 방법을 배운다. 서버 템플릿 엔진과 클라이언트 템플릿 엔진의 차이는 무엇인지, 머스테치를 통해 기본적인 CRUD 화면 개발 방법등을 차례로 진행한다.


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

템플릿 엔진이란, 지정된 템플릿 양식과 데이터가 합쳐져 HTML문서를 출력하는 소프트웨어를 말한다. ( JSP,Freemarker,React,Vue 등 )

JSP,Freemarker는 서버 템플릿 엔진, React와 Vue는 클라이언트 템플릿 엔진이라 불린다. 자바스크립트가 작동하는 영역과 JSP가 작동하는 영역이 다르다. JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동 된다.

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

반면 자바스크립트는 브라우저 위에서 작동 한다. 자바스크립트 코드가 실행되는 장소는 서버가 아닌 브라우저 이다. 즉, 브라우저에서 작동 될때는 서버 템플릿 엔진의 손을 벗어나 제어할 수가 없다.

Vue.js나 React.js를 이용한 SPA(Single Page Application)은 브라우저에서 화면을 생성한다. 즉, 서버에서 이미 코드가 벗어난 경우이다. 그래서 서버에서는 Json 혹은 Xml형식의 데이터만 전달하고 클라이언트에서 조립한다.

머스테치란 ?

머스테치는 수많은 언어를 지원하는 가장 심플한 템플릿 엔진 이다. 루비,자바스크립트,파이썬,PHP,자바,펄,Go,ASp등 현존하는 대부분 언어를 지원하고 있다. 자바에서 사용될 때는 서버템플릿 엔진으로 자바스크립트에서 사용될때는 클라이언트 템플릿 엔진으로 모두 사용할 수 있다.

자바 진영에는 JSP,Velocity,Freemarker,Thymleaf등 다양한 서버 템플릿 엔진이 존재한다.

템플릿 엔진들의 단점

  • JSP,Velocity : 스프링 부트에서는 권장하지 않음
  • Freemarker : 너무 과하게 많은 기능 지원, 숙련도 낮을 수록 비지니스 로직 추가 확률 높음
  • Thymleaf : 스프링 진영에서 적극적으로 밀고 있지만 문법이 어려움. HTML 태그에 속성으로 템플릿 기능을 사용하는 방식을 사용

머스테치의 장점

  • 문법이 심플, 로직 코드를 사용할 수 없어 서버 , View역할 명확 분리
  • Mustache.js와 Mustache.java 2가지 다 있어, 하나의 문법으로 클라이언트/서버 템플릿 모두 사용 가능

머스테치 플러그인 설치

해당 플러그인을 사용하면 문법 체크, HTML 문법 지원, 자동완성 등이 지원된다.

설치가 완료되면 인텔리제이를 재시작하여 플러그인이 작동하는 것을 확인한다.


2. 기본 페이지 만들기

가장 먼저 머스테치 스타터 의존성을 build.gradle에 등록한다.

compile('org.springframework.boot:spring-boot-starter-mustache')

머스테치의 파일 위치는 기본적으로 src/main/resources/templates이다. 이 위치에 머스테치 파일을 두면 스프링 부트에서 자동으로 로딩한다.

index.mustache

간단하게 h1 크기로 "스프링 부트로 시작하는 웹서비스" 를 출력하는 페이지이다.

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

IndexController.java

이 머스테치에 URL을 매핑한다. URL 매핑은 Controller에서 진행한다.


@Controller
public class IndexController {

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

머스테치 스타터 덕분에 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다. 여기선 "index"를 반환하므로 src/main/resources/templates/index.mustache 로 전환되어 View Resolver가 처리하게 된다.

IndexControllerTest.java

테스트 코드를 통한 검증을 진행한다.


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest extends TestCase {
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    public void 메인페이지_로딩(){
       String body = this.restTemplate.getForObject("/",String.class);
       
       assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
    }
}

실제로 URL 호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트입니다. TestRestTemplate을 통해 "/"로 호출했을 때 index.mustache에 포함된 코드들이 있는지 확인하면 된다. 전체 코드를 다 검증할 필요는 없으니 해당 문자열이 포함되어 있는지만 비교한다.

테스트 코드를 수행해보면 정상적으로 코드가 수행되는 것을 확인할 수 있다.

실제 실행 페이지


3. 게시글 등록 화면 만들기

부트스트랩을 이용하여 화면을 만든다. 부트스트랩,제이쿼리 등 프론트 엔드 라이버러리를 사용할 수 있는 방법은 크게 2가지가 있다. 하나는 외부 CDN을 사용하는 거이고, 다른 하나는 직접 라이브러리를 받아서 사용하는 방법이다. 전자의 외부 CDN을 사용한다. ( HTML/JSP/Mustache에 코드만 한 줄 추가하면 되므로 굉장히 간단하다.)

2개의 라이브러리를 index.mustache에 추가해야 한다.바로 추가하지 않고 레이아웃 방식으로 추가한다. (레이아웃 방식 - 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식)

header.mustache와 footer.mustache를 생성한다.

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>

코드를 보면 css와 js의 위치가 서로 다르다. 페이지 로딩속도를 높이기 위해 css는 header에,js는 footer에 두었다. HTML은 위에서부터 코드가 실행되기 때문에 header가 다 실행되고서야 body가 실행된다.

즉, header가 다 불러지지 않으면 사용자 쪽에서는 백지 화면만 노출된다. 특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.

반면 css는 화면을 그리는 역할이므로 head에서 불러오는 것이 좋다. 그렇지 않으면 css가 적용되지 않은 깨진 화면을 사용자가 볼 수 있기 때문이다. 추가로, bootstrap.js는 제이쿼리가 꼭 있어야만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성한다. (앞선 상황을 의존한다고 한다. )

index.mustache 수정

라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가되므로 index.mustache에는 필요한 코드만 남게 된다. index.mustache는 다음과 같이 변경된다.

{{>layout/header}}

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

{{>layout/footer}}
  • {{>layout/header}} : {{>}}는 현재 머스테치 파일을 기준으로 다른 파일을 가져온다.

레이아웃으로 파일을 분리하였으니 글 등록 버튼을 하나 추가한다.

{{>layout/header}}

<h1> 스프링 부트로 시작하는 웹 서비스</h1>
<div class = "col-md-12">
    <div class = "row">
        <a href="/posts/save" role = "button" class = "btn btn-primary">글 등록</a>
    </div>
</div>
{{>layout/footer}}

<a > 태그를 이용해 글 등록 페이지로 이동하는 글 등록 버튼이 생성되었다. 이동할 페이지의 주소는 posts/save 이다.

IndexController.java 수정

해당 주소에 해당하는 컨트롤러를 생성한다. 페이지에 관련된 컨트롤러는 모두 IndexController를 사용한다.


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

/posts/save를 호출하면 posts-save.mustache를 호출하는 메소드이다. 해당 파일을 생성한다.

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 - 글 동록 버튼 기능

src/main/resourcesstatic/js/app디렉토리를 생성하고 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();
  • window.location.href='/' : 글 등록이 성공하면 메인페이지(/)로 이동한다.

footer.mustache 수정

생성된 index.js를 머스테치 파일이 쓸수있게 footer.mustach에 추가한다.

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

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


4. 전체 조회 화면 만들기

<!-- 목록 출력 영역 -->

<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>{{title}}</td>
            <td>{{author}}</td>
            <td>{{modifiedDate}}</td>
        </tr>
    {{/posts}}
    </tbody>
</table>
  • {{#posts}} : posts라는 List를 순회한다. for문과 동일
  • {{#id}}등의 변수명 : List에서 뽑아낸 객체의 필드를 사용

Controller,Service,Repository 코드 작성

PostRepostiroy 인터페이스

@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();

PostService 클래스

	@Transactional(readOnly = true)
    public List<PostsListResponseDto> findAllDesc(){
        return postsRepository.findAllDesc().stream()
                .map(PostsListResponseDto::new)
                .collect(Collectors.toList());
    }
  • @Transactional(readOnly=true) : readOnly를 true로 주면 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선되기 때문에 추천한다.

  • .map(PostsListResponseDto::new) : 실제로 .map(posts->new PostsListResponseDto(posts))와 같다. postRepository로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto 변환 -> List로 변환하는 메소드 이다.

PostsListResponseDto 클래스

@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.author = entity.getAuthor();
        this.title = entity.getTitle();
        this.modifiedDate = entity.getModifiedDate();
    }
}

IndexController 클래스

    @GetMapping("/")
    public String index(Model model){
        model.addAttribute("posts",postsService.findAllDesc());
        return "index";
    }
  • Model : 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있다. 여기서는 postsService().findAllDesc()에서 가져온 결과를 posts로 index.mustache에 전달한다.

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

게시글 수정

API

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

posts-update.mustache

{{>layout/header}}
<h1>게시글 수정</h1>

<div class = "col-md-14">
    <div class = "col-md-4">
        <form>
            <div class="form-group">
                <label for = "id">글 번호</label>
                <input type="text" class = "form-control" id = "id" value = "{{posts.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}}

index.js 수정

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

게시글 삭제

게시글 삭제기능도 위와 동일하게 구현이 가능하다.

profile
멋쟁이 토마토

0개의 댓글