스프링부트와 AWS로 혼자 구현하는 웹서비스 따라하기

시작👊

화면 만들어보기

어제 JPA Hibernate Spring Data Jpa 에 대해 공부했다.
JPA의 더티 체킹으로 update 쿼리 없이 테이블을 수정해보고
Spring Data Jpa 로 관계형 데이터베이스를 객체지향적으로 관리해보고
JPA Auditing 을 이용하여 등록/수정 시간을 자동으로 기록해봤다.

ORM 은 데이터베이스를 사용하는 서비스를 객체지향적으로 구현하는데 큰 도움을 주는 도구입니다.

열심히 공부해보겠씁니다!

오늘은 머스테치 Mustache 를 이용해 화면 영역을 개발해볼 겁니다.

서버 템플릿 엔진과 클라이언트 템플릿 엔진으 ㅣ차이는 무엇인지, 왜 이책이 JSP 가 아닌 머스테치를 선택했는지, 머스테치를 통해 기본적인 CRUD 화면 개발 방법 등을 차례로 진행해보겠습니다.

좋습니다 시작하시지여

우선

서버 템플릿 엔진

템플릿 엔진이란?
일반적으로 웹 개발에 있어 템플릿 엔진이란, 지정된 템플리 ㅅ양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 얘기합니다. JSP, Freemarker 가 떠오를 수도 있고, React, Vue의 View 가 떠오를 수도 있습니다.

맞습니다. React 말하는 건가? 싶었습니다.

둘 모두 결과적으로 지정된 템플릿과 데이터를 이용해 HTML을 생성하는 템플릿 엔진입니다.

오호...

하지만 조금의 차이가 있습니다. 전자(JSP, Freemarker)는 서버 템플릿 엔진이라 불리며, 후자(React, Vue)는 클라이언트 템플릿 엔진이라 불립니다. 개발을 시작하는 많은 개발자들이 이 둘간에 많은 오해를 합니다.
해서 '자바스크립트에서 JSP나 Freemarker처럼 자바 코드를 사용할 순 없나요?' 라는 질문이 커뮤니티에 올라오기도 합니다.


결론부터 말하면 FE 의 JS(Nodejs가 아닙니다)가 작동하는 영역JSP 가 작동하는 영역이 다르기 때문인데 JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동되므로, 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달합니다. 이 때 JS코드는 단순한 문자열일 뿐이므로 코드로서 작동하지 않습니다.

호오...전혀 몰랐던 사실. 나도 공부 헛하면 저런 질문 올렸을 거 같다.
이거 이렇게도 할 수 있지 않을까? 하면서 혼자 시간낭비 하고 있었을지도...

JS 의 경우(React, Vue) 브라우저 위에서 작동하므로 서버에서는 Json 이나 Xml 형태의 데이터만 전달하고 클라이언트에서 받은 데이터를 조립하게 됩니다.
물론 최근엔 React 나 Vue 같은 자바 스크립트 프레임워크에서도 서버사이드 랜더링을 지원하는 모습을 볼 수 있지만, 이 책에서 다루지는 않겠습니다.

왜죠....알고싶어요... ㅠ
더 알아봐야겠다 서버사이드랜더링을 선생님이 말해주셨던 거 같다 그래서 공부해봐야겠다.

간단하게 설명하자면 자바 스크립트 프레임워크의 화면 생성 방식을 서버에서 실행하는 것을 이야기 합니다. V8 엔진 라이브러리들이 지원하기 때문입니다. 스프링부트에서 사용할 수 있는 대표적인 기술로는 Nashorn, J2V8 이 있습니다.
다만 스프링 부트를 사용하면서 JS 를 서버사이드에서 랜더링하도록 구현하는 것은 많은 수고가 필요하므로 시작하는 단계에서는 추천하지 않습니다. 스프링 부트에 대한 이해도와 자바스크립트 프레임워크 양쪽 모두에 대한 이해도가 높아졌을 때 시도해보세요!

넵! 이 책 두 번 보고 바로 도전하겠습니다!

머스테치

머스테치
수많은 언어를 지원하는 템플릿 엔진. 자바에서 사용될 때는 서버 템플릿 엔진으로, JS 에서 사용될 때는 클라이언트 템플릿 엔진으로 모두 사용할 수 있다고합니다.

