🛠️Backend/Spring

[Spring Boot] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 07

뉴발자 2023. 1. 2.
728x90

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Mustache

 

 

Mustache로 화면 구성하기


템플릿 엔진이란?

 

웹 개발에 있어서 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 말한다.

 

JSP, Freemarker, Vue, React 등이 있다.

 

다만 JSP와 Freemarker서버 템플릿 엔진이라 불리고 Vue와 React클라이언트 템플렛 엔진이라 불린다.

 

 

 

Q) JavaScript에서 JSP나 Freemarker처럼 Java 코드를 사용할 수는 없을까?

 

예시 코드

<script type="text/javascript">

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

});

 

위 코드는 if문과 관계없이 "test"를 콘솔에 무조건 출력하는 코드이다.

 

이유는 Frontend의 JavaScript가 작동하는 영역과 JSP가 작동하는 영역이 다르기 때문이다.

 

JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동된다.

 

JSP는 명확하게 서버 템플릿 엔진은 아니지만,

 

View의 역할만 하도록 구성할 땐 템플릿 엔진으로써 사용할 수 있다.

 

이 경우엔 Spring + JSP로 사용한 경우로 보면 된다.


 

서버 템플릿 엔진을 이용한 화면 생성서버에서 Java 코드로 문자열을 만든 뒤

 

이 문자열을 HTML로 변환하여 브라우저로 전달한다.

 

앞선 코드는 HTML을 만드는 과정에서 System.out.println("test");를 실행할 뿐이며,

 

이때의 JavaScript 코드는 단순한 문자열일 뿐이다.

 

서버 템플릿 엔진

 

반면 JavaScript브라우저 위에서 작동한다.

 

앞에서 작성된 코드가 실행되는 장소는 서버가 아닌 브라우저이다.

 

즉, 브라우저에서 작동될 때는 서버 템플릿 엔진의 손을 벗어나 제어할 수 없게 된다.

 

흔히 이용하는 Vue.js와 React.js를 이용한 SPA(Single Page Application)는 브라우저에서 화면을 생성한다.

 

즉, 서버에서 이미 벗어난 코드인 것이다.

 

그래서 아래과 같이 서버에서는 Json 혹은 Xml 형식의 데이터만 전달하고 클라이언트에서 데이터를 조립한다.

 

클라이언트 템플릿 엔진

 

물론 React.js나 Vue.js와 같은 JavaScript Framework에서 서버 사이드 렌더링을 지원하는 모습을 볼 수는 있다.

 

간단하게 설명하자면 JavaScript Framework의 화면 생성 방식을 서버에서 실행하는 것을 말한다.

 

이는 V8 엔진 라이브러리를 지원하기 때문이며,

 

Spring Boot에서 사용할 수 있는 대표적인 기술들로는 Nashorn, J2V8이 있다.

 

다만 Spring Boot를 사용하면서 JS를 서버 사이드에서 렌더링하도록 구현하는 것은

 

Spring Boot에 대한 이해도가 낮은 초급 개발자에겐 추천하지 않는다.

 

 

 

 

Mustache

Mustache란?

수많은 언어를 지원하는 템플릿 엔진이다.

 

루비, JS, 파이썬, PHP, Java, 펄, GO, ASP 등 현존하는 대부분의 언어를 지원하고 있다.

 

그러다 보니 Java에서 사용할 때는 서버 템플릿 엔진으로,

 

JS에서 사용될 때는 클라이언트 템플릿 엔진으로 모두 사용할 수 있다.

 

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

 

책의 저자가 생각하는 템플릿 엔진들의 단점은 아래와 같다.

 


JSP, Velocity

  • Spring Boot에서는 권장하지 않는 템플릿 엔진이다.

 

 

Freemarker

  • 템플릿 엔진으로는 너무 과하게 많은 기능을 지원한다.
  • 높은 자유도로 인해 숙련도가 낮을수록 Freemarker 안에 비즈니스 로직이 추가될 확률이 높다.

 

 

