스프링 MVC 웹페이지 만들기

뚝딱이·2022년 8월 3일
0

스프링 MVC

목록 보기
10/23

요구사항이 정리되고 디자이너, 웹 퍼블리셔, 백엔드 개발자가 업무를 나누어 진행한다.
디자이너: 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.
웹 퍼블리셔: 다자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.
백엔드 개발자: 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심 비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을그리고, 또 웹 화면의 흐름을 제어한다.

참고
React, Vue.js 같은 웹 클라이언트 기술을 사용하고, 웹 프론트엔드 개발자가 별도로 있으면, 웹 프론트엔드 개발자가 웹 퍼블리셔 역할까지 포함해서 하는 경우도 있다.
웹 클라이언트 기술을 사용하면, 웹 프론트엔드 개발자가 HTML을 동적으로 만드는 역할과 웹 화면의 흐름을 담당한다. 이 경우 백엔드 개발자는 HTML 뷰 템플릿을 직접 만지는 대신에, HTTP API를 통해 웹 클라이언트가 필요로 하는 데이터와 기능을 제공하면 된다.

참고

정적 리소스가 공개되는 /resources/static 폴더에 HTML을 넣어두면, 실제 서비스에서도
공개된다. 서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의하자.

타임리프 간단히 알아보기

타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">

속성 변경 - th:href

th:href="@{/css/bootstrap.min.css}"
href="value1" 을 th:href="value2" 의 값으로 변경한다.
타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다. 만약 값이 없다면 새로 생성한다.
HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 의 값이 href 로 대체되면서 동적으로 변경할 수 있다.대부분의 HTML 속성을 th:xxx 로 변경할 수 있다.

속성 변경 - th:action

HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분한다.
상품 등록 폼: GET /basic/items/add
상품 등록 처리: POST /basic/items/add
이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.

타임리프 핵심

핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.
HTML을 파일로 직접 열었을 때, th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다.
따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.

URL 링크 표현식 - @{...},

th:href="@{/css/bootstrap.min.css}"
@{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용한다. 이것을 URL 링크 표현식이라 한다.
URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
상품 등록 폼으로 이동 속성 변경 - th:onclick
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
여기에는 다음에 설명하는 리터럴 대체 문법이 사용되었다. 자세히 알아보자.

리터럴 대체 - |...|

|...| :이렇게 사용한다.
타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
<span th:text="|Welcome to our application, ${user.name}!|">
결과를 다음과 같이 만들어야 하는데
location.href='/basic/items/add'
그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해진다.
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용할 수 있다.
th:onclick="|location.href='@{/basic/items/add}'|"

반복 출력 - th:each

<tr th:each="item : ${items}">
반복은 th:each 를 사용한다. 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
컬렉션의 수 만큼 .. 이 하위 테그를 포함해서 생성된다.

변수 표현식 - ${...}

<td th:text="${item.price}">10000</td>
모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
프로퍼티 접근법을 사용한다. ( item.getPrice() )

내용 변경 - th:text

<td th:text="${item.price}">10000</td>
내용의 값을 th:text 의 값으로 변경한다.
여기서는 10000을 ${item.price} 의 값으로 변경한다.

URL 링크 표현식2 - @{...},

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
상품 ID를 선택하는 링크를 확인해보자.
URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크: http://localhost:8080/basic/items/1?query=test

URL 링크 간단히

th:href="@{|/basic/items/${item.id}|}"
상품 이름을 선택하는 링크를 확인해보자.
리터럴 대체 문법을 활용해서 간단히 사용할 수도 있다.

참고
타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다. JSP를 생각해보면, JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다. 오직 서버를 통해서 JSP를 열어야 한다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다.

ModelAttribute

요청 파라미터 처리

@ModelAttribute 는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.

Model 추가

@ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다. model.addAttribute()를 해주지 않아도 지정한 객체를 자동으로 넣어준다는 뜻이다.

모델에 데이터를 담을 때는 이름이 필요하다. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용한다. 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
@ModelAttribute("hello") Item item 이름을 hello 로 지정
model.addAttribute("hello", item); 모델에 hello 이름으로 저장

@ModelAttribute 의 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다. 이때 클래스의 첫글자만 소문자로 변경해서 등록한다.

PRG Post/Redirect/Get

전체적인 흐름은 위와 같다. 눈여겨 봐야할 부분이 있는데, 바로 상품 저장이다. 상품을 저장하면 내부 호출로 상품 상세화면을 띄우는데 이때 url의 변경은 일어나지 않는다. 어떤 문제가 있는지 아래 그림들을 살펴보자.

위의 그림은 내부호출로 설계를 할 시에 상품 저장후 새로 고침을 누르면 일어나는 일의 과정이다.
새로고침은 이전의 요청을 다시 보내는데, 상품을 저장하고 상세로 이동해도 내부 호출이 일어나는 것이므로 마지막 요청은 POST이다. 따라서 새로고침을 누르게 되면 한번 더 상품이 저장되는 일이 일어난다.

그렇다면 중복저장이 되지 않기 위해선 어떻게 해야할까. 새로고침을 할 때 마지막 요청이 POST가 아니면 된다.

위의 그림은 Redirect 방식으로 변경한 것이다. Redirect하게 되면 POST 후에 다시 GET으로 요청이 와 마지막 요청이 GET이 된다. 따라서 새로고침을 해도 GET을 요청하기 때문에 중복 저장될 일이 없다.

한가지 주의할 점이 있는데, PRG 패턴을 사용하면 중복 호출을 확율적으로 상당히 많이 줄일 수 있지만, POST의 중복을 궁극적으로 막을 수는 없다.
요청이 오래걸려 GET이 요청되기 전에 새로고침을 누를 수 있기 때문이다. 따라서 중복 호출을 막으려면 서버에서 중복을 체크하도록 로직을 추가로 개발해야 한다.

Redirect는 redirect:/basic/items/" + item.getId()와 같이 사용한다. 이때 인코딩이 안될 수도 있기 때문에 직접 item.getId()를 붙이는 것은 위험하다.

RedirectAttributes

RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다.

    @PostMapping("/add")
    public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);

        return "redirect:/basic/items/{itemId}";
    }

redirect:/basic/items/{itemId}
pathVariable 바인딩: {itemId}
나머지는 쿼리 파라미터로 처리: ?status=true

이렇게 추가한 status는 타임리프를 통해 간편하게 사용할 수 있다.
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
status가 true일 경우 text를 출력한다.

원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 하지만 쿼리 파라미터는 자주 사용해서 타임리프에서 ${param.status}형식으로 직접 지원한다.


출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
백엔드 개발자 지망생

0개의 댓글