저자가 생각한 각 템플릿 엔진

  • JSP, Velocity : 스프링 부트에서는 권장하지 않는 템플릿 엔진
  • Freemarker : 템플릿 엔진으로는 너무 과하게 많은 기능을 지원합니다. 높은 자유도로 인해 숙련도가 낮을 수록 Freemarker 안에 비즈니스 로직이 추가될 확률이 높습니다.
  • Thymeleaf : 스프링 진영에서 적극적으로 밀고 있지만 문법이 어렵습니다. HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 기존 개발자분들께 높은 허들로 느껴지는 경우가 많습니다. Vue 를 사용해본 경험이 있어 태그 속성 방식이 익숙하다면 선택해도 됩니다.
  • Mustache : 문법 심플, 로직 코드를 사용할 수 없기 때문에 View 의 역할과 서버의 역할이 명확하게 분리됩니다. 무엇보다 인텔리제이 커뮤니티(IntelliJ Community)에서도 플러그인 설치로 간단히 사용할 수 있다.(Thymeleaf, JSP인텔리제이 얼티메이트(Ultimate) 에서만 지원한다)


    템플릿 엔진이 복잡하게 되면 서로 로직을 나눠갖게 되어 유지보수가 굉장히 어렵습니다.

머스타치 설치

  1. 플러그인 마켓플레이스에 들어가서 mustache 를 검색한다.
  2. 설치 후 재시작 해준다.
  3. build.gradle 에 의존성을 등록해준다.
// build.gradle

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

보이는 것처럼 머스타치는 스프링부트에서 공식 지원하는 템플릿 엔진입니다. 의존성하나만추가하면 다른 스타터 패키지와 마찬가지로 추가 설정없이 설치가 끝납니다. 별도 버전을 신경쓰지 않아도 되는 장점!

  1. src / main / resources / templates 를 만들고, index.mustaches 파일을 만들어준다.

이 위치에 머스타치 파일을 두면 스프링 부트에서 자동으로 로딩한다고 한다.

머스테치로 index 페이지 만들기

  1. index.mustaches 에 코드를 적어봅니다.
// index.mustaches

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

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

</html>

여담인데 이거는 단축키 없나 vscode 는 rafce 하면 기본적으로 세팅되잖아. 그런거말이야.

  1. web 에 IndexController.java 를 만들어준다.(dto 안이 아님 web 임)
// IndexController.java

package com.prac.webservice.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;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

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

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

❌ ERROR

build.gradle 에 spring.framework 라고 써버렸다.

test h1 태그에 '웹서비스' 라고 붙여서 써버렸다. 원본엔 '웹 서비스' 라고 띄워놓았다. 그래서 같은 문자열이 없으니 에러발생

✅ 테스트 성공

이번 테스트는 실제로 url 호출 시 페이지의 내용이 제대로 호출되는 지에 대한 테스트 입니다. / 을 호출했을 때 index.mustache 에 포함된 코드들이 있는지 확인하면 됩니다. 하지만 뭔가 아쉬우니 화면이 잘 나오는 확인해봅시다.

좋습니다. 화면으로 확인해보면 더 좋습니다.

Application.java 를 실행시킨 뒤에 localhost:8080 에 접속해보면 이게 표시된다. 만약 html 을 적어넣었다면 html 이 표시됐겠지?

게시글 등록 화면 만들기

PostsApiController 로 API는 구현해놓았으니 바로 화면을 개발해봅시다. 하지만 그냥 HTML만 사용하기에는 멋이 없으니, 오픈소스인 부트스트랩을 이용해 화면을 만들어봅시다.

처음 들어보는 것 같다. 부트스트랩.

부트스트랩, 제이쿼리 같은 프론트엔드 라이브러리를 사용할 수 있는 방법은 크게 2가지가 있습니다. 하나는 외부 CDN 을 사용하는 것이고 하나는 직접 라이브러리를 받아서 사용하는 것입니다. 우리는 외부 CDN 을 사용할 겁니다. 직접 받을 필요없고, 사용방법도 HTML/JSP/Mustache 에 코드 한 줄만 추가하면 되니 굉장히 간단합니다.

✔실제 서비스에서는 이 방법을 잘 사용하지 않습니다. 결국은 외부 서비스에 우리 서비스가 의존하게 되어버리는 꼴이라, CDN을 서비스하는 곳에 문제가 생기면 덩달아 같이 문제가 생기기 때문입니다.

일단, 2개의 라이브러리 부트스트랩과 제이쿼리를 index.mustache 에 추가해야합니다. 하지만 우리는 바로 추가하지 않고 레이아웃 방식으로 추가해보겠습니다 * 레이아웃 방식? 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식

매번 해당 라이브러리를 머스타치 파일에 추가하는 것은 번거로우니 레이아웃 파일들을 만들어 추가합니다.

코드를 작성해보자

  1. src / main / resources / templates 디렉토리에 layout 디렉토리를 추가로 생성하고 header.mustache, footer.mustache 파일을 만든다.
  2. 코드를 추가한다.
<!-- 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>

너무 신기하다. 코드가.
서로 연결된 파일이 아닌데 header부분에서 body가 시작되고 footer 에서 body 가 끝나고 html 이 끝난다.
그리고 저자의 github 에 가서 index 파일을 보면 더 신기하다.