Thymeleaf

  • Spring에서는 적극적으로 밀고 있지만 문법이 어렵고 HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 기존 개발자들에게는 높은 허들로 느껴지는 경우가 많다.
  • 실제로 사용해본 분들은 JS Framework를 배우는 기분이라고 후기를 이야기도 한다.
  • Vue.js를 사용해 본 경험이 있어 태그 속성 방식이 익숙한 분이라면 Thymeleaf를 선택해도 된다.

 

반면 Mustache의 장점은 아래와 같다.

 


Mustache의 장점

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

 

책의 저자는 템플릿 엔진은 화면 역할에만 충실해야 한다고 생각한다.

 

너무 많은 기능을 제공할 시 API와 템플릿 엔진, JS가 서로 로직을 나눠 갖게 되어 유지보수하기가 굉장히 어려워진다.

 

 

 

 

1. Mustache Plug-in 설치

앞서 언급한 장점 외에 한가지 장점이 더 존재한다.

 

그건 바로 IntelliJ 커뮤니티 버전을 사용해도 플러그인을 사용할 수 있다는 것이다.

 

Thymeleaf나 JSP 등은 커뮤니티 버전에서 지원하지 않고 얼티메이트 버전(유료)에서만 공식 지원한다.

 

Mustache는 이와 달리 커뮤니티 버전에서도 설치 가능한 플러그인이 존재한다.

 

그래서 커뮤니티 버전을 사용하는 개발자들은 Mustache를 사용하는 것이 좋다.

 

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

 

마켓플레이스에서 mustache를 검색해 설치한 후 IntelliJ를 재시작하여 플로그인이 작동하는 것을 확인해주면 된다.

 

mustache 설치

 

 

 

2. 기본 페이지 생성

Mustache를 설치한 후 편하게 사용할 수 있도록 Mustache Starter 의존성을 build.gradle에 등록한다.

...

dependencies {

...

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

}

 

이처럼 Mustache는 Spring Boot에서 공식 지원하는 템플렛 엔진이다.

 

의존성 하나만 추가하면 다른 스타터 패키지와 마찬가지로 추가 설정 없이 설치가 끝난다.

 

별도로 Spring Boot 버전을 개발자가 신경 쓰지않아도 되는 장점도 있다.

 

Mustache의 파일 위치는 기본적으로 src/main/resources/templates이다.

 

이 위치에 Mustache 파일을 두면 Spring Boot에서 자동으로 로딩한다.

 

첫 페이지를 담당할 index.mustache를 src/main/resources/templates에 생성한다.

 

mustache 폴더 생성 위치

 

index.mustache의 코드는 아래와 같다.

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

 

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

 

web package 안에 IndexController를 생성한다.

 

 

IndexController의 코드는 아래와 같다.

 

package com.jojoIdu.book.springboot.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

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

}

 

Mustache Starter 덕분에 Controller에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다.

 

앞의 경로src/main/resources/templates로, 뒤의 파일 확장자.mustache가 붙게 된다.

 

즉, 여기서는 "index"를 return해주므로,

 

src/main/resources/templates/index.mustache로 전환되어 View Resolver를 처리하게 된다.

 

 

이제 테스트 코드를 작성하여 실행시켜보겠다.

 

test package의 web에 IndexControllerTest Class를 생성하고 아래와 같이 코드를 작성한다.

 

package com.jojoIdu.book.springboot.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;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IndexControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void 메인페이지_로딩() {

        //when
        String body = this.restTemplate.getForObject("/", String.class);

        //then
        assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");

    }

}

 

위 코드는 실제로 URL 호출 시 내용이 제대로 호출되는지에 대한 테스트이다.

 

HTML도 결국은 규칙이 있는 문자열이다.

 

