HDA 시리즈 #0 - 전통적인 MPA 폼 처리와 그 한계
이번 글에서는 Spring Boot MVC로 간단한 Todo 애플리케이션을 만들면서, 전통적인 MPA(Multi-Page Application) 방식의 폼 처리가 어떻게 동작하는지, 그리고 어떤 불편함이 있는지 살펴본다.
예제 코드: https://github.com/dev-goraebap/hypermedia-driven-demo/tree/step-00
프로젝트 구조
이 예제는 단일 페이지에서 Todo 목록 조회와 추가를 모두 처리한다. /todos 경로 하나로 GET(목록 조회)과 POST(등록)를 담당한다.
src/main/java/com/example/demo/app/
├── controller/
│ └── TodoController.java
├── domain/
│ ├── Todo.java
│ └── TodoService.java
└── dto/
└── TodoCreateRequest.java
src/main/jte/pages/todos/
└── index.jte
컨트롤러 코드
@Controller
@RequestMapping("/todos")
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@GetMapping
public String index(Model model) {
List<Todo> todoList = todoService.getTodoList();
model.addAttribute("todoList", todoList);
return "pages/todos/index";
}
@PostMapping
public String create(
@Valid @ModelAttribute TodoCreateRequest dto,
BindingResult bindingResult,
RedirectAttributes redirectAttributes
) {
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors()
.stream().map(x -> x.getField() + ": " + x.getDefaultMessage())
.toList();
redirectAttributes.addFlashAttribute("errors", errors);
redirectAttributes.addFlashAttribute("todoCreateRequest", dto);
return "redirect:/todos";
}
try {
todoService.save(dto.content());
} catch (ResponseStatusException ex) {
redirectAttributes.addFlashAttribute("errors", List.of(ex.getMessage()));
redirectAttributes.addFlashAttribute("todoCreateRequest", dto);
}
return "redirect:/todos";
}
}
POST 요청 처리 후 항상 redirect:/todos로 리다이렉트한다. 이것이 바로 PRG(Post-Redirect-Get) 패턴이다.
PRG 패턴이란?
PRG는 폼 제출 후 새로고침(F5)으로 인한 중복 요청을 방지하기 위한 패턴이다.
- Post: 사용자가 폼을 제출한다
- Redirect: 서버가 처리 후 다른 URL로 리다이렉트 응답을 보낸다 (HTTP 302/303)
- Get: 브라우저가 리다이렉트된 URL로 GET 요청을 보낸다
만약 POST 후 리다이렉트 없이 바로 페이지를 렌더링하면, 사용자가 새로고침할 때마다 같은 POST 요청이 재전송된다. PRG 패턴을 사용하면 새로고침해도 마지막 GET 요청만 반복되므로 안전하다.
문제점: 리다이렉트 시 데이터 손실
PRG 패턴의 단점은 리다이렉트가 발생하면 이전 요청의 데이터가 사라진다는 것이다. 유효성 검사 실패 시 사용자가 입력했던 값을 다시 보여주려면 별도 처리가 필요하다.
Spring MVC에서는 RedirectAttributes의 Flash Attributes 기능을 사용한다. Flash Attribute는 세션에 임시로 데이터를 저장하고, 리다이렉트 후 한 번만 사용되면 자동으로 삭제된다.
// 에러와 입력값을 Flash Attribute로 저장
redirectAttributes.addFlashAttribute("errors", errors);
redirectAttributes.addFlashAttribute("todoCreateRequest", dto);
템플릿에서 Flash 데이터 사용
@param List<Todo> todoList = new java.util.ArrayList()
@param TodoCreateRequest todoCreateRequest = null
@param List<String> errors = null
!{ var content = todoCreateRequest != null ? todoCreateRequest.content() : ""; }
<form action="/todos" method="post">
<input name="content" value="${content}" placeholder="할일 입력.." />
<button>추가</button>
</form>
@if(errors != null && !errors.isEmpty())
<div class="error-box">
@for(var error : errors)
<p>${error}</p>
@endfor
</div>
@endif
Flash Attribute로 전달된 todoCreateRequest에서 이전 입력값을 꺼내 input의 value로 설정한다. 이렇게 하면 유효성 검사 실패 후에도 사용자가 입력했던 내용이 유지된다.
MPA 폼 처리의 번거로움
이처럼 MPA 방식에서는 폼 요청이 실패했을 때 사용자 경험을 저하시키지 않기 위해 폼 양식을 다시 유지해주는 작업이 번거로운 편이다. 에러 메시지와 입력값을 Flash Attribute에 담고, 템플릿에서 이를 다시 꺼내 폼에 채워 넣는 일련의 과정을 매번 신경 써야 한다.
SPA 프레임워크에 익숙한 개발자라면 이러한 과정이 다소 납득되지 않을 수 있다. SPA에서는 클라이언트 상태가 그대로 유지되기 때문에 폼 입력값이 자연스럽게 보존된다. 하지만 MPA에서는 페이지가 새로 로드되므로 서버에서 명시적으로 데이터를 전달해야만 한다.
깜빡임 현상(Page Flicker)
전통적인 MPA 방식에서 페이지를 이동하거나 리다이렉트가 발생하면 브라우저가 화면을 완전히 비우고 새 페이지를 렌더링한다. 이 과정에서 화면이 순간적으로 하얗게 변하는 깜빡임 현상이 발생한다.
이 깜빡임은 사용자 경험을 저하시킨다. 페이지 전환이 부드럽지 않고 끊기는 느낌을 주며, SPA에 익숙한 사용자에게는 다소 구식으로 느껴질 수 있다.
정리
이번 글에서 살펴본 내용을 정리하면:
- PRG 패턴: POST 후 리다이렉트로 중복 제출 방지
- Flash Attributes: 리다이렉트 간 임시 데이터 전달
- 폼 유지의 번거로움: 에러 발생 시 입력값을 보존하려면 수동 처리 필요
- 깜빡임 현상: 페이지 전환 시 화면이 깜빡이는 현상
다음 글에서는 HTMX를 도입해서 이러한 불편함을 어떻게 해결할 수 있는지 알아본다. 깜빡임 없는 부드러운 페이지 전환과 더 나은 사용자 경험을 제공하는 방법을 살펴볼 예정이다.
이 글은 Hypermedia-Driven Applications 시리즈의 일부입니다.