🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [39]일차
🚀39일차에는 쿠키와 세션에 대해 더 자세히 배웠다.
쿠키와 세션의 차이를 이해하고 각각 스프링 프레임워크에서 어떻게 쓰이는지를 실습해볼 수 있었다.
쿠키
- 서버가 사용자의 웹 브라우저에 저장하는 작은 데이터 조각
- 클라이언트가 서버에 요청을 할때마다 "쿠키데이터"가 요청과 함께 서버로 전송
- 쿠키를 통해 서버는 사용자의 상태정보를 유지 가능
- 사이트에 대한 설정을 저장하는데 도움이 되며 브라우저 간 세션 유지에 유용
- 보안에 취약할 수 있어 중요 정보는 저장하지 않아야함
쿠키 사용의 과정
첫번째 요청
- 사용자가 웹페이지에 액세스 요청 (쿠키 정보는 없는 상태)
- HTTP 요청 수신 - 서버는 이 요청을 받아서 사용자에 대한 어떠한 정보도 없으므로 기본설정이나 일반 페이지 제공
- HTTP 응답 전송 - 서버는 사용자의 브라우저로 응답을 보냄. 이때 사용자 설저에 따라 쿠키를 설정 가능
(ex. 세션ID나 선호하는 언어) - 페이지 표시 - 브라우저는 서버로부터 받은 응답을 사용자에게 표시
두번째 요청
- 사용자가 다시 페이지를 요청하거나 다른 페이지로 이동할때
브라우저가 이전 요청에서 설정된 쿠키를 함께 전송 - 서버는 HTTP요청을 받아 쿠키 검증 및 사용자 선호, 설정 등을 확인
- 서버는 맞춤정보를 제공 (맞춤형 컨텐츠를 포함하여 응답을 보냄)
- 브라우저는 맞춤형 페이지를 사용자에게 표시
쿠키 관련 어노테이션 : @CookieValue
🚀쿠키 설정 페이지에 접속안되는 문제 해결 (localhost:8080/cookieSetForm)
문제 원인:
<form th:action="/cookieSet" method="get">
- form th:action="/cookieSet"에서의 경로 문제
해결 방법:
<form th:action="@{/cookieSet}" method="get">
- @{} 구문을 사용하여 URL 경로를 안전하게 처리함
@{} 표현식
- @{}는 Thymeleaf의 URL 표현식으로 Spring MVC의 컨텍스트 경로(Context Path)와 동적 파라미터 바인딩까지 지원
❓상대경로 vs 절대경로 vs @{}의 비교
상대경로 | th:action="submitForm" | 현재 페이지의 경로 기준 | /exam → /exam/submitForm |
절대경로 | th:action="/submitForm" | 애플리케이션의 루트(/) 기준 | 항상 /submitForm로 요청 |
Thymeleaf URL 표현식 (@{}) |
th:action="@{/submitForm}" | 컨텍스트 루트 + 절대 경로 기준 | /app 컨텍스트 → /app/submitForm |
정리하자면
상대경로에서는 URL경로에 따라 변동되므로 불안정하고
절대경로에서는 경로가 고정되어 안정적이지만 컨텍스트의 경로를 무시한다.
Thymeleaf의 URL표현식인 @{}은 컨텍스트의 경로를 자동으로 인식하므로 안정적이고 유연하다.
@{}의 "컨텍스트의 경로 자동인식" 예시
<form th:action="@{/submitForm}" method="post">
- /submitForm만 지정해주어도 실제 경로인 /exam/submitForm을 찾아가 실제 URL요청을 처리하게됨
@{}의 "동적 파라미터 바인딩" 예시
<form th:action="@{/user/{id}(id=${user.id})}" method="get">
- 경로에 ${user.id}처럼 값을 넣을 수 있고 user.id가 "james"이면
- localhost:8080/exam/user/james 로 매핑된다.
▶️실습 - 쿠키 설정 페이지
coockieset.html
<body>
<h1>🍪Cookie Test🍪</h1>
<form th:action="@{/cookieSet}" method="get">
<label for="cookieName">쿠키 이름</label>
<input type="text" id="cookieName" name="cookieName"/><br>
<label for="cookieValue">쿠키 값</label>
<input type="text" id="cookieValue" name="cookieValue"/><br>
<button type="submit">쿠키 저장</button>
</form>
</body>
쿠키 관련 Controller
@GetMapping("/cookieSetForm")
public String cookieSetForm(){
return "cookieset";
}
@GetMapping("/cookieSet")
public String cookieSet(@RequestParam(name = "cookieName") String cookieName,
@RequestParam(name = "cookieValue") String cookieValue,
HttpServletResponse response){
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setPath("/");
cookie.setMaxAge(7*24*60*60); // 7일 유지
response.addCookie(cookie); // 반드시 addCookie해주어야 "응답에 쿠키 추가"
return "redirect:/cookieView";
}
@GetMapping("/cookieView") // 이 요청에서는 모든 쿠키를 보여주는 화면을 만듦
public String cookieView(){
return "cookieview";
}
- @GetMapping("/cookieSetForm")
➡️해당 폼이 요청이 되면, 쿠키 이름과 쿠키 값을 받는 화면을 만듦 - Cookie cookie = new Cookie(cookieName, cookieValue);
➡️쿠키 이름과 쿠키 값을 받아서 쿠키를 저장하는 부분
cookieName이라는 이름으로 cookieValue를 가지는 쿠키를 만듦 - return "redirect:/cookieView";
➡️쿠키가 저장되면 /cookieView로 리다이렉트되도록 구현 - response.addCookie(cookie);
➡️ response는 addCookie()메소드를 통해 여러 개의 쿠키를 저장할 수 있고
쿠키를 얻어올때 "배열"로 얻어올 수도 있다.
➡️ 이 쿠키정보들은 addCookie() 메소드를 통해 반드시 응답에 포함시켜야한다.
▶️실습 - 쿠키 설정 페이지 (수정)
@GetMapping("/cookieView")
public String cookieView(HttpServletRequest request, Model model){
// 이 요청에서는 모든 쿠키를 보여주는 화면을 만듦
Cookie[] cookies = request.getCookies();
model.addAttribute("cookies", cookies);
// cookieview HTML의 타임리프에서 쿠키를 받아들이지 못하는 오류로 인한 테스트 for문
for(Cookie cookie : cookies){
System.out.println(cookie.getName() + " ::: ");
System.out.println(cookie.getValue());
}
return "cookieview";
}
- 쿠키 목록을 보여주는 cookieView에서는
HttpServletRequest를 통해 요청을 만들고 Model에 담아 cookieview HTML로 응답하는 것을 구현
- 하지만 쿠키의 형식이 알아볼 수 없는 형태로 출력되고 있다.
▶️실습 - cookieview.html에서 쿠키를 받아들이지 못하는 것 문제해결
@GetMapping("/cookieView")
public String cookieView(HttpServletRequest request, Model model){
//이 요청에서는 모든 쿠키를 보여주는 화면을 만들어 주세요.
Cookie[] cookies = request.getCookies();
List<String> cookieList = new ArrayList<>();
if(cookies != null){
for (Cookie cookie : cookies){
System.out.print(cookie.getName() +":::");
System.out.println(cookie.getValue());
cookieList.add(cookie.getName()+"="+cookie.getValue());
}
model.addAttribute("cookies",cookieList);
}
return "cookieview";
}
- List<String> 을 추가하여 문자열로 쿠키를 가져올 수 있도록함
cookieview.html
<ul>
<li th:each="cookie : ${cookies}">
<span th:text="${cookie}"></span>
</li>
</ul>
- <span th:text="${cookie}">
➡️${cookie}처럼 cookie만 가져오는 것으로 해결 가능
- ArrayList사용 전에는 알아볼 수 없게 출력되던 것을 ArrayList 사용으로 문자열로써 출력되도록 구현
▶️실습 - 쿠키 삭제 구현
쿠키 컨트롤러 - cookieDelete 추가
// 쿠키 삭제
@GetMapping("/cookieDelete")
public String cookieDelete(@RequestParam(name="cookieName") String cookieName,
HttpServletResponse response){
Cookie cookie = new Cookie(cookieName, ""); // Value값을 주지 않음으로 delete기능 구현
cookie.setPath("/");
cookie.setMaxAge(0); // 생명주기를 0으로 줌으로써 delete기능 구현
response.addCookie(cookie);
return "redirect:/cookieView";
}
- Cookie cookie = new Cookie(cookieName, "");
➡️Value값을 주지 않음으로 delete기능 구현
➡️쿠키는 기본 생성자를 사용할 수 없고 반드시 쿠키의 이름과 값은 필수적으로 제공해야한다.
cookieValue 위치에는 서버와 클라이언트 사이 교환될 정보가 위치 - cookie.setMaxAge(-1);
➡️브라우저가 유지되는 동안(=브라우저가 열려있는 동안)쿠키를 유지하는 옵션값
➡️ 0으로 설정하게되면 쿠키는 즉시 삭제 - cookie.setMaxAge(7*24*60*60);
➡️쿠키가 7일동안 유지되는 옵션 값
cookieview.html 수정
<ul>
<li th:each="cookie : ${cookies}">
<span th:text="${cookie}"></span>
<a th:href="@{/cookieDelete(cookieName=${#strings.substringBefore(cookie, '=')})}">쿠키삭제</a>
</li>
</ul>
- 쿠키를 삭제할때는 이름만 필요하므로 (cookieName=...)으로 쿠키이름을 가져온다.
- #strings.substringBefore(cookie, '=')
➡️타임리프문법에서 substring사용 시 substringBefore()사용
➡️cookie를 기준으로 '='를 만나면 잘라내서 '=' 문자 이전까지를 cookieName으로 설정-->
- 쿠키 삭제에 마우스를 올려보면 브라우저의 밑 문구처럼 cookieName=oreo 출력된다.
- 이는 substringBefore()메소드로 잘려진 부분 문자열이 cookieName으로 잘 가져와진 것을 확인 가능
@CookieValue(name = "lastVisit", defaultValue = "N/A")String lastVisit,
스프링에서는 직접 Request로부터 쿠키의 정보를 얻어낼 필요없이 @CookieValue 어노테이션이 대신 수행
인코딩 / 디코딩
- 인코딩 문법 : URLEncoder.encode(시간, 문자셋)
- 쿠키 값에 한글, 공백 포함 등이 되면 안되므로 URLEncoder의 encode()메소드를 이용하여 인코딩
- 인코딩, 디코딩 등은 쿠키가 한글, 공백을 가질 수 없을때 바꿔주는 역할을 수행
ex. google에 “동물” 검색 시 URL창에 “동물”이 한글로 포함되어있지만
실제로는 URL에 “한글”이 들어가는 것이 아니라 인코딩되어 서버에 값이 넣어진다. (인코딩) - 인코딩 Encoding : 값을 변환하여 들여보내는 과정
- 디코딩 Decoding : 값을 변환하여 꺼내는 과정
❓URL과 URI의 차이점
URI (Uniform Resource Identifier)
- 리소스를 식별하는 문자열
- 리소스의 위치(URL) 또는 이름(URN)을 포함할 수 있음
- URI = URL + URN
- ex. https://localhost:8080/exam/submitForm - 이 전체가 URI
ex. submitForm/user - 이와 같은 상대경로도 URI
URL (Uniform Resource Locator)
- 리소스의 위치(주소)를 가리키는 문자열
- 리소스를 찾기 위해 프로토콜(http, ftp)과 경로가 반드시 포함됨
- ex. https://localhost:8080/exam/submitForm - 위치를 알려주므로 URL이기도 함
@RestController
@RequestMapping("/exam")
public class MyController {
// URI - 상대 경로
@GetMapping("/{id}")
public String MyId(){
//...
}
// URL - 절대 경로로 리다이렉트
@GetMapping("/redirect")
public String MyRedirect {
return "redirect:<https://localhost:8080/exam/submitForm>";
}
}
- @GetMapping("/{id}")
➡️URI를사용
➡️서버는 이 경로로 요청이 들어오면 id를 매핑하여 처리 - public String MyRedirect
➡️외부 주소로 리다이렉트하는 메소드 - return "redirect:<https://localhost:8080/exam/submitForm>";
➡️리소스의 실제 위치를 가리키는 URL입니다.
정리하자면
URI | URL | |
스프링 프레임워크 | @GetMapping("/submitForm/{id}") (상대 경로) |
redirect:<https://localhost:8080/exam/submitForm> (절대 경로) |
활용 | @RequestMapping, @GetMapping 등으로 URI를 정의 시 사용 |
외부 페이지로 리다이렉트하거나 API호출 시 활용 |
쿠키의 보안
- 클라이언트 측에서 저장되고 조작될 수 있기에 보안에 취약할 수 있음
- 중요정보(ex.인증토큰)은 암호화하거나 서버 측 세션을 활용하여 관리하는 것이 안전
(세션이 서버에 저장 = WAS(톰캣)에 저장되는 것) - HttpOnly 플래그를 사용하면 자바스크립트를 통한 쿠키 접근을 차단할 수 있어 XSS공격으로부터 보호하는데에 도움
세션
- HttpSession은 세션이 제공하는 형태 중 하나
- 서버 측에서 세션을 통해 사용자와 관련된 데이터를 저장하는 방법을 제공
- 세션은 서버에 생성되며, 세션 ID를 통해 각 클라이언트를 식별하는데
이 세션 ID는 보통 쿠키를 사용하여 클라이언트에 저장되고 각 요청마다 서버로 전송된다. - 따라서 HttpSession은 객체저장, 사용자 로그인 상태 유지 등과 같이 복잡한 데이터 관리에 적합
- 서버 메모리에 데이터를 저장하기 때문에 쿠키보다는 보안이 강화된 특징을 가짐
❓세션과 HttpSession의 차이점
➡️ 세션과 HttpSession 모두 서버와 클라이언트 간 상호작용 시 생성되는 일시적 상태 정보를 저장하는 기술
세션 : 클라이언트 별로 각각의 상태 정보를 저장하는 기술, 서버 내부에 저장되고 특정 시간 사용되지 않으면 소멸
HttpSession : 자바 Servlet이 제공하는 쿠키, 로그인 처리에 주로 사용
세션 접근 방법 2가지
▶️첫번째 방법 : HttpSession에 직접 접근하는 방법
@Controller
@RequestMapping("/session")
public class SessionController {
@GetMapping("/visit") // @RequestMapping 설정으로 visit URI사용 가능
public String visit(HttpSession session, Model model){
//세션에서 방문 횟수 가져오기
//...
return "visitSession";
}
}
- HttpSession은 HttpServletRequest의 getSession() 메소드로 얻어올 수 있다.
- getSession() 메소드 : 세션이 이미 존재하면 있는 것을 리턴해줌
만약 세션이 없다면 새로 생성하여 리턴해줌
➡️이 과정은 내부적으로
1. 세션객체를 생성하고,
2. sessionId를 발급받아 쿠키로 만듦
3. 만들어진 쿠키를 response에 넣는 작업이 실행된다.
Integer visitCount = (Integer)session.getAttribute("visitCount");
- (Integer)session.getAttribute("visitCount");
➡️visitCount는 int형이므로 (Integer)로 형변환하는데 그 이유는
- 이처럼 getAttribute의 메소드가 Object로 리턴되기때문이다.
▶️첫번째 방법 : HttpSession에 직접 접근하는 방법 (수정)
@Controller
@RequestMapping("/session")
public class SessionController {
@GetMapping("/visit") // @RequestMapping 설정으로 visit URI사용 가능
public String visit(HttpSession session, Model model){
//세션에서 방문 횟수 가져오기
// visitCount는 int형일테니 Integer로 얻어온다 (getAttribute의 메소드는 Object로 리턴되기때문)
Integer visitCount = (Integer)session.getAttribute("visitCount");
if(visitCount == null){
visitCount = 0;
}
visitCount++;
session.setAttribute("visitCount", visitCount);
model.addAttribute("visitCount", visitCount);
return "visitSession";
}
}
- session.getAttribute(”visitCount”);
➡️session에 visitCount이 있는지 확인
visitSession.html
<body>
<h1>방문횟수 출력</h1>
<p>방문 횟수 : <span th:text="${visitCount}"></span></p>
</body>
- 새로고침하여 페이지에 접속할수록 방문 횟수 카운트가 올라가는 것을 확인 가능
- 이때 크롬의 시크릿모드같은 기능으로 접속하게되면,
클라이언트/브라우저 마다 방문횟수를 다르게 카운트하는 것을 확인할 수 있다.
▶️두번째 방법 : 스프링이 제공하는 기능으로 세션에 접근
@Controller
@RequestMapping("/session")
public class SessionController {
// 세션 접근 방법 두번째. 스프링에서 제공하는 기능 사용
// =HttpSession을 직접 접근하지 않고 스프링을 이용하는 방식
@ModelAttribute("visitCount")
public Integer initVisitCount(){
return 0;
}
@GetMapping("/visit2")
public String visit(@ModelAttribute("visitCount") Integer visitCount, Model model){
visitCount++;
model.addAttribute("visitCount", visitCount);
return "visitSession";
}
}
- @RequestMapping("/session")
➡️Scope(범위)가 Request
➡️Request Scope : 요청이 들어왔다가 나갈때까지의 생명주기를 가지기때문에 방문횟수는 계속해서 1이 찍힘 - 여기서 @SessionAttributes(”visitCount”) 추가하면
Request Scope가 아닌 Session Scope를 사용하도록 설정 가능 - @SessionAttributes 추가로 방문횟수가 제대로 카운트되어 올라가는 것을 확인가능
<body>
<h1>방문횟수 출력</h1>
<p>방문 횟수 : <span th:text="${visitCount}"></span></p>
</body>
세션 초기화
- 쿠키에서 삭제를 구현할때 setMaxAge(0) 옵션값을 주어 구현했던 것이
세션에서는 세션 초기화 등을 통해서 세션을 삭제하게된다. - 사용자마다 각 세션을 가지고 이 세션들은 각각 ID가 가리키고 있을 것이다.
- 이때 더 이상 세션을 사용하지 않으면
1. 세션을 아예 없애는 방법과 (ex. 로그아웃 시 장바구니도 모두 소멸)
2. 세션에 해당하는 value(값)만 지우는 방법 (ex. 로그아웃해도 장바구니가 유지 - 이 방법을 권장) - 완전히 없애기보단 필요한 부분만 지우는 방식으로 세션 초기화를 구현함
▶️실습 - 세션 초기화 구현
// 세션 초기화
@GetMapping("/resetVisit")
public String resetVisit(SessionStatus status){
status.setComplete();
return "redirect:/session/visit2";
}
- localhost:8080/session/resetVisit
➡️방문횟수를 초기화하고 다시 visit2로 리다이렉트할 수 있다. (방문횟수가 다시 1로 초기화됨) - status.setComplete();
➡️위에서 @SessionAttributes(”visitCount”) 처럼 등록한 값들만 삭제 - setComplete() 메소드 사용의 외로 세션을 삭제하는 다른 방법은
1. session.invalidate(); // 세션 객체삭제로 세션자체를 삭제한다.
2. session.removeAttribute(”visitCount”); // 직접 원하는 값만 삭제할 수 있다.
@ReuestMapping
- SpringMVC에서 가장 핵심적인 어노테이션 중 하나로 요청 URL을 컨트롤러의 특정 메소드에 매핑하는데 사용
- Spring4.3이후 각 HTTP 메소드를 더 간단히 처리할 수 있는 구체적인 어노테이션이 도입되었는데
그것이 바로 @GetMapping(@RequestMapping의 단축형으로 코드의 가독성을 높이고 사용을 단순화)
@RequestMapping(value="/visit", method=RequestMethod.GET)
@GetMapping("/visit")
- 이 두 코드는 같은 기능을 한다. @RequestMapping 어노테이션을 개선하여
@GetMapping 어노테이션이 추가된 것이다. - 그 외 @PostMapping, @PutMapping, @DeleteMapping 등이Spring 4.3이후 추가된 어노테이션들
@RequestParam
- 요청 데이터 처리에 필요한 어노테이션
- URL 쿼리 파라미터나 폼 데이터를 메소드 파라미터로 매핑한다.
@PathVariable
- URI의 일부를 변수로 가져온다. (=URI에는 변수가 포함)
@GetMapping("/users/{userId}")
public String getUser(@PathVariable("userId") String userId){
return "userProfile";
}
- http://localhost:8080/users/kimi
➡️{userId} 위치에는 kimi라는 값이 들어온 것을 확인 가능
View Resolver 뷰 리졸버
- 컨트롤러에서 반환된 뷰 이름을 기반으로 실제 뷰를 찾아내고 렌더링을 위해 뷰 객체를 반환하는 역할을 함
- 컨트롤러가 반환하는 뷰 이름 (ex. home)을 실제 뷰 리소스 (ex.home.html)로 매핑
- 가장 중요한 특징으로 “다중 뷰 리졸버”를 지원
즉 다양한 유형의 뷰를 처리할 수 있도록 뷰 리졸버를 순서대로 배치할 수 있다.
(ex. setOrder()메소드로 우선순위를 지정)
▶️첫번째 방법 - Thymeleaf의 뷰 사용
@Bean
public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine){
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setOrder(1);
return viewResolver;
}
- setTemplateEngine으로 처리할 Thymeleaf 템플릿을 설정
- setOrder메소드로 뷰 리졸버의 우선순위를 지정
▶️두번째 방법 - 사용자 정의 뷰 사용
public class MyCustomView implements View{
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>사용자 정의 뷰!!</h1>");
out.println("<p>나는 사용자 정의 뷰입니다.^^</p>");
out.print("</body></html>");
out.close();
}
}
- public class MyCustomView implements View
➡️View를 구현하는 MyCustomView는 사용자 정의 뷰 - render()
➡️직접 뷰를 만들어서 렌더하게됨 - return "text/html"
➡️text로된 html로 리턴할 것(기본값) - 이렇게 만든 사용자 정의뷰를 사용하기위해서는 설정을 해주어야한다.
▶️실습 - 사용자 정의 뷰 설정
view/MyCustomView 클래스
- 실질적으로 사용자 정의뷰를 만든 곳
public class MyCustomView implements View {
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>사용자 정의 뷰!!</h1>");
out.println("<p>나는 사용자 정의 뷰입니다.^^</p>");
out.print("</body></html>");
out.close();
}
}
- out.println("<html><body>");
➡️HTML형식을 out.println()으로 만들어서 render하는 것을 확인 할 수 있음
config/MyCustomViewResolver 클래스
public class MyCustomViewResolver implements ViewResolver, Ordered {
private int order;
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
// my-prefix로 시작되는 viewName을 가지면 사용자 정의 뷰를 사용하도록 할 것
if(viewName.startsWith("my-prefix")){
return new MyCustomView(); // 사용자 정의 뷰 사용
}
return null; // 다음의 뷰 리졸버가 처리함
}
}
config/WebConfig 클래스
@Configuration
@EnableWebMvc
public class WebConfig {
// 사용자 정의 뷰가 사용될 수 있도록 Bean으로 등록
@Bean
public MyCustomViewResolver myCustomViewResolver(){
MyCustomViewResolver resolver = new MyCustomViewResolver();
resolver.setOrder(0); // 가장 먼저 실행되도록 0의 옵션값을 지정
return resolver;
}
}
controller/MyViewController
- 사용자 정의뷰를 수행할 컨트롤러
@Controller
public class MyViewController {
@GetMapping("/custom")
public String customView(){
return "my-prefix-custom";
}
}
- return “my-prefix-custom”;
my-prefix로 시작하도록 설정해주었으므로 연동
만약 config패키지의 MyCustomViewResolver, WebConfig클래스 등에서 설정하지 않았다면
타임리프를 통해 templates 패키지에서 따로 custom.html을 찾아서 실행했을 것
🚀한글깨짐 현상 해결법
➡️ response.setContentType("text/html")만 설정되어 있고 문자의 인코딩(ex. UTF-8)을 지정하지 않아 깨짐 현상 발생
💡브라우저는 기본적으로 (ISO-8859-1)로 인코딩을 처리하려 하기 때문에 한글이 깨짐
(ex. PrintWriter의 기본 인코딩 문제로 response.getWriter()로 출력 스트림을 얻을 때 인코딩이 ISO-8859-1로 설정됨)
// 명시적으로 UTF-8 설정
response.setCharacterEncoding("UTF-8");
//...
out.println("<head><meta charset='UTF-8'></head>"); // HTML 메타 태그 추가
//...
- 이처럼 UTF-8설정과 HTML 메타태그에 charset으로 UTF-8 설정을 명시해준다.
MyCustomView 클래스 (수정)
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
response.setCharacterEncoding("UTF-8"); // 명시적으로 UTF-8 설정
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head><meta charset='UTF-8'></head>"); // HTML 메타 태그 추가
out.println("<body>");
out.println("<h1>사용자 정의 뷰!!</h1>");
out.println("<p>나는 사용자 정의 뷰입니다.^^</p>");
out.println("</body>");
out.println("</html>");
out.close();
}
- 제대로 출력되는 것을 확인할 수 있다.
🚀 쿠키에 이어 세션까지 다시 스프링 감을 잡는 것에 시간이 좀 걸렸던 회고였다.
쿠키와 세션 개념은 네트워크에서도 중요한 개념이어서 많은 검색으로 이해를 하는데 힘을 쓴 것 같다.
더불어 회고를 통해 이해하기 조금 부족했던 부분들을 채워나갈 수 있었던 시간이었다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_41일차_"람다식 / 스트림 API" (0) | 2025.02.05 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_40일차_"Spring JDBC" (1) | 2025.02.04 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_38일차_"스프링 포워딩/리다이렉팅" (2) | 2025.01.24 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_37일차_"스프링 Thymeleaf" (1) | 2025.01.23 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_36일차_"스프링 MVC" (1) | 2025.01.22 |