TestRestTemplate를 통해 "/"로 호출했을 때 index.mustache에 포함된 코드들이 있는지 확인하면 된다.

 

전체 코드를 다 검증할 필요는 없으니 "스프링 부트로 시작하는 웹 서비스" 문자열이 포함되어 있는지만 비교한다.

 

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

 

IndexControllerTest 결과

 

테스트 코드의 검증이 끝났으니 실제로 화면이 잘 나오는지 테스트를 해보겠다.

 

Application.java의 main() 메소드를 실행하고

 

브라우저에 http://localhost:8080/으로 접속하면 아래와 같은 화면이 나온다.

 

http://localhost:8080/ 접속 시 나오는 화면

 

 

 

3. 게시글 등록 화면 생성

이전 포스팅에서 PostsApiController로 API를 구현하였으니 여기선 바로 화면을 개발한다.

 

그냥 HTML을 사용하기에는 화면 구성이 단조롭고 밋밋하다.

 

그래서 오픈 소스인 Bootstrap을 이용하여 화면을 구성하겠다.

 

Bootstrap, jQuery 등 Frontend 라이브러리를 사용할 수 있는 방법은 크게 2가지가 있다.

 

하나는 외부 CDN을 사용하는 것이고,

 

다른 하나는 직접 라이브러리를 받아서 사용하는 방법이다.

 

교재에서는 외부 CDN을 사용하여 화면을 구성다.

 

 

2개의 라이브러리 Bootstrap과 jQuery를 index.mustache에 추가해야 한다.

 

하지만, 바로 추가하지 않고 레이아웃 방식으로 라이브러리를 추가해보겠다.

 

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

 

이번에 추가할 라이브러리들인 Bootstrap과 jQuery는 Mustache 화면 어느 곳에서든 필요하다.

 

매번 해당 라이브러리를 Mustache 파일에 추가하는 것은 귀찮고 소모적인 일이다.

 

src/main/resources/templates 디렉터리 아래에 layout 디렉터리를 추가로 생성해준다.

 

그리고 layout 디렉터리에 header.mustache 파일과 footer.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>

</body>
</html>

 

코드를 보면 css와 js의 위치가 서로 다르다.

 

페이지의 로딩속도를 높이기위해 css는 header.mustache에, js는 footer.mustache에 위치했다.

 

HTML위에서부터 코드가 실행되기 때문에 head가 전부 실행되고나서 body가 실행된다.

 

즉, head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출되게 된다.

 

특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에

 

js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.

 

반면 css는 화면을 그리는 역할이므로 head에서 불러오는 것이 좋다.

 

그렇지 않으면 css가 적용되지 않은 깨진 화면을 사용자가 볼 수 있기 때문이다.

 

추가로, bootstrap.js의 경우 jQuery가 꼭 있어야만 하기 때문에

 

Bootstrap보다 먼저 호출되도록 코드를 작성했다.

 

보통 앞선 상황을 bootstrap.js가 jQuery에 의존한다고 한다.

 

 

라이브러리를 비롯해 기타 HTML 코드들이 모두 layout에 추가되니

 

이제 index.mustache에는 필요한 코드만 남게 된다.

 

index.mustache의 코드를 아래와 같이 변경된다.

 

{{>layout/header}}

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

{{>layout/footer}}

 

* {{> }}는 현재 Mustache 파일(여기선, index.mustache)을 기준으로 가른 파일을 가져온다.

 

레이아웃으로 파일이 분리되었으니 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>
    </div>

{{>layout/footer}}

 

위 코드에서 <a>태그를 이용해 글 등록 페이지로 이동하는 '글 등록'버튼이 생성되었다.

 

이동할 페이지의 URL은 '/posts/save'이다.

 

이 주소에 해당하는 메소드를 IndexController에 추가로 작성한다.

 

package com.jojoIdu.book.springboot.web;

...

@Controller
public class IndexController {

...

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

}

 