css 와 js 의 위치가 다른데, 페이지 로딩속도를 높이기 위해 css 는 header 에 js 는 footer 에 넣었습니다. HTML 은 위에서부터 아래로 코드가 실행되므로 head 가 다 실행되고 나서 body 가 실행됩니다. 그래서 head 가 로딩되지 않으면 사용자는 백지만 보게됩니다. 그래서 용량이 큰 js 는 body 하단에 둬 화면이 다 그려진 뒤에 호출하는 것이 좋습니다. css 가 불러와지지 않으면 깨진 화면을 사용자가볼 수 있으므로 head 에서 불러옵니다.

오 맞는 말이야 참 신기해.

추가로, bootstrap.js 는 제이쿼리가 꼭 있어야만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성했습니다. 이런 상황을 보통 부트스트랩이 제이쿼리에 의존한다. 고 합니다.

이해가 잘 안되지만, 라이브러리를 비롯해 HTML 태그들이 모두 레이아웃에 추가되었으니 index.mustache 에는 필요한 코드만 남고

<!-- index.mustache -->

{{>layout/header}}

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

...


{{>layout/footer}}

{{>}} 는 현재 머스테치 파일을 기준으로 다른 파일을 가져옵니다.

위 아래에서 layout 의 header 를 가져오고 footer 를 가져오는 거다.

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

글 등록 을 누르면 /posts/save 로 이동하는 코드다.
이 주소에 해당하는 Controller 를 만들어보자.

페이지에 관련된 컨트롤러는 모두 IndexController 를 사용합니다.

  1. IndexController.java 를 채워주자
//  IndexController.java

// 클래스 위에 @RequiredArgsConstructor 를 추가하고

...

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

index.mustache 같이 /posts/save 를 호출하면 posts-save.mustache 가 호출된다.
아니 어떻게 return "posts-save" 를 하는데 posts-save.mustache 가 호출되지? 알고 싶다.

  1. index.mustache 파일 옆에 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}}
  1. 모두 완성되었으니 사용해보자. Application 에서 프로젝트를 실행하고 localhost:8080/ 으로 접속해보자.


어머 글 등록이라는 버튼이 생겼네?!
버튼이 생긴건 호들갑 떨 일이 아니다.
버튼을 누르고 posts-save 로 옮겨졌을 때 호들갑 떨어야한다.

미쳤다...!

하지만 아직 등록버튼은 작동하지 않습니다.

??
뭐예요 ;; 빨리 작동되게 해줘요 ;;

API 를 호출하는 JS 가 전혀 없기 때문입니다. 그래서 src / main / resources 에 static / js / app 디렉토리를 만듭니다.

전혀 이해하지 못했다. API?
API 가 뭔지 이해 못한 것 같다 나는.
JS 가 없다는 건 이해했다.

이게 무슨소리야...?

인텔리제이 커뮤니티는 자바스크립트 지원 안 해...?

속상하다. 일단 코드를 적어보자.

// index.js

var index = {
    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));
        });
    }
};

index.init();

done 메서드는 글 등록이 성공하면 alert 로 글이 등록되었다는 말을 띄우고 다시 '/' 로 메인으로 돌아가게 한다.
fail 은 에러를 띄운다.

footer.mustache 에 코드를 추가해주자

<!-- footer.mustache -->

...

<!--index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>

게시글을 등록해보자!

서버를 실행하고 localhost:8080 에 들어가서 버튼을 누르고 게시글을 작성하고 등록 버튼을 누른다.

❌ 아 왜 안돼 ;

❌ 1. 않이; 책에 오타있자나여 ;;;;

아냐 우리 저자님이 실수할 수도 있지 ㅡㅡ
그 기능을 알지 못하고 수정하지 못한 내가 잘못한 거지
index.js 의 코드는
var index 로 시작해야한다. 책은 var main 으로 시작함 ;;
아니 해야한다 보다는 위 아래가 맞아야 한다. 시작이 main 이면 바닥에 index.init(); 이 main.init(); 으로 바뀌어야 한다. 아니면 시작을 var index 로 바꾸어야 하지.

시작은 var main, 마무리는 index.init 으로 적어서 실행하면
마지막에 index 가 정의되지 않았다고 하면서 posts 페이지로 넘어가면 바로 에러가 발생해있다.

별거 아니다. 하지만 js 를 모르면 이해할 수 없다.
나는 몰랐으니 그냥 오타를 고치는 수준이다.

❌ 2. 이거는 내 코드의 오타

done 인데 don 이라고 적어넣음. 그래서 에러가 표시됐다.

오타만 고치면 된다.

✅ 등록 성공!

게시글을 적고 등록 버튼을 누르면


꺅!!!

확인 버튼을 누르면 메인페이지로 잘 이동한다.
localhost:8080/h2-console 로 들어가 조회해보자.

SELECT * FROM posts

등록도 매우 잘 된 것 같다.

이해한 거 없이 거의따라 적기만 했다.

힘들다.

내일은 조회를 해보자.

profile
BEAT A SHOTGUN

0개의 댓글