index.mustache와 마찬가지로 /posts/save를 호출하면 posts-save.mustache를 호출하는 메소드가 추가되었다.

 

Controller 코드가 생성되었다면 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>
                <input type="text" class="form-control"
                       id="content" placeholder="내용을 입력하세요">
            </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}}

 

위의 코드까지 작성하였다면 다시 main() 메소드를 실행시키고 http://localhost:8080/ 으로 접근해보겠다.

 

해당 페이지의 '글 등록' 버튼을 클릭하면 http://localhost:8080/posts/save화면으로 이동하게 된다.

 

http://localhost:8080/

 

http://localhost:8080/posts/save

 

하지만, 아직 게시글 등록 화면에서 등록 버튼은 기능이 없다.

 

API를 호출하는 JS가 전혀 없기 때문이다.

 

API를 호출하기 위해 src/main/resources에 static/js/app 디렉토리를 하나 생성하고 index.js 파일을 생성한다. 

 

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=utr-8',
            data: JSON.stringify(data)
        }).done(function() {
            alert('글이 등록되었습니다.');
            window.location.href='/';
        }).fail(function(error) {
            alert(JSON.stringify(error));
        });

    }

};

main.init();

 

*window.location.href='/'; - 글 등록이 성공하면 메인페이지('localhost:8080/')로 이동한다.

 

index.js의 첫 문장에 var main = {...} 란 코드를 선언했다.

 

굳이 index라는 변수의 속성으로 function을 추가한 이유는 다음과 같다.


만약 index.js가 다음과 같이 function을 작성한 상황이라고 가정해보자.

var init = function() {

	...

	};

	var save = function() {
    
    	...
    
    	};
    
    	init();

 

index.mustache에서 a.js가 추가되어 a.js도

 

a.js만의 init과 save function을 가지고 있다면,

 

브라우저의 스코프는 공용 공간으로 쓰이기 때문에

 

나중에 로딩된 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 된다.

 

여러 사람이 참여하는 프로젝트에서는 중복된 함수 이름은 자주 발생할 수 있다.

 

하지만 모든 function이름을 확인하면서 코드를 작성할 수는 없다.

 

그러다 보니 이런 문제를 피하려고 index.js만의 유효 범위를 만들어서 이용하게 된다.

 

방법은 var index란 객체를 만들어 해당 객체에서 필요한 모든 function을 선언하는 것이다.

 

이렇게 하면 index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹칠 위험이 사라지게 된다.


 

이제 생성된 index.js를 mustache 파일이 쓸 수 있게 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>

 

index.js 호출 코드를 보면 절대 경로(/)로 바로 시작된다.

 

Spring Boot는 기본적으로 src/main/resources/static에 위치한 JS, CSS, IMG 등 정적 파일들은 URL에서 '/'로 설정된다.

 

그래서 다음과 같이 파일이 위치하면 위치에 맞게 호출이 가능하다.

 


  • src/main/resources/static/js/...(http://domain/js/...)
  • src/main/resources/static/css/...(http://domain/css/...)
  • src/main/resources/static/image/...(http://domain/image/...)

 

이제 등록 기능을 브라우저에서 직접 테스트 해보겠다.

 

화면에서 제목, 작성자, 내용을 작성한 후 등록버튼을 클릭하면

 

아래와 같이 "글이 등록되었습니다"라는 Alert가 노출된다.

 

게시글 등록시 Alert 노출 화면

 

확인 버튼을 누르면 'http://localhost:8080/'화면으로 이동된다.

 

실제로 DB에 데이터가 등록되었는지 확인하기 위해 'http://localhost:8080/h2-console'로 접속해 확인한다.

 

http://localhost:8080/h2-console - SELECT TABLE

 

정상적으로 데이터가 insert된 것을 확인할 수 있다.

 

 

 

 

다음번에는 전체 조회 화면 및 게시글 수정, 삭제 화면을 생성하는 방법을 포스팅하겠다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글