<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Woojoo's Devlog</title>
    <link>https://woojoo-devlog.tistory.com/</link>
    <description>woojoo-devlog 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 25 Jun 2026 17:12:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>sqaxe1</managingEditor>
    <image>
      <title>Woojoo's Devlog</title>
      <url>https://tistory1.daumcdn.net/tistory/8515443/attach/4e2acf7df2054fff8e9d6a89033e5d91</url>
      <link>https://woojoo-devlog.tistory.com</link>
    </image>
    <item>
      <title>#24 Servlet과 JSP에서 request는 어떻게 사용될까</title>
      <link>https://woojoo-devlog.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet/JSP를 공부하면서 가장 헷갈렸던 부분은 request와 response였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 이렇게만 이해했다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request
&amp;rarr; 브라우저가 서버로 보내는 메시지

response
&amp;rarr; 서버가 브라우저로 보내는 메시지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 방향에서는 맞는 설명이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Servlet/JSP를 실제로 사용하다 보면 request가 단순히 브라우저가 보낸 정보만 담는 객체가 아니라는 것을 알게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request는 요청 처리용 객체다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 서버에 요청을 보내면 Tomcat이 request와 response 객체를 만든다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;브라우저 요청
&amp;rarr; Tomcat
&amp;rarr; request 생성
&amp;rarr; response 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 둘을 Servlet에게 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;doGet(request, response)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 HttpServletRequest는 단순한 요청 메시지 원본이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 정확히는 다음처럼 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이번 요청을 처리하기 위한 컨텍스트 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request 안에는 브라우저가 보낸 정보뿐 아니라, 이번 요청을 처리하는 동안 필요한 여러 정보가 함께 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;요청 주소
요청 방식
파라미터
헤더
쿠키
세션 접근 정보
서버 내부에서 담아둔 attribute
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점이 처음에는 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request에서 입력값 꺼내기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 form으로 보낸 값이나 URL query string 값은 getParameter()로 꺼낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 URL이 다음과 같다면,&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;/memos?title=hello&amp;amp;content=study
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에서는 이렇게 꺼낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String title = request.getParameter(&quot;title&quot;);
String content = request.getParameter(&quot;content&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML form에서도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;input name=&quot;title&quot;&amp;gt;
&amp;lt;textarea name=&quot;content&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 name 값을 기준으로 꺼낸다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;request.getParameter(&quot;title&quot;);
request.getParameter(&quot;content&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 getParameter()는 브라우저가 요청으로 보낸 파라미터 값을 꺼낼 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request에는 주소도 들어 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request에는 입력값만 있는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 주소도 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주소는 getParameter()로 꺼내는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;request.getRequestURI();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 방식은 다음으로 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;request.getMethod();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더는 다음처럼 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;request.getHeader(&quot;User-Agent&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 request 안의 정보는 종류에 따라 꺼내는 메서드가 다르다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;입력값
&amp;rarr; getParameter()

요청 주소
&amp;rarr; getRequestURI()

요청 방식
&amp;rarr; getMethod()

헤더
&amp;rarr; getHeader()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 request 안에 있는 값을 다 같은 방식으로 꺼내는 줄 알았는데, 정보의 종류에 따라 사용하는 메서드가 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request는 저장소 역할도 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 헷갈렸던 부분은 이것이었다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request는 브라우저가 보낸 요청인데,
왜 서버에서 request에 데이터를 저장할까?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에서는 다음처럼 데이터를 담을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;request.setAttribute(&quot;message&quot;, &quot;메모 목록입니다.&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 담은 데이터는 JSP에서 꺼낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;${message}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request는 브라우저가 보낸 정보만 담는 것이 아니라, Servlet에서 JSP로 데이터를 넘기는 임시 저장소 역할도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getParameter와 setAttribute의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getParameter()와 setAttribute()는 반드시 구분해야 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;request.getParameter(&quot;title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 브라우저가 보낸 값을 꺼내는 것이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;form input 값
URL query string 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음 코드는 서버가 request 안에 데이터를 담는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;request.setAttribute(&quot;memos&quot;, memos);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 Servlet에서 JSP로 데이터를 넘길 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;parameter
&amp;rarr; 브라우저가 서버로 보낸 값

attribute
&amp;rarr; 서버가 request에 추가로 담은 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 이해해야 Servlet에서 입력값을 받는 코드와 JSP로 데이터를 넘기는 코드를 구분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 하필 request에 데이터를 담을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet과 JSP는 같은 요청을 이어서 처리한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;브라우저가 /memos 요청
&amp;rarr; Servlet이 먼저 처리
&amp;rarr; JSP가 이어서 화면 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Servlet이 JSP에게 데이터를 넘겨야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모 목록을 JSP에서 출력하려면, Servlet이 먼저 메모 목록을 준비해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;request.setAttribute(&quot;memos&quot;, memoService.getMemos());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JSP로 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
        .forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP는 같은 request를 이어받기 때문에 memos를 꺼내서 화면에 출력할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;${memos}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request는 같은 요청 처리 흐름 안에서 Servlet과 JSP가 공유하는 작업 공간으로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;response는 왜 저장소로 쓰지 않을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response도 Servlet에서 JSP로 함께 넘어간다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;forward(request, response)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 request와 response 둘 다 데이터를 공유하는 저장소처럼 쓰는 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 둘의 역할은 다르다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request
&amp;rarr; 요청 정보 + 서버 내부 데이터 전달 공간

response
&amp;rarr; 브라우저로 나갈 응답 작성 통로
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response에도 값을 설정하긴 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;response.setStatus(200);
response.setHeader(&quot;Cache-Control&quot;, &quot;no-cache&quot;);
response.setContentType(&quot;text/html;charset=UTF-8&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이것은 JSP에게 데이터를 넘기기 위한 저장소가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에게 보낼 응답 정보를 설정하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request attribute
&amp;rarr; Servlet에서 JSP로 데이터 전달

response
&amp;rarr; 브라우저로 나갈 상태 코드, 헤더, 본문 작성
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward에서 request와 response를 둘 다 넘기는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에서 JSP로 이동할 때 다음 코드를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
        .forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 request와 response를 둘 다 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 둘의 역할은 다르다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;request
&amp;rarr; JSP가 Servlet이 담아둔 데이터를 꺼내기 위해 사용

response
&amp;rarr; JSP가 최종 HTML을 브라우저로 보내기 위해 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 둘 다 JSP로 넘어가지만, 둘 다 같은 방식으로 저장소처럼 쓰는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet에서 request로 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에서는 request를 주로 다음 용도로 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 브라우저가 보낸 입력값 꺼내기
2. 요청 정보 확인하기
3. JSP에 넘길 데이터 저장하기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모 작성 요청에서는 브라우저가 보낸 값을 꺼낸다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String title = request.getParameter(&quot;title&quot;);
String content = request.getParameter(&quot;content&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록 화면 요청에서는 JSP에 넘길 데이터를 담는다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;request.setAttribute(&quot;memos&quot;, memoService.getMemos());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Servlet은 request를 읽고, 필요한 데이터를 다시 request에 담는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSP에서 request로 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP에서는 Servlet이 request에 담아둔 데이터를 꺼내서 화면에 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet에서는 다음처럼 데이터를 담는다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;request.setAttribute(&quot;message&quot;, &quot;Servlet에서 준비한 데이터입니다.&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP에서는 이렇게 출력한다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;${message}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Servlet이 request에 message 저장
&amp;rarr; forward
&amp;rarr; JSP가 같은 request에서 message 꺼냄
&amp;rarr; HTML로 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JSP는 request를 통해 Servlet이 준비한 데이터를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request의 수명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request에 저장한 데이터는 오래 유지되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request는 하나의 요청이 처리되는 동안만 살아 있다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;/memos 요청
&amp;rarr; request 생성
&amp;rarr; Servlet 처리
&amp;rarr; JSP 처리
&amp;rarr; 응답 완료
&amp;rarr; request 사라짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침하거나 다른 요청을 보내면 새로운 request가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 request는 장기 저장소가 아니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;request
&amp;rarr; 한 요청 동안만 사용하는 임시 저장소
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래 저장해야 하는 데이터는 나중에 DB나 session 등을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet과 JSP에서 request가 사용되는 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;브라우저가 /memos 요청
&amp;rarr; Tomcat이 request와 response 생성
&amp;rarr; Servlet 실행
&amp;rarr; Servlet이 request에서 필요한 정보 읽음
&amp;rarr; Servlet이 JSP에 넘길 데이터를 request에 저장
&amp;rarr; forward(request, response)
&amp;rarr; JSP가 같은 request에서 데이터 꺼냄
&amp;rarr; JSP가 HTML 생성
&amp;rarr; response를 통해 브라우저로 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 보면 request가 단순히 브라우저에서 서버로 온 메시지만은 아니라는 것을 알 수 있다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/189</guid>
      <comments>https://woojoo-devlog.tistory.com/189#entry189comment</comments>
      <pubDate>Mon, 22 Jun 2026 15:13:32 +0900</pubDate>
    </item>
    <item>
      <title>#23 Servlet에서 JSP로 forward하기</title>
      <link>https://woojoo-devlog.tistory.com/188</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Servlet이 직접 HTML을 출력하던 방식에서 JSP로 화면을 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Servlet 안에서 브라우저에 직접 HTML을 보냈다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response.setContentType(&quot;text/html;charset=UTF-8&quot;);
response.getWriter().println(&quot;&amp;lt;h1&amp;gt;Servlet 메모장&amp;lt;/h1&amp;gt;&quot;);
response.getWriter().println(&quot;&amp;lt;p&amp;gt;Servlet이 정상적으로 실행되었습니다.&amp;lt;/p&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Servlet이 요청 처리와 화면 출력을 모두 담당하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 확인용 화면에서는 문제없지만, 화면이 길어질수록 Java 코드 안에 HTML 문자열이 계속 들어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 역할을 나누기 위해 JSP를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;Servlet
&amp;rarr; 요청 처리 담당

JSP
&amp;rarr; HTML 화면 출력 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP 파일은 다음 위치에 만들었다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;servlet-jsp-memo/src/main/webapp/WEB-INF/views/memos.jsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WEB-INF 안에 둔 이유는 브라우저가 JSP에 직접 접근하지 못하게 하기 위해서다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;브라우저가 직접 JSP 접근
&amp;rarr; 막음

Servlet을 통해 JSP로 이동
&amp;rarr; 허용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 사용자가 이런 주소로 JSP에 직접 들어가는 것이 아니라,&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/WEB-INF/views/memos.jsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 Servlet을 통해 들어가도록 했다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/memos
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 Servlet이 흐름을 제어하고 JSP가 화면을 담당하는 방식에 더 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet 코드 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Servlet이 직접 HTML을 출력했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response.getWriter().println(&quot;&amp;lt;h1&amp;gt;Servlet 메모장&amp;lt;/h1&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 JSP로 요청을 넘겼다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
        .forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
            .forward(request, response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 /memos 요청이 들어오면 MemoServlet이 실행되고, 그 안에서 JSP로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 현재 요청을 서버 내부의 특정 JSP로 넘기기 위한 준비를 한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;이 요청을 /WEB-INF/views/memos.jsp가 이어서 처리하게 하겠다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 getRequestDispatcher()는 요청을 전달할 대상을 찾는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward()는 실제로 요청을 JSP로 넘기는 실행 부분이다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;.forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 브라우저가 새로 요청하는 것이 아니라는 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;브라우저가 새로 요청하는 것이 아니다.
서버 내부에서 Servlet &amp;rarr; JSP로 이동한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 브라우저 주소창은 그대로 유지된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8080/memos
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버 내부에서는 다음 흐름으로 이동한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/memos 요청
&amp;rarr; MemoServlet
&amp;rarr; /WEB-INF/views/memos.jsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 사용자는 /memos로 접속했지만, 실제 화면 출력은 JSP가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request와 response는 누가 만드는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 response가 블랙박스처럼 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 요청을 보내면 Tomcat이 먼저 두 객체를 만든다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request
response
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request는 브라우저가 보낸 요청 정보를 담고 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;요청 URL
요청 방식
파라미터
사용자가 보낸 값
Servlet이 담아둔 데이터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response는 브라우저에게 돌려줄 응답을 만들 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;상태 코드
헤더
Content-Type
응답 본문
출력 통로
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 이 두 객체를 Servlet에게 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;doGet(request, response)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;response는 완성된 응답이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요하게 정리한 부분은 이것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Tomcat이 완성된 response를 JSP에 넘기는 것처럼 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정확히는 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Tomcat이 비어 있는 response 객체를 만든다.
Servlet에게 request와 response를 넘긴다.
Servlet은 JSP로 forward한다.
JSP가 response 안에 HTML을 채운다.
Tomcat이 완성된 response를 브라우저로 보낸다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Tomcat이 화면 내용을 알아서 만들어주는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 응답 객체를 만들고, 최종적으로 브라우저에 보내는 역할을 한다.&lt;br /&gt;응답의 내용은 Servlet이나 JSP가 채운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet이 직접 출력하는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 방식은 Servlet이 직접 response를 채우는 방식이었다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response.getWriter().println(&quot;&amp;lt;h1&amp;gt;Servlet 메모장&amp;lt;/h1&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;Tomcat이 response 생성
&amp;rarr; Servlet이 response에 HTML 작성
&amp;rarr; Tomcat이 response를 브라우저에 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Servlet이 직접 응답 본문을 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSP로 forward하는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP 방식에서는 Servlet이 직접 HTML을 작성하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
        .forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;Tomcat이 response 생성
&amp;rarr; Servlet이 response를 직접 채우지 않음
&amp;rarr; Servlet이 request와 response를 JSP로 넘김
&amp;rarr; JSP가 response에 HTML 작성
&amp;rarr; Tomcat이 response를 브라우저에 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 이것이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Servlet 직접 출력
&amp;rarr; Servlet이 response를 채움

JSP forward
&amp;rarr; JSP가 response를 채움
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSP에서는 response가 안 보이는데 어떻게 응답할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP 파일에는 보통 HTML만 작성한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Servlet/JSP 메모장&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;JSP 화면이 정상적으로 출력되었습니다.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 response를 직접 쓰지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Tomcat은 JSP를 내부적으로 Servlet 코드로 변환해서 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 이런 코드로 바뀐다고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;out.write(&quot;&amp;lt;h1&amp;gt;Servlet/JSP 메모장&amp;lt;/h1&amp;gt;&quot;);
out.write(&quot;&amp;lt;p&amp;gt;JSP 화면이 정상적으로 출력되었습니다.&amp;lt;/p&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 out은 JSP가 제공하는 출력 객체다.&lt;br /&gt;이 out은 내부적으로 response와 연결되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;JSP에 작성한 HTML
&amp;rarr; Tomcat이 내부적으로 out.write(...)로 변환
&amp;rarr; response body에 기록
&amp;rarr; 브라우저로 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JSP에서 직접 response.getWriter()를 쓰지 않아도 응답이 되는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;response에는 본문만 있는 것이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response에는 HTML 본문만 들어가는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;상태 코드
헤더
Content-Type
쿠키
본문
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 정보가 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 JSP가 주로 채우는 것은 HTML 본문이다.&lt;br /&gt;상태 코드나 헤더는 보통 Servlet에서 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(&quot;text/html;charset=UTF-8&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 코드에서 상태 코드와 Content-Type은 어떻게 처리될까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Servlet에서는 이렇게만 작성했다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/memos.jsp&quot;)
        .forward(request, response);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데도 응답이 정상적으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;상태 코드
&amp;rarr; 정상 처리 시 Tomcat이 기본적으로 200 OK 사용

Content-Type
&amp;rarr; JSP의 page 지시어에서 설정

HTML 본문
&amp;rarr; JSP가 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP 맨 위에는 이런 코드가 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지시어가 응답의 Content-Type을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 현재 구조에서는 다음처럼 처리된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;상태 코드 200
&amp;rarr; Tomcat 기본값

Content-Type
&amp;rarr; JSP page 지시어

HTML 본문
&amp;rarr; JSP가 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request를 넘기는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward(request, response)에서 request를 넘기는 이유는 JSP가 Servlet이 받은 요청 정보를 이어서 사용해야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 Servlet에서 다음처럼 데이터를 담을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;request.setAttribute(&quot;memos&quot;, memoService.getMemos());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 JSP에서 그 데이터를 꺼내 화면에 출력할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;Servlet이 request에 데이터 저장
&amp;rarr; forward
&amp;rarr; JSP가 request에서 데이터 꺼내 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request는 Servlet과 JSP 사이에서 데이터를 전달하는 통로로도 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;response를 넘기는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response를 넘기는 이유는 JSP가 최종 HTML 응답을 브라우저로 보내야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP 코드에서는 response가 직접 보이지 않지만, JSP가 생성하는 HTML은 결국 response에 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JSP가 같은 응답 객체를 이어서 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Servlet이 받은 response
&amp;rarr; JSP로 전달
&amp;rarr; JSP가 HTML을 response에 기록
&amp;rarr; Tomcat이 브라우저로 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward와 redirect 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 forward를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;forward(request, response)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 서버 내부 이동이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;브라우저는 /memos로 요청
서버 내부에서 JSP로 이동
주소창은 /memos 그대로
request 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 redirect는 브라우저에게 다시 요청하라고 시키는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;response.sendRedirect(&quot;/memos&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;서버가 브라우저에게 /memos로 다시 가라고 응답
브라우저가 새 요청을 보냄
request 새로 만들어짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Servlet이 받은 request와 response를 JSP가 이어서 사용해야 하므로 forward를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;브라우저가 /memos 요청
&amp;rarr; Tomcat이 request, response 생성
&amp;rarr; MemoServlet.doGet(request, response) 실행
&amp;rarr; Servlet이 getRequestDispatcher로 JSP 위치 지정
&amp;rarr; forward(request, response)로 JSP에 전달
&amp;rarr; JSP가 HTML을 response에 작성
&amp;rarr; Tomcat이 완성된 response를 브라우저에 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Servlet이 직접 HTML을 쓰지 않고, JSP가 화면을 담당하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Servlet과 JSP의 역할을 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;Servlet이 요청 처리와 HTML 출력 모두 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;Servlet
&amp;rarr; 요청 처리와 흐름 제어 담당

JSP
&amp;rarr; HTML 화면 출력 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`request.getRequestDispatcher(...).forward(request, response)`는 단순히 외워야 하는 코드가 아니라, 다음 의미를 가진다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;현재 요청과 응답을 유지한 채
서버 내부에서 JSP에게 화면 출력을 맡긴다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 response는 완성된 결과물이 아니라, 브라우저로 나갈 응답을 담는 객체다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Tomcat이 response를 만든다.
Servlet 또는 JSP가 response를 채운다.
Tomcat이 최종 response를 브라우저로 보낸다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 이해하니 Servlet/JSP가 블랙박스처럼 느껴지는 부분이 조금 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 Servlet에서 메모 데이터를 request에 담고, JSP에서 그 데이터를 출력하는 방식으로 확장해볼 예정이다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/188</guid>
      <comments>https://woojoo-devlog.tistory.com/188#entry188comment</comments>
      <pubDate>Sun, 21 Jun 2026 17:51:52 +0900</pubDate>
    </item>
    <item>
      <title>#22 Tomcat으로 Servlet 실행하기</title>
      <link>https://woojoo-devlog.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Java 콘솔 메모장에서 한 단계 넘어가, 브라우저 요청을 Java Servlet이 처리하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Servlet이 정상적으로 실행되는지 확인하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 콘솔에서 Java 프로그램을 실행했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사용자 입력
&amp;rarr; Main
&amp;rarr; MemoService
&amp;rarr; 콘솔 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계부터는 브라우저에서 요청을 보내고, Tomcat이 그 요청을 받아 Servlet을 실행하는 구조로 넘어간다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;브라우저 요청
&amp;rarr; Tomcat
&amp;rarr; Servlet
&amp;rarr; 브라우저 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 콘솔에서 직접 실행하던 Java 코드가 아니라, 브라우저 요청을 Java 서버가 받는 구조를 처음 확인하는 것이 목적이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet/JSP 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memo Evolution 프로젝트 안에 새 Maven 모듈을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;servlet-jsp-memo/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 전체 학습 프로젝트 구조는 다음처럼 발전했다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;Memo Evolution/
├─ react-memo/
├─ java-console-memo/
└─ servlet-jsp-memo/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;servlet-jsp-memo는 Servlet/JSP 학습을 위한 Maven 웹 프로젝트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 java-console-memo가 콘솔에서 Java CRUD 흐름을 연습하기 위한 단계였다면,&lt;br /&gt;이번 servlet-jsp-memo는 브라우저 요청을 Java 코드로 처리하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pom.xml 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet을 사용하기 위해 pom.xml에 Servlet API 의존성을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 jakarta.servlet-api를 사용했지만, 현재 사용 중인 Tomcat 버전이 9였기 때문에 맞지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat 9는 javax.servlet 기반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종적으로는 다음 의존성을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;javax.servlet&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;javax.servlet-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.0.1&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 Tomcat 버전과 Servlet API 패키지를 맞춰야 한다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Tomcat 9
&amp;rarr; javax.servlet 사용

Tomcat 10 이상
&amp;rarr; jakarta.servlet 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 버전 차이를 맞추지 않으면 Servlet이 정상적으로 등록되지 않아 404가 발생할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Servlet 클래스 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 요청을 처리하기 위해 MemoServlet 클래스를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;servlet-jsp-memo/src/main/java/com/memo/MemoServlet.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;package com.memo;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(&quot;/memos&quot;)
public class MemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType(&quot;text/html;charset=UTF-8&quot;);
        response.getWriter().println(&quot;&amp;lt;h1&amp;gt;Servlet 메모장&amp;lt;/h1&amp;gt;&quot;);
        response.getWriter().println(&quot;&amp;lt;p&amp;gt;Servlet이 정상적으로 실행되었습니다.&amp;lt;/p&amp;gt;&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 메모 목록을 출력하거나 저장하지 않았다.&lt;br /&gt;Servlet이 요청을 받고 HTML 응답을 돌려주는지만 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HttpServlet 상속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoServlet은 HttpServlet을 상속한다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class MemoServlet extends HttpServlet
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet은 일반 Java 클래스처럼 main()으로 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 콘솔 프로그램은 main 메서드가 시작점이었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public static void main(String[] args) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Servlet은 Tomcat 같은 Servlet Container가 실행해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Java 콘솔
&amp;rarr; main 메서드가 시작점

Servlet
&amp;rarr; Tomcat이 요청에 맞는 Servlet을 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점이 콘솔 프로그램과 Servlet의 큰 차이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@WebServlet으로 URL 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WebServlet은 URL과 Servlet을 연결한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@WebServlet(&quot;/memos&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;/memos 요청이 들어오면 MemoServlet이 처리한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 브라우저에서 다음 주소로 요청하면,&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8080/memos
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 /memos에 매핑된 MemoServlet을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;doGet 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창으로 접속하는 요청은 보통 GET 요청이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 /memos로 접속하면 doGet()이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /memos
&amp;rarr; doGet 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 doGet() 안에서 간단한 HTML 응답을 만들어 브라우저로 보냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;request와 response&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;doGet()에는 두 개의 중요한 객체가 들어온다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;HttpServletRequest request
HttpServletResponse response
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request는 브라우저가 보낸 요청 정보를 담고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 정보가 들어갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;요청 URL
요청 파라미터
form 데이터
요청 방식
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response는 브라우저에 돌려줄 응답을 만들 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 응답을 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;HTML 응답
문자 인코딩
상태 코드
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 response를 사용해 간단한 HTML을 보냈다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response.setContentType(&quot;text/html;charset=UTF-8&quot;);
response.getWriter().println(&quot;&amp;lt;h1&amp;gt;Servlet 메모장&amp;lt;/h1&amp;gt;&quot;);
response.getWriter().println(&quot;&amp;lt;p&amp;gt;Servlet이 정상적으로 실행되었습니다.&amp;lt;/p&amp;gt;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 브라우저에게 HTML 응답을 보내는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tomcat 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet은 혼자 실행되지 않기 때문에 Tomcat이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ에서 Tomcat 실행 설정을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Run
&amp;rarr; Edit Configurations
&amp;rarr; Tomcat Server
&amp;rarr; Local
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Deployment 탭에서 다음 artifact를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;servlet-jsp-memo:war exploded&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;war &amp;rarr; Tomcat에 배포 가능한 웹 애플리케이션 형식&lt;br /&gt;exploded &amp;rarr;&amp;nbsp;압축하지&amp;nbsp;않고&amp;nbsp;폴더&amp;nbsp;형태로&amp;nbsp;펼쳐진&amp;nbsp;배포&amp;nbsp;방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라서 보통 개발중에는 exploded를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat 실행 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Tomcat 실행
&amp;rarr; servlet-jsp-memo 배포
&amp;rarr; 브라우저 요청 대기
&amp;rarr; /memos 요청 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Tomcat은 브라우저 요청을 받아 적절한 Servlet으로 전달하는 역할을 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8080 포트 충돌 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Tomcat 실행 시 8080 포트 충돌이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에는 다음과 같은 메시지가 있었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Caused by: java.net.BindException: Address already in use
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뜻은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;8080 포트를 이미 다른 프로그램이 사용 중이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 Docker 컨테이너가 8080 포트를 사용하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;heartdoor-server-backend-1
0.0.0.0:8080-&amp;gt;8080/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해당 컨테이너를 중지했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;docker stop heartdoor-server-backend-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Tomcat이 정상적으로 8080 포트에서 실행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 서버를 실행할 때는 포트 충돌도 확인해야 한다는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;404 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat은 정상 실행되고 artifact도 배포되었지만, 처음에는 /memos 접속 시 404가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 Tomcat 버전과 Servlet API 패키지가 맞지 않았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 코드와 의존성은 jakarta.servlet 기준이었다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import jakarta.servlet.http.HttpServlet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용 중인 Tomcat은 9였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Tomcat 9
&amp;rarr; javax.servlet 기반
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 import를 아래처럼 수정했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import javax.servlet.http.HttpServlet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 pom.xml도 javax.servlet-api로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;groupId&amp;gt;javax.servlet&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;javax.servlet-api&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;4.0.1&amp;lt;/version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 Tomcat을 다시 실행하자 /memos 요청이 정상 처리되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정상 실행 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 다음 주소로 접속했다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:8080/memos
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 다음 화면이 출력되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;1468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ivv6t/dJMcagspnSm/Esv7SwAmfEkobajqpxWeX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ivv6t/dJMcagspnSm/Esv7SwAmfEkobajqpxWeX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ivv6t/dJMcagspnSm/Esv7SwAmfEkobajqpxWeX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvv6t%2FdJMcagspnSm%2FEsv7SwAmfEkobajqpxWeX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2116&quot; height=&quot;1468&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;1468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 브라우저 요청이 Java Servlet까지 도달하고, Servlet이 응답을 만들어 브라우저에 돌려주는 흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계는 Servlet/JSP 학습의 첫 단계였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 메모 CRUD를 웹으로 구현한 것은 아니지만, 브라우저 요청이 Tomcat을 거쳐 Java Servlet까지 도달하는 흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;브라우저 요청
&amp;rarr; Tomcat
&amp;rarr; MemoServlet
&amp;rarr; HTML 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Java 코드가 콘솔에서만 실행되는 것이 아니라, 브라우저 요청을 처리하는 서버 코드로 동작하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 이 Servlet 안에서 메모 목록을 다루고, JSP를 이용해 화면을 분리하는 방향으로 확장할 수 있을 것 같다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/187</guid>
      <comments>https://woojoo-devlog.tistory.com/187#entry187comment</comments>
      <pubDate>Sun, 21 Jun 2026 16:45:23 +0900</pubDate>
    </item>
    <item>
      <title>#21 Java 콘솔 메모장: MemoService로 로직 분리하기</title>
      <link>https://woojoo-devlog.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Main.java에 있던 메모 관리 로직을 MemoService 클래스로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 새로 추가한 것은 아니다.&lt;br /&gt;이미 구현되어 있던 메모 작성, 목록 보기, 수정, 삭제 기능의 위치를 나누는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Main.java 안에서 입력 처리와 메모 데이터 관리가 모두 이루어졌다.&lt;br /&gt;이번에는 Main은 사용자 입력과 출력 흐름을 담당하고, MemoService는 메모 데이터를 관리하도록 역할을 나누었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 구조의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Main.java가 너무 많은 일을 하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메뉴 출력
사용자 입력 받기
메모 저장
메모 목록 출력
메모 수정
메모 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Main이 사용자 입력 흐름도 처리하고, 메모 데이터 관리도 직접 하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이전에는 Main.java 안에서 직접 리스트를 가지고 있었다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;List&amp;lt;Memo&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 직접 메모를 추가하거나 수정하거나 삭제했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;memos.add(memo);
memos.set(index, updateMemo);
memos.remove(index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식도 동작은 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기능이 늘어날수록 Main.java가 계속 길어지고, 어떤 코드는 사용자 입력 처리인지, 어떤 코드는 메모 데이터 처리인지 구분하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 메모 관리 로직을 별도 클래스로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분리 후 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 메모 관리 로직을 MemoService로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할은 다음처럼 나누었다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Main
&amp;rarr; 사용자 입력과 출력 담당

MemoService
&amp;rarr; 메모 데이터 관리 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;java-console-memo/src/
├─ Main.java
├─ Memo.java
└─ MemoService.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memo는 메모 하나의 데이터를 표현하고,&lt;br /&gt;MemoService는 여러 메모를 저장하고 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MemoService 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoService는 메모 목록을 직접 가지고 있다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

public class MemoService {
    private List&amp;lt;Memo&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 memos 리스트는 Main이 아니라 MemoService 안에 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Main이 직접 메모 목록을 관리하지 않는다.
MemoService가 메모 목록을 관리한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면서 Main의 책임을 줄이고, 메모 데이터 관리 책임을 MemoService로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 추가 로직 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Main에서 직접 메모 객체를 만들고 리스트에 추가했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Memo memo = new Memo(title, content);
memos.add(memo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 MemoService 안에 메모 추가 메서드를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public void addMemo(String title, String content) {
    Memo memo = new Memo(title, content);
    memos.add(memo);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main에서는 직접 리스트에 추가하지 않고, 메서드를 호출만 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;memoService.addMemo(title, content);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음처럼 바뀌었다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Main
&amp;rarr; 제목과 내용을 입력받는다.
&amp;rarr; memoService.addMemo(title, content)를 호출한다.

MemoService
&amp;rarr; Memo 객체를 만든다.
&amp;rarr; memos 리스트에 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 목록 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록을 출력하려면 Main에서 메모 목록을 가져와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MemoService에 getMemos()를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public List&amp;lt;Memo&amp;gt; getMemos() {
    return memos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main에서는 이 메서드를 통해 메모 목록을 가져온다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Memo memo = memoService.getMemos().get(i);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 getMemos()로 리스트를 그대로 반환하고 있다.&lt;br /&gt;나중에는 외부에서 리스트를 직접 수정하지 못하게 구조를 더 다듬을 수도 있겠지만, 지금 단계에서는 역할 분리를 먼저 연습하는 데 집중했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 수정 로직 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Main에서 직접 리스트의 특정 위치를 교체했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;Memo updateMemo = new Memo(newTitle, newContent);
memos.set(index, updateMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 MemoService에 수정 메서드를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public void editMemo(int index, String newTitle, String newContent) {
    Memo updatedMemo = new Memo(newTitle, newContent);
    memos.set(index, updatedMemo);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main에서는 이렇게 호출한다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;memoService.editMemo(index, newTitle, newContent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Main은 &amp;ldquo;몇 번째 메모를 어떤 값으로 수정할지&amp;rdquo;만 전달하고, 실제 리스트 교체는 MemoService가 처리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 삭제 로직 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Main에서 직접 삭제했다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;memos.remove(index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 MemoService에 삭제 메서드를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public void deleteMemo(int index) {
    memos.remove(index);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main에서는 이렇게 호출한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;memoService.deleteMemo(index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제할 index는 Main에서 사용자 입력을 받아 계산하고, 실제 삭제 처리는 MemoService에게 맡긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비어 있는지 확인, 크기 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main에서는 메모가 비어 있는지 확인하거나, 메모 개수를 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 목록을 출력할 때 메모가 하나도 없으면 안내 문구를 보여줘야 한다.&lt;br /&gt;수정이나 삭제를 할 때도 입력한 번호가 유효한지 확인하려면 전체 개수가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MemoService에 다음 메서드도 만들었다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public boolean isEmpty() {
    return memos.isEmpty();
}

public int size() {
    return memos.size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Main은 직접 리스트를 가지고 있지 않지만, 필요한 정보는 MemoService에게 물어볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;memoService.isEmpty()
memoService.size()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Main.java의 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Main.java에서는 List&amp;lt;Memo&amp;gt;를 직접 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 다음처럼 작성했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;List&amp;lt;Memo&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 MemoService 객체를 만든다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;MemoService memoService = new MemoService();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 기능을 실행할 때 memos를 직접 넘기는 대신 memoService를 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;addMemo(scanner, memoService);
printMemos(memoService);
editMemo(scanner, memoService);
deleteMemo(scanner, memoService);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Main은 메모 목록 자체를 직접 다루기보다, MemoService를 통해 메모 기능을 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;역할 분리 후 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 작성 흐름은 이렇게 바뀌었다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Main
&amp;rarr; 제목과 내용을 입력받는다.
&amp;rarr; memoService.addMemo(title, content)를 호출한다.

MemoService
&amp;rarr; Memo 객체를 만든다.
&amp;rarr; memos 리스트에 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 수정 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;Main
&amp;rarr; 수정할 번호와 새 제목/내용을 입력받는다.
&amp;rarr; memoService.editMemo(index, newTitle, newContent)를 호출한다.

MemoService
&amp;rarr; 해당 index의 메모를 새 Memo 객체로 교체한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 삭제 흐름도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Main
&amp;rarr; 삭제할 번호를 입력받는다.
&amp;rarr; memoService.deleteMemo(index)를 호출한다.

MemoService
&amp;rarr; 해당 index의 메모를 삭제한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기능에서 실제 데이터 처리는 MemoService가 담당하고, Main은 사용자와 주고받는 흐름에 집중한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Java 콘솔 메모장은 다음 구조를 가진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Main.java
&amp;rarr; 메뉴 출력, 사용자 입력, 결과 출력

Memo.java
&amp;rarr; 메모 하나의 데이터 표현

MemoService.java
&amp;rarr; 메모 목록 관리, 추가, 수정, 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능은 이전과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메모 작성
메모 목록 보기
메모 수정
메모 삭제
프로그램 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기능이 들어 있는 위치가 정리되었다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/186</guid>
      <comments>https://woojoo-devlog.tistory.com/186#entry186comment</comments>
      <pubDate>Sun, 21 Jun 2026 12:35:19 +0900</pubDate>
    </item>
    <item>
      <title>#20 Java 콘솔 메모장 : String 배열에서 Memo 클래스로 변경하기</title>
      <link>https://woojoo-devlog.tistory.com/185</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 메모 하나를 String[] 배열이 아니라 Memo 클래스로 표현하도록 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서는 Java 콘솔에서 메모 작성, 목록 보기, 수정, 삭제 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 메모 하나는 다음처럼 문자열 배열로 저장했다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;String[] memo = {title, content};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 값을 꺼낼 때는 배열 인덱스를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memo[0] // 제목
memo[1] // 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식도 동작은 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드를 볼 때 memo[0]이 제목인지, memo[1]이 내용인지 계속 기억해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 메모 하나를 더 명확하게 표현하기 위해 Memo 클래스를 만들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 메모 하나를 String[] 배열로 다뤘다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;String[] memo = {title, content};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 메모 목록은 다음처럼 선언했다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;List&amp;lt;String[]&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 메모 하나가 배열이기 때문에 제목과 내용을 인덱스로 구분해야 했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memo[0] // 제목
memo[1] // 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 프로그램에서는 크게 문제되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드가 조금만 길어져도 memo[0]이 무엇을 의미하는지 바로 보이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 목록을 출력할 때도 다음처럼 작성해야 했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;System.out.println((i + 1) + &quot;. &quot; + memo[0]);
System.out.println(memo[1]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 하지만 의미가 명확하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Memo 클래스 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메모 하나를 표현하는 Memo 클래스를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Memo {
    private String title;
    private String content;

    public Memo(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메모 하나는 단순한 문자열 배열이 아니라 Memo 객체가 되었다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;Memo
&amp;rarr; title
&amp;rarr; content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;title과 content라는 이름이 생겼기 때문에, 메모가 어떤 데이터를 가지고 있는지 더 명확해졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필드를 private으로 둔 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memo 클래스 안의 필드는 private으로 만들었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;private String title;
private String content;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private으로 두면 클래스 밖에서 필드에 직접 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 값을 가져올 때는 getter 메서드를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public String getTitle() {
    return title;
}

public String getContent() {
    return content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 단순히 값을 꺼내는 정도지만, 이렇게 작성하면 나중에 클래스 내부 구조를 조금 더 안전하게 관리할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memo 객체를 만들 때는 생성자를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public Memo(String title, String content) {
    this.title = title;
    this.content = content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 생성자는 제목과 내용을 받아서 Memo 객체 안에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 코드를 실행하면,&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Memo memo = new Memo(title, content);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력받은 title, content 값이 Memo 객체의 필드에 들어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;List 타입 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 문자열 배열 목록을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;List&amp;lt;String[]&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 Memo 객체 목록으로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;List&amp;lt;Memo&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, memos는 이제 문자열 배열이 아니라 Memo 객체들을 저장하는 리스트다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;기존:
List&amp;lt;String[]&amp;gt;

변경 후:
List&amp;lt;Memo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변화만으로도 코드의 의미가 더 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memos 리스트 안에는 메모 객체들이 들어간다는 것을 타입만 봐도 알 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 작성 코드 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 제목과 내용을 배열로 묶어서 저장했다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;String[] memo = {title, content};
memos.add(memo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 Memo 객체를 생성해서 저장한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Memo memo = new Memo(title, content);
memos.add(memo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;제목 입력
&amp;rarr; 내용 입력
&amp;rarr; Memo 객체 생성
&amp;rarr; memos 리스트에 추가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 메모 하나를 배열로 만들었는지, 클래스로 만들었는지다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 목록 출력 코드 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 배열 인덱스로 제목과 내용을 꺼냈다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;System.out.println((i + 1) + &quot;. &quot; + memo[0]);
System.out.println(memo[1]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 getter 메서드를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;System.out.println((i + 1) + &quot;. &quot; + memo.getTitle());
System.out.println(memo.getContent());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드를 읽었을 때 의미가 더 분명하다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;memo.getTitle()
&amp;rarr; 메모 제목을 가져온다.

memo.getContent()
&amp;rarr; 메모 내용을 가져온다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo[0], memo[1]보다 훨씬 읽기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 수정 코드 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 때도 기존에는 새 배열을 만들어 교체했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;String[] updateMemo = {newTitle, newContent};
memos.set(index, updateMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 새 Memo 객체를 만들어 교체했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;Memo updateMemo = new Memo(newTitle, newContent);
memos.set(index, updateMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 해당 위치의 기존 메모 객체를 새 메모 객체로 바꾸는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 흐름은 이전과 같다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;수정할 번호 입력
&amp;rarr; index 계산
&amp;rarr; 새 제목 입력
&amp;rarr; 새 내용 입력
&amp;rarr; 새 Memo 객체 생성
&amp;rarr; memos.set(index, updateMemo)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 것은 데이터 표현 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 코드는 거의 그대로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 기능은 크게 바뀌지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제는 특정 index의 메모를 리스트에서 제거하는 작업이기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;memos.remove(index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 List&amp;lt;String[]&amp;gt;에서 제거했고, 이제는 List&amp;lt;Memo&amp;gt;에서 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 안에 들어 있는 타입은 바뀌었지만, 삭제 흐름 자체는 그대로다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 가장 크게 바뀐 점은 메모 하나를 표현하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;메모 하나 = String 배열

memo[0] = 제목
memo[1] = 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후에는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;메모 하나 = Memo 객체

memo.getTitle() = 제목
memo.getContent() = 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 새로 늘어난 것은 아니지만, 코드의 의미가 더 명확해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 String[]만으로도 충분해 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모에 필요한 정보가 늘어나면 배열 방식은 금방 불편해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 나중에 다음 정보가 추가될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;id
작성일
수정일
중요 표시
카테고리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열로 계속 관리한다면 이런 식으로 외워야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memo[0] // 제목
memo[1] // 내용
memo[2] // 작성일
memo[3] // 수정일
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 코드가 점점 헷갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 클래스를 사용하면 필드 이름으로 의미를 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;memo.getTitle()
memo.getContent()
memo.getCreatedAt()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서 Memo 클래스로 바꾼 것은 나중에 구조를 확장하기 위한 준비이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 파일 저장이나 DB 저장은 없다.&lt;br /&gt;프로그램을 종료하면 메모는 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 단계에서는 저장 기능보다, Java에서 데이터를 더 명확한 구조로 표현하는 데 집중했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 이 구조를 바탕으로 기능을 더 나누거나, 메모 저장 방식을 개선해볼 수 있을 것 같다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/185</guid>
      <comments>https://woojoo-devlog.tistory.com/185#entry185comment</comments>
      <pubDate>Sun, 21 Jun 2026 09:40:05 +0900</pubDate>
    </item>
    <item>
      <title>#19 Java 콘솔 메모장: CRUD기능 구현하기</title>
      <link>https://woojoo-devlog.tistory.com/183</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Java 콘솔 메모장에 실제 CRUD 기능을 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서는 메뉴만 출력하고, 각 메뉴를 선택했을 때 &amp;ldquo;아직 구현되지 않았습니다.&amp;rdquo;라는 안내 문구만 출력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 사용자가 콘솔에서 직접 메모를 작성하고, 목록을 보고, 수정하고, 삭제할 수 있도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 프로그램 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 콘솔 메모장은 다음과 같은 메뉴를 가진다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;===== 메모장 =====
1. 메모 작성
2. 메모 목록 보기
3. 메모 수정
4. 메모 삭제
0. 종료
메뉴 선택:
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 번호를 입력해서 원하는 기능을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1 &amp;rarr; 메모 작성
2 &amp;rarr; 메모 목록 보기
3 &amp;rarr; 메모 수정
4 &amp;rarr; 메모 삭제
0 &amp;rarr; 프로그램 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 메뉴만 있었고 실제 기능은 없었다.&lt;br /&gt;이번 단계부터는 각 메뉴가 실제로 메모 데이터를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모를 저장할 List 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 메모를 저장하기 위해 List를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;List&amp;lt;String[]&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 memos는 전체 메모 목록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 하나는 아직 별도의 클래스로 만들지 않고, 문자열 배열로 저장했다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;String[] memo = {title, content};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조에서는 메모 하나가 다음처럼 구성된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memo[0] = 제목
memo[1] = 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 전체 메모 목록은 개념적으로 이런 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;memos
&amp;rarr; [
    [&quot;제목1&quot;, &quot;내용1&quot;],
    [&quot;제목2&quot;, &quot;내용2&quot;],
    [&quot;제목3&quot;, &quot;내용3&quot;]
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서 배열에 메모 객체를 넣었던 것과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 다음처럼 추가했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;memos.push(newMemo)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 다음처럼 추가한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;memos.add(memo);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 처리는 nextLine으로 통일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 메뉴 입력에 scanner.nextInt()를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이후 제목과 내용 입력까지 함께 처리하다 보니 nextInt()와 nextLine()을 섞어 쓰는 것이 헷갈릴 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 입력 처리를 nextLine() 기준으로 통일했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int menu = Integer.parseInt(scanner.nextLine());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 사용자가 입력한 문자열을 숫자로 변환해서 메뉴 번호로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목과 내용도 nextLine()으로 입력받았다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String title = scanner.nextLine();
String content = scanner.nextLine();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 메뉴 입력, 제목 입력, 내용 입력 흐름을 한 방식으로 처리할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1번: 메모 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 메뉴를 선택하면 제목과 내용을 입력받는다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;if (menu == 1) {
    System.out.println(&quot;제목을 입력해 주세요&quot;);
    String title = scanner.nextLine();

    System.out.println(&quot;내용을 입력해 주세요&quot;);
    String content = scanner.nextLine();

    String[] memo = {title, content};
    memos.add(memo);

    System.out.println(&quot;저장 완료되었습니다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;제목 입력 안내
&amp;rarr; 제목 입력받기
&amp;rarr; 내용 입력 안내
&amp;rarr; 내용 입력받기
&amp;rarr; 제목과 내용을 String[]로 묶기
&amp;rarr; memos에 추가하기
&amp;rarr; 저장 완료 메시지 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 제목과 내용을 하나의 메모로 묶는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;String[] memo = {title, content};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 메모 하나를 전체 메모 목록에 추가한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;memos.add(memo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로그램이 실행 중인 동안에는 작성한 메모가 memos 리스트 안에 저장된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2번: 메모 목록 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 메뉴를 선택하면 저장된 메모 목록을 출력한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;else if (menu == 2) {
    if (memos.isEmpty()) {
        System.out.println(&quot;작성된 메모가 없습니다.&quot;);
    } else {
        for (int i = 0; i &amp;lt; memos.size(); i++) {
            String[] memo = memos.get(i);

            System.out.println((i + 1) + &quot;. &quot; + memo[0]);
            System.out.println(memo[1]);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 메모가 비어 있는지 확인했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;memos.isEmpty()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모가 없으면 안내 문구를 출력한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;작성된 메모가 없습니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모가 있으면 for문으로 전체 메모를 출력한다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; memos.size(); i++)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 memos.get(i)로 i번째 메모를 꺼낸다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;String[] memo = memos.get(i);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력할 때는 i + 1을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;System.out.println((i + 1) + &quot;. &quot; + memo[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 리스트의 index는 0부터 시작하지만, 사용자에게 보여줄 번호는 1부터 시작하는 것이 자연스럽기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;리스트 index: 0, 1, 2
사용자 번호: 1, 2, 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3번: 메모 수정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 메뉴에서는 수정할 메모 번호를 입력받고, 해당 메모를 새 제목과 새 내용으로 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 기능의 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;수정할 메모 번호 입력
&amp;rarr; 사용자 번호를 index로 변환
&amp;rarr; index 범위 검사
&amp;rarr; 새 제목 입력
&amp;rarr; 새 내용 입력
&amp;rarr; 새 메모 배열 생성
&amp;rarr; memos.set(index, updatedMemo)로 교체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 보는 번호는 1부터 시작하지만, List의 index는 0부터 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 계산이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int index = memoNumber - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 1번 메모를 수정하려고 하면 실제 index는 0이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용자 번호 1 &amp;rarr; index 0
사용자 번호 2 &amp;rarr; index 1
사용자 번호 3 &amp;rarr; index 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위 검사도 필요하다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if (index &amp;lt; 0 || index &amp;gt;= memos.size()) {
    System.out.println(&quot;올바른 메모 번호를 입력하세요.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 검사를 하지 않으면 존재하지 않는 위치에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 번호라면 새 제목과 내용을 입력받아 기존 메모를 교체한다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;String[] updatedMemo = {newTitle, newContent};
memos.set(index, updatedMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 memos.set(index, updatedMemo)는 해당 위치의 메모를 새 메모로 바꾸는 코드다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4번: 메모 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번 메뉴에서는 삭제할 메모 번호를 입력받고, 해당 메모를 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 기능은 수정 기능과 흐름이 거의 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;삭제할 메모 번호 입력
&amp;rarr; 사용자 번호를 index로 변환
&amp;rarr; index 범위 검사
&amp;rarr; memos.remove(index)로 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제의 핵심은 다음 코드다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;memos.remove(index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 memos 리스트에서 해당 index 위치의 메모를 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 2번 메모를 삭제한다고 하면,&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int index = 2 - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;index = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 코드로 두 번째 메모가 삭제된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;memos.remove(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정과 삭제의 공통점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정과 삭제는 모두 사용자가 보는 번호와 실제 List index를 연결해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용자 번호 1 &amp;rarr; index 0
사용자 번호 2 &amp;rarr; index 1
사용자 번호 3 &amp;rarr; index 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공통적으로 다음 계산을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int index = memoNumber - 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 잘못된 번호가 들어오지 않도록 범위 검사를 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if (index &amp;lt; 0 || index &amp;gt;= memos.size()) {
    System.out.println(&quot;올바른 메모 번호를 입력하세요.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 단순해 보이지만 중요했다.&lt;br /&gt;사용자가 존재하지 않는 번호를 입력했을 때 프로그램이 바로 오류로 멈추지 않도록 막아주는 역할을 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 사용한 자료구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 List&amp;lt;String[]&amp;gt; 구조를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;List&amp;lt;String[]&amp;gt; memos = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 하나를 String[]로 다루는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;String[] memo = {title, content};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 완성형 구조는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목은 memo[0], 내용은 memo[1]로 접근해야 하기 때문에 코드가 길어지면 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 String[] 대신 Memo 클래스를 만들어서 더 명확하게 바꿀 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 구조다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;class Memo {
    private String title;
    private String content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 단계에서는 클래스 설계보다 Java에서 CRUD 흐름을 먼저 익히는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 배열과 리스트를 이용해 메모 데이터를 먼저 다뤄봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 단계에서 사용한 Java 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Java의 기본 문법을 사용해서 CRUD 흐름을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;List
ArrayList
String[]
Scanner
nextLine
Integer.parseInt
for문
if / else if
isEmpty
get
add
set
remove
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 state 배열을 기준으로 메모를 추가, 수정, 삭제했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 콘솔에서는 List를 기준으로 메모를 추가, 수정, 삭제했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구와 환경은 다르지만 결국 흐름은 비슷했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메모 작성 &amp;rarr; 데이터 추가
메모 목록 &amp;rarr; 데이터 전체 조회
메모 수정 &amp;rarr; 특정 데이터 교체
메모 삭제 &amp;rarr; 특정 데이터 제거
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 구현된 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Java 콘솔 메모장은 다음 기능을 가진다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메모 작성
메모 목록 보기
메모 수정
메모 삭제
프로그램 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 파일 저장이나 DB 저장은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 종료하면 메모는 사라진다.&lt;br /&gt;현재 메모는 프로그램이 실행되는 동안만 memos 리스트 안에 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 단계의 목적은 저장소를 만드는 것이 아니라, Java에서 메모 데이터를 어떻게 다루는지 연습하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 Java 콘솔 환경에서 CRUD 기능을 직접 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저나 React 없이도 다음 흐름을 Java 코드로 만들 수 있게 되었다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력받기
데이터 저장하기
목록 출력하기
기존 데이터 수정하기
기존 데이터 삭제하기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 나중에 Servlet/JSP나 Spring으로 넘어갈 때도 이어질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;콘솔 입력
&amp;rarr; 나중에는 브라우저 요청

List에 저장
&amp;rarr; 나중에는 DB에 저장

if문으로 기능 분기
&amp;rarr; 나중에는 Controller에서 요청 분기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이번 단계는 Java 웹 개발로 넘어가기 전에 메모 CRUD 로직을 Java 문법으로 먼저 익혀보는 과정이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 String[]로 메모를 다루는 방식에서 벗어나, Memo 클래스를 만들어 메모 데이터를 조금 더 명확한 구조로 바꿔볼 예정이다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/183</guid>
      <comments>https://woojoo-devlog.tistory.com/183#entry183comment</comments>
      <pubDate>Sat, 20 Jun 2026 16:55:11 +0900</pubDate>
    </item>
    <item>
      <title>#18 Java 콘솔 메모장 시작 중 만난 IntelliJ 설정 문제</title>
      <link>https://woojoo-devlog.tistory.com/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 콘솔 메모장을 시작하면서 IntelliJ에서 이상한 문제가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sout을 입력했을 때 System.out.println() 자동완성은 되는데, Scanner에는 빨간줄이 뜨지 않고 import java.util.Scanner; 추천도 나오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 JDK 문제라고 생각했지만, 확인해보니 단순히 JDK만의 문제는 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 코드는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상이라면 Scanner에 빨간줄이 뜨고, IntelliJ가 다음 import를 추천해야 한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import java.util.Scanner;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 빨간줄도 없고, import 추천도 나오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, IntelliJ가 Java 코드를 제대로 분석하지 않고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 확인한 것: JDK 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 프로젝트에서는 JDK 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ에서는 메뉴 이름이 SDK로 나오지만, Java 기준으로는 JDK라고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 경로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;File
&amp;rarr; Project Structure...
&amp;rarr; Project
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 SDK가 No SDK로 되어 있으면 Java 기능이 제대로 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SDK: temurin-17
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK가 잡혀 있어야 IntelliJ가 Java 표준 라이브러리를 인식할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 클래스들은 JDK 안에 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;String
System
Scanner
List
ArrayList
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JDK가 없으면 자동완성, import 추천, 컴파일, 실행 기능이 제대로 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SDK와 JDK 용어 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 SDK와 JDK라는 말이 섞여서 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JDK
&amp;rarr; Java 개발 도구

SDK
&amp;rarr; 개발 도구 묶음을 부르는 일반적인 이름
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ에서 Project SDK라고 나오는 것은 Java 프로젝트에서는 사실상 JDK를 선택하라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이렇게 이해하면 된다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;IntelliJ의 SDK 설정 = Java에서는 JDK 설정
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Language level 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK를 설정한 뒤에는 Language level도 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 경로는 같다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;File
&amp;rarr; Project Structure...
&amp;rarr; Project
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 SDK는 temurin-17인데, Language level이 이상하게 높은 버전으로 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Language level: 26 - No new language features
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 17을 사용할 거라면 Language level도 17로 맞추는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Language level: 17
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 버전과 Language level이 맞지 않으면 프로젝트 설정이 꼬여 보일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Highlight 설정 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK가 설정되어 있는데도 빨간줄이 나오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 오른쪽 위를 보니 OFF가 표시되어 있었다.&lt;br /&gt;이 상태는 코드 분석 또는 하이라이팅이 꺼져 있는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 위 OFF를 눌러보면 다음과 비슷한 메뉴가 나온다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Highlight: None
Syntax
All Problems
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 None이면 코드 분석이 꺼진 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 오류 표시와 import 추천을 보려면 다음으로 바꿔야 한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;All Problems
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 꺼져 있으면 Java 코드에 문제가 있어도 빨간줄이 보이지 않을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Power Save Mode 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 확인할 것은 Power Save Mode다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 경로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;File
&amp;rarr; Power Save Mode
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션이 켜져 있으면 IntelliJ가 코드 분석, 자동완성, inspection 기능을 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 자동완성이나 빨간줄이 이상하게 동작하지 않으면 Power Save Mode가 켜져 있는지 확인해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;src 폴더 위치 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 프로젝트에서는 보통 소스 코드를 src 폴더 안에 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 의도한 구조는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;java-console-memo/
└─ src/
   └─ Main.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Main.java가 src 밖에 있거나, IntelliJ가 src를 소스 폴더로 인식하지 못하면 Java 코드 분석이 제대로 되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 src 폴더를 우클릭해서 다음 메뉴를 선택한다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Mark Directory As
&amp;rarr; Sources Root
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 설정되면 src 폴더가 파란색 계열로 표시된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sources Root 메뉴가 보이지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 여기서 끝나지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src 폴더를 우클릭했는데 Sources Root 메뉴가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 메뉴 위치를 잘못 찾은 줄 알았지만, 실제 원인은 따로 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Project Structure에서 Modules를 확인해보니 다음처럼 나왔다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Nothing to show
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, IntelliJ가 현재 프로젝트에 Java 모듈을 하나도 등록하지 않은 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 없으면 IntelliJ 입장에서는 어떤 폴더를 Java 소스 폴더로 다뤄야 하는지 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 src를 Sources Root로 지정하는 메뉴도 제대로 보이지 않았던 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 문제의 핵심은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;JDK는 설치되어 있었지만,
현재 프로젝트에 Java 모듈이 등록되어 있지 않았다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 IntelliJ는 파일을 단순 텍스트처럼 어느 정도 보여줄 수는 있었지만, Java 프로젝트로 제대로 분석하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 다음 문제가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Scanner 빨간줄이 안 뜸
import 추천이 안 뜸
Java 오류 분석이 안 됨
Sources Root 설정이 제대로 안 보임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 코드 문제가 아니라 프로젝트 설정 문제였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 먼저 Java 모듈을 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 경로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;File
&amp;rarr; Project Structure...
&amp;rarr; Modules
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 아무것도 없고 Nothing to show가 나온다면, 왼쪽 위 + 버튼을 눌러 모듈을 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 폴더를 Java 모듈로 등록하려면 다음 순서로 진행한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;+ 버튼
&amp;rarr; Import Module
&amp;rarr; java-console-memo 폴더 선택
&amp;rarr; Java 모듈로 등록
&amp;rarr; JDK는 temurin-17 선택
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 등록되면 이후에 src 폴더를 Sources Root로 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;src 우클릭
&amp;rarr; Mark Directory As
&amp;rarr; Sources Root
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 IntelliJ가 프로젝트를 Java 프로젝트로 제대로 인식하면 빨간줄, import 추천, 자동완성 기능도 정상적으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 같은 문제가 생기면 아래 순서대로 확인하면 된다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;1. JDK 설정 확인
File &amp;rarr; Project Structure &amp;rarr; Project &amp;rarr; SDK

2. Language level 확인
JDK 버전과 맞는지 확인

3. 오른쪽 위 Highlight 확인
None이 아니라 All Problems인지 확인

4. Power Save Mode 확인
File &amp;rarr; Power Save Mode 꺼져 있는지 확인

5. Main.java 위치 확인
java-console-memo/src/Main.java 구조인지 확인

6. src가 Sources Root인지 확인
src 우클릭 &amp;rarr; Mark Directory As &amp;rarr; Sources Root

7. Sources Root 메뉴가 안 보이면 Modules 확인
File &amp;rarr; Project Structure &amp;rarr; Modules

8. Modules에 Nothing to show가 나오면 Java 모듈 추가
+ &amp;rarr; Import Module &amp;rarr; java-console-memo 선택
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 과정에서 추가로 확인한 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 콘솔 메모장을 실행하려고 하면서 IntelliJ 설정 문제를 한 번 더 확인하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 자동 import나 빨간줄 표시 문제만 봤는데, 실행 단계에서도 빌드 관련 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 코드 자체의 문제라기보다는 IntelliJ가 Java 프로젝트 구조를 제대로 인식하지 못해서 생긴 문제였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 시 확인한 오류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main.java를 작성하고 실행하려고 했을 때 다음과 같은 문제가 있었다.&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;자동 import가 제대로 안 됨
Scanner 오류 표시가 안 보임
실행 시 빌드 관련 오류 발생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 다음 메시지도 확인했다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;The output path is not specified for module java-console-memo
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메시지는 Java 코드를 컴파일한 결과물인 .class 파일을 어디에 저장할지 지정되어 있지 않다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compiler output 설정 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 코드는 바로 실행되는 것이 아니라 먼저 컴파일된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Main.java
&amp;rarr; 컴파일
&amp;rarr; Main.class
&amp;rarr; 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 IntelliJ에 컴파일 결과물을 저장할 위치가 설정되어 있지 않으면 실행할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Project Structure에서 Compiler output을 지정해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;File
&amp;rarr; Project Structure...
&amp;rarr; Project
&amp;rarr; Compiler output
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 예를 들어 다음과 같은 폴더를 지정한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;/Users/woojoo/workspace/Memo Evolution/out
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 IntelliJ에게 다음을 알려주는 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Java 파일을 컴파일한 결과물을 이 폴더에 저장해라.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;out 폴더의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일 설정을 잡으면 프로젝트에 out 폴더가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 폴더는 내가 직접 작성하는 소스 코드가 아니다.&lt;br /&gt;IntelliJ가 Java 파일을 컴파일한 결과물을 저장하는 곳이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;src
&amp;rarr; 내가 작성하는 Java 소스 코드

out
&amp;rarr; 컴파일된 결과물이 저장되는 폴더
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 out 폴더는 직접 수정하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드는 src 안에서 작성하고, out은 IntelliJ가 빌드 결과를 저장하는 공간으로 두면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;java-console-memo.iml 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 하면서 java-console-memo.iml 파일도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 파일이 내가 만든 파일인지, 지워도 되는 파일인지 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.iml 파일은 IntelliJ가 만든 모듈 설정 파일이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;.iml = IntelliJ Module File
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 내가 직접 작성하는 Java 소스 파일이 아니라 IntelliJ가 프로젝트 설정을 기억하기 위해 만든 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일 안에는 다음과 같은 정보가 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;이 폴더가 Java 모듈이라는 정보
src 폴더가 소스 폴더라는 정보
프로젝트 JDK를 사용한다는 정보
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 파일 안에는 이런 설정이 있었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;sourceFolder url=&quot;file://$MODULE_DIR$/src&quot; isTestSource=&quot;false&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 다음 의미다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;java-console-memo/src 폴더를 Java 소스 폴더로 인식한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 iml 파일이 생겼을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 java-console-memo 폴더가 단순 폴더처럼 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 IntelliJ에서 이 폴더를 Java 모듈로 인식하게 만들면서 .iml 파일이 생성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, .iml 파일은 IntelliJ가 다음 정보를 저장하기 위해 만든 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 폴더는 Java 모듈이다.
src 폴더는 소스 코드 폴더다.
JDK는 프로젝트 설정을 따른다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 파일이 생겼다는 것은 IntelliJ가 해당 폴더를 단순 폴더가 아니라 Java 모듈로 관리하기 시작했다는 의미로 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;iml 파일을 삭제해도 될까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 단계에서는 삭제하지 않는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 사라지면 IntelliJ가 다시 Java 모듈 정보를 잃을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다시 다음 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;src 폴더 인식 안 됨
자동완성 이상함
import 추천 안 됨
실행 설정 꼬임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 현재처럼 IntelliJ로 Java 학습을 진행하는 동안에는 유지하는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 파일은 Java 문법이나 메모장 기능과 직접 관련된 코드는 아니다.&lt;br /&gt;IntelliJ가 프로젝트를 어떻게 인식할지 저장하는 설정 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/182</guid>
      <comments>https://woojoo-devlog.tistory.com/182#entry182comment</comments>
      <pubDate>Sat, 20 Jun 2026 13:12:12 +0900</pubDate>
    </item>
    <item>
      <title>#17 Java 콘솔 메모장 시작하기</title>
      <link>https://woojoo-devlog.tistory.com/181</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React로 메모장 기능을 다시 구현한 뒤, 다음 단계로 Java 콘솔 메모장을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 브라우저 안에서 메모장을 만들었다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;JavaScript
&amp;rarr; 브라우저 안에서 배열로 메모 관리

React
&amp;rarr; state로 메모 배열 관리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 같은 메모장 기능을 Java 코드로 다시 구현해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 웹 화면을 만드는 것이 아니라, Java에서 메모 데이터를 어떻게 다룰 수 있는지 연습하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Java 콘솔부터 시작했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Servlet/JSP나 Spring으로 넘어가면 한 번에 봐야 할 개념이 너무 많아진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HTTP 요청
Servlet
JSP
Tomcat
DB
Controller
Service
DAO
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 콘솔 환경에서 Java 문법과 CRUD 흐름을 잡아보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔 메모장은 화면은 단순하지만, 나중에 서버로 넘어가기 전 기본 로직을 연습하기에 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 먼저 만들고 싶은 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메뉴 출력
&amp;rarr; 사용자 입력
&amp;rarr; 선택한 기능 실행
&amp;rarr; 다시 메뉴 출력
&amp;rarr; 종료 선택 시 프로그램 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 나중에 웹으로 넘어가도 비슷하게 이어질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저 요청
&amp;rarr; Controller 또는 Servlet
&amp;rarr; Service
&amp;rarr; 결과 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 기능을 바로 완성하기보다, Java 콘솔 프로그램의 기본 뼈대를 먼저 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 콘솔 메모장은 React 프로젝트와 분리해서 새 폴더로 만들었다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;Memo Evolution/
├─ react-memo/
└─ java-console-memo/
   └─ src/
      └─ Main.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Main.java는 Java 프로그램의 시작점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 main 메서드가 프로그램 실행의 출발점이 된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public static void main(String[] args) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 이 Main.java 안에서 메뉴 출력과 입력 처리를 먼저 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 작성한 Main.java&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 메모 작성, 수정, 삭제 기능을 모두 만들지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 사용자가 메뉴를 선택할 수 있는 구조부터 만들었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        boolean running = true;

        while (running) {
            System.out.println(&quot;===== 메모장 =====&quot;);
            System.out.println(&quot;1. 메모 작성&quot;);
            System.out.println(&quot;2. 메모 목록 보기&quot;);
            System.out.println(&quot;3. 메모 수정&quot;);
            System.out.println(&quot;4. 메모 삭제&quot;);
            System.out.println(&quot;0. 종료&quot;);
            System.out.print(&quot;메뉴 선택: &quot;);

            int menu = scanner.nextInt();

            if (menu == 1) {
                System.out.println(&quot;메모 작성 기능은 아직 구현되지 않았습니다.&quot;);
            } else if (menu == 2) {
                System.out.println(&quot;메모 목록 보기 기능은 아직 구현되지 않았습니다.&quot;);
            } else if (menu == 3) {
                System.out.println(&quot;메모 수정 기능은 아직 구현되지 않았습니다.&quot;);
            } else if (menu == 4) {
                System.out.println(&quot;메모 삭제 기능은 아직 구현되지 않았습니다.&quot;);
            } else if (menu == 0) {
                running = false;
                System.out.println(&quot;프로그램을 종료합니다.&quot;);
            } else {
                System.out.println(&quot;올바른 메뉴 번호를 입력하세요.&quot;);
            }

            System.out.println();
        }

        scanner.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 실제 메모 데이터는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 사용자가 메뉴를 선택하면 해당 기능이 아직 구현되지 않았다는 문구만 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력을 받기 위해 Scanner 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에서 사용자의 입력을 받기 위해 Scanner를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Scanner scanner = new Scanner(System.in);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 사용자가 메뉴 번호를 입력하면 그 값을 읽는다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;int menu = scanner.nextInt();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 1을 입력하면 menu 변수에는 1이 들어간다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용자 입력: 1
menu = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 기준으로 어떤 기능을 실행할지 나누었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;while문으로 메뉴 반복하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모장 프로그램은 메뉴를 한 번 보여주고 끝나면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 종료를 선택할 때까지 계속 메뉴가 나와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 while문을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;boolean running = true;

while (running) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 running 값이 true이기 때문에 반복문이 계속 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 0을 입력하면 running을 false로 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;running = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 while (running) 조건이 더 이상 참이 아니게 되고, 프로그램이 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메뉴 번호에 따라 기능 나누기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 번호에 따라 다른 동작을 해야 하므로 if / else if / else를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if (menu == 1) {
    System.out.println(&quot;메모 작성 기능은 아직 구현되지 않았습니다.&quot;);
} else if (menu == 2) {
    System.out.println(&quot;메모 목록 보기 기능은 아직 구현되지 않았습니다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 실제 기능은 구현하지 않았기 때문에, 지금은 각 메뉴마다 안내 문구만 출력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 목표는 메모 기능 자체가 아니라 프로그램의 흐름을 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0을 입력하면 종료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 0을 입력하면 프로그램이 종료되도록 했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;} else if (menu == 0) {
    running = false;
    System.out.println(&quot;프로그램을 종료합니다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;running이 false가 되면 반복문이 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 프로그램은 사용자가 직접 종료를 선택하기 전까지 계속 메뉴를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잘못된 번호 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 1, 2, 3, 4, 0이 아닌 숫자를 입력할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 경우에는 안내 문구를 출력하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;} else {
    System.out.println(&quot;올바른 메뉴 번호를 입력하세요.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 복잡한 예외 처리는 하지 않았지만, 최소한 잘못된 번호를 입력했을 때 프로그램 흐름이 이상해지지 않도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;메모 작성
메모 목록 저장
메모 수정
메모 삭제
파일 저장
DB 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계는 기능 구현보다 Java 콘솔 프로그램의 기본 흐름을 만드는 데 집중했다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/181</guid>
      <comments>https://woojoo-devlog.tistory.com/181#entry181comment</comments>
      <pubDate>Sat, 20 Jun 2026 13:07:15 +0900</pubDate>
    </item>
    <item>
      <title>#16 HTML/CSS/JS 메모장을 React로 고도화 정리</title>
      <link>https://woojoo-devlog.tistory.com/180</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 HTML/CSS/JS 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 파일 역할이 명확하게 나뉘어 있었다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;index.html
&amp;rarr; 화면 구조

style.css
&amp;rarr; 디자인

script.js
&amp;rarr; 동작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 DOM을 직접 찾고 조작했다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;document.getElementById(...)
document.createElement(...)
appendChild(...)
innerHTML = ''
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가하거나 삭제한 뒤에는 직접 renderMemos()를 호출해서 화면을 다시 그렸다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;memos.push(newMemo)
renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 기존 방식은 다음 흐름이었다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;데이터 변경
&amp;rarr; DOM 직접 조작
&amp;rarr; 화면 직접 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 화면이 어떻게 만들어지고 바뀌는지 직접 확인하기 좋았다.&lt;br /&gt;하지만 기능이 늘어날수록 DOM을 직접 다루는 코드가 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React 방식으로 바꾸기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 HTML 역할을 JSX가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존처럼 index.html에 화면을 직접 작성하는 대신, App.jsx의 return 안에 화면 구조를 작성했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;return (
  &amp;lt;div className=&quot;app&quot;&amp;gt;
    ...
  &amp;lt;/div&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX는 HTML처럼 보이지만 JavaScript 안에서 사용하는 문법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 HTML의 class는 JSX에서 className으로 작성하고, for는 htmlFor로 작성한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;label htmlFor=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&amp;lt;div className=&quot;memo-card&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 HTML과 비슷하지만, 실제로는 React 컴포넌트가 반환하는 화면 구조라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React의 핵심은 state&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 화면에 영향을 주는 데이터를 state로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [memos, setMemos] = useState([])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 값의 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;title
&amp;rarr; 현재 제목 입력값

content
&amp;rarr; 현재 내용 입력값

memos
&amp;rarr; 메모 배열
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 setTitle, setContent, setMemos는 각각의 state를 바꾸는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 React에서는 state가 바뀌면 자동으로 다시 렌더링된다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;set 함수 실행
&amp;rarr; state 변경
&amp;rarr; 컴포넌트 다시 실행
&amp;rarr; JSX 다시 계산
&amp;rarr; 화면 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 화면을 직접 수정하는 것이 아니라 화면의 기준이 되는 데이터를 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력값 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JavaScript에서는 input 값을 직접 꺼냈다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;titleInput.value
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 input을 state와 연결했다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;&amp;lt;input
  value={title}
  onChange={(event) =&amp;gt; setTitle(event.target.value)}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;value={title}
&amp;rarr; title 값을 input에 보여준다.

onChange
&amp;rarr; input 값이 바뀔 때 실행된다.

event.target.value
&amp;rarr; 사용자가 input에 입력한 현재 값이다.

setTitle(event.target.value)
&amp;rarr; 입력값을 title state에 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;title &amp;rarr; input
input &amp;rarr; setTitle &amp;rarr; title
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 입력값도 state로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JavaScript에서는 배열에 직접 메모를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;memos.push(newMemo)
renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 새 배열을 만들어 state를 변경했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;setMemos([...memos, newMemo])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;기존 memos 배열을 펼친다.
뒤에 newMemo를 추가한다.
새 배열을 만든다.
setMemos로 memos state를 교체한다.
React가 다시 렌더링한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 기존 배열을 직접 수정하기보다, 새 배열을 만들어 state를 바꾸는 방식으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 목록 출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JavaScript에서는 직접 DOM 요소를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;document.createElement('div')
appendChild(memoCard)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 map()을 사용해서 배열을 화면으로 바꿨다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{memos.map((memo) =&amp;gt; (
  &amp;lt;MemoItem
    key={memo.id}
    memo={memo}
    onEditMemo={handleEditMemo}
    onDeleteMemo={handleDeleteMemo}
  /&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 메모 배열을 돌면서 메모 하나를 화면 요소 하나로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 key={memo.id}는 React가 목록 항목을 구분하기 위해 사용하는 값이다.&lt;br /&gt;화면에 직접 보이는 값은 아니지만, React가 어떤 항목이 추가되었고 삭제되었는지 구분하는 데 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정과 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제는 filter()를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;const nextMemos = memos.filter((memo) =&amp;gt; {
  return memo.id !== id
})

setMemos(nextMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter()는 조건에 맞는 요소만 남겨 새 배열을 만든다.&lt;br /&gt;삭제하려는 id와 다른 메모만 남기면, 해당 메모가 제외된 새 배열이 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정은 map()을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;const nextMemos = memos.map((memo) =&amp;gt; {
  if (memo.id === id) {
    return {
      id: memo.id,
      title: newTitle.trim(),
      content: newContent.trim(),
    }
  }

  return memo
})

setMemos(nextMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 배열을 돌면서 새 배열을 만든다.&lt;br /&gt;수정할 id와 같은 메모를 만나면 새 제목과 내용으로 바꾼 객체를 반환하고, 나머지는 그대로 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제와 수정 모두 공통점이 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;기존 배열을 기준으로 새 배열을 만든다.
setMemos로 state를 변경한다.
React가 자동으로 화면을 다시 그린다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;localStorage 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서도 localStorage를 사용해 새로고침 후에도 메모가 유지되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 실행될 때 localStorage에서 메모를 불러왔다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const savedMemos = localStorage.getItem('memos')

  if (savedMemos !== null) {
    setMemos(JSON.parse(savedMemos))
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 memos가 바뀔 때마다 localStorage에 저장했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  localStorage.setItem('memos', JSON.stringify(memos))
}, [memos])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;useState
&amp;rarr; 화면에 영향을 주는 데이터 관리

useEffect
&amp;rarr; 렌더링 이후에 실행할 작업 처리

JSON.stringify
&amp;rarr; 배열을 문자열로 변환해 저장

JSON.parse
&amp;rarr; 문자열을 다시 배열로 변환해 불러오기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage는 화면을 그리는 작업이 아니라 브라우저 저장소를 다루는 작업이므로 useEffect에서 처리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 App.jsx 안에 모든 코드가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 역할별로 컴포넌트를 분리했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;App.jsx
&amp;rarr; state와 기능 관리

MemoForm.jsx
&amp;rarr; 메모 입력 폼

MemoList.jsx
&amp;rarr; 메모 목록

MemoItem.jsx
&amp;rarr; 메모 카드 하나
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 분리하면서 props를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;MemoForm
  title={title}
  content={content}
  onTitleChange={(event) =&amp;gt; setTitle(event.target.value)}
  onContentChange={(event) =&amp;gt; setContent(event.target.value)}
  onAddMemo={handleAddMemo}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props는 부모 컴포넌트가 자식 컴포넌트에게 넘겨주는 값 또는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;App이 state를 가지고 있다.
MemoForm은 props로 title, content, 함수를 받는다.
MemoForm에서 이벤트가 발생하면 App에서 받은 함수를 실행한다.
App의 state가 바뀐다.
React가 다시 렌더링한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 컴포넌트를 분리해도 데이터의 중심은 App에 있고, 하위 컴포넌트는 필요한 값과 함수를 props로 받아 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML/CSS/JS와 React의 가장 큰 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 화면을 직접 조작했다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;HTML/CSS/JS
&amp;rarr; DOM을 직접 찾는다.
&amp;rarr; DOM을 직접 만든다.
&amp;rarr; DOM을 직접 붙인다.
&amp;rarr; 화면을 직접 갱신한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 방식은 state를 기준으로 화면을 그린다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;React
&amp;rarr; state를 바꾼다.
&amp;rarr; React가 컴포넌트를 다시 실행한다.
&amp;rarr; JSX를 다시 계산한다.
&amp;rarr; 화면이 자동으로 갱신된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 차이는 이것이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;기존 JS는 화면을 직접 바꾼다.
React는 state를 바꾸면 화면이 따라온다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 이해하는 것이 이번 React 단계의 핵심이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 React 메모장의 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 React 버전 메모장은 기존 JavaScript 버전에서 만들었던 기능을 대부분 다시 구현한 상태다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메모 작성
메모 목록 보기
메모 수정
메모 삭제
새로고침 후 유지
컴포넌트 분리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 자체는 기존과 비슷하지만, 구현 방식은 달라졌다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/180</guid>
      <comments>https://woojoo-devlog.tistory.com/180#entry180comment</comments>
      <pubDate>Sat, 20 Jun 2026 12:21:33 +0900</pubDate>
    </item>
    <item>
      <title>#15 컴포넌트 분리와 props 이해하기</title>
      <link>https://woojoo-devlog.tistory.com/179</link>
      <description>&lt;h1&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이번 단계에서는 React 메모장의 기능을 새로 추가한 것이 아니라, 기존 코드를 컴포넌트 단위로 분리했다.&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 App.jsx 안에 모든 코드가 들어 있었다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;입력 폼
메모 목록
메모 카드
추가 함수
수정 함수
삭제 함수
localStorage 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이렇게 한 파일에 작성하는 것이 이해하기 쉽다.&lt;br /&gt;하지만 코드가 길어질수록 어떤 부분이 입력 폼인지, 어떤 부분이 목록인지, 어떤 부분이 메모 하나인지 구분하기 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 화면 역할별로 컴포넌트를 나누었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트를 분리한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 분리는 기능을 바꾸는 작업이 아니다.&lt;br /&gt;기존에 잘 동작하던 코드를 역할별로 나누는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 다음처럼 역할을 나누었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;App.jsx
&amp;rarr; 전체 데이터와 기능 관리

MemoForm.jsx
&amp;rarr; 메모 입력 폼 담당

MemoList.jsx
&amp;rarr; 메모 목록 영역 담당

MemoItem.jsx
&amp;rarr; 메모 카드 하나 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 화면을 더 작은 단위로 나누어 관리하기 위한 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분리 후 파일 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 구조는 다음과 같이 바뀌었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;src/
 ├─ App.jsx
 ├─ App.css
 └─ components/
     ├─ MemoForm.jsx
     ├─ MemoList.jsx
     └─ MemoItem.jsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파일은 자신의 역할만 담당한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;MemoForm
&amp;rarr; 제목 입력
&amp;rarr; 내용 입력
&amp;rarr; 메모 추가 버튼

MemoList
&amp;rarr; 메모 목록 전체
&amp;rarr; 메모가 없을 때 안내 메시지

MemoItem
&amp;rarr; 메모 하나의 제목
&amp;rarr; 메모 하나의 내용
&amp;rarr; 수정 버튼
&amp;rarr; 삭제 버튼
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나누면 화면 구조를 더 쉽게 파악할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;App.jsx의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 분리해도 중요한 데이터는 여전히 App.jsx가 가지고 있다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [memos, setMemos] = useState([])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, App.jsx는 메모장의 중심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx가 담당하는 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;title state 관리
content state 관리
memos state 관리
메모 추가 함수 관리
메모 수정 함수 관리
메모 삭제 함수 관리
localStorage 저장/불러오기 관리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면은 여러 컴포넌트로 나누었지만, 실제 데이터와 기능은 App.jsx가 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;props란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 분리하면 한 가지 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 title state는 App.jsx에 있는데, 실제 input은 MemoForm.jsx 안에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 MemoForm은 title 값을 어떻게 사용할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 props다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;props
= 부모 컴포넌트가 자식 컴포넌트에게 넘겨주는 값 또는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조에서는 App이 부모 컴포넌트이고, MemoForm, MemoList, MemoItem은 자식 컴포넌트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MemoForm에 props 넘기기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx에서는 MemoForm을 이렇게 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;MemoForm
  title={title}
  content={content}
  onTitleChange={(event) =&amp;gt; setTitle(event.target.value)}
  onContentChange={(event) =&amp;gt; setContent(event.target.value)}
  onAddMemo={handleAddMemo}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 MemoForm에게 필요한 값과 함수를 넘겨주는 코드다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;title
&amp;rarr; 제목 input에 보여줄 값

content
&amp;rarr; 내용 textarea에 보여줄 값

onTitleChange
&amp;rarr; 제목 input이 바뀔 때 실행할 함수

onContentChange
&amp;rarr; 내용 textarea가 바뀔 때 실행할 함수

onAddMemo
&amp;rarr; 메모 추가 버튼을 눌렀을 때 실행할 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;title={title} 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 헷갈렸던 부분은 다음 코드였다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;title={title}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽과 오른쪽의 의미가 다르다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;왼쪽 title
&amp;rarr; MemoForm에게 넘겨줄 props 이름

오른쪽 {title}
&amp;rarr; App.jsx 안에 있는 title state 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 코드는 다음처럼 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;App이 가지고 있는 title state 값을
MemoForm에게 title이라는 이름으로 넘긴다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 App의 title 값이 &quot;오늘 할 일&quot;이라면,&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;MemoForm title={title} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은 개념적으로 이런 느낌이다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;MemoForm title=&quot;오늘 할 일&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MemoForm에서 props 받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoForm.jsx에서는 App에서 넘겨준 props를 이렇게 받는다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function MemoForm({ title, content, onTitleChange, onContentChange, onAddMemo }) {
  return (
    ...
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;App이 넘겨준 props 중에서
title을 꺼낸다.
content를 꺼낸다.
onTitleChange를 꺼낸다.
onContentChange를 꺼낸다.
onAddMemo를 꺼낸다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 값을 input과 textarea, button에 연결한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;input
  value={title}
  onChange={onTitleChange}
/&amp;gt;

&amp;lt;textarea
  value={content}
  onChange={onContentChange}
&amp;gt;&amp;lt;/textarea&amp;gt;

&amp;lt;button type=&quot;button&quot; onClick={onAddMemo}&amp;gt;
  메모 추가
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;value={title}의 의미&lt;/h2&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;value={title}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 input에 title 값을 넣어서 보여주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;title state &amp;rarr; input 화면
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, title 값을 input에 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 사용자가 input에 입력한 값을 title state에 저장하는 코드는 이것이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;onChange={onTitleChange}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 App.jsx에서 이렇게 넘겨준 함수다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;onTitleChange={(event) =&amp;gt; setTitle(event.target.value)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 input에 입력하면 다음 흐름으로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;사용자가 제목 input에 입력
&amp;rarr; onChange 실행
&amp;rarr; onTitleChange 실행
&amp;rarr; event.target.value로 input의 현재 값 가져옴
&amp;rarr; setTitle 실행
&amp;rarr; App의 title state 변경
&amp;rarr; App 다시 렌더링
&amp;rarr; MemoForm에 새 title 전달
&amp;rarr; input value={title}에 새 값 표시
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;값과 함수가 같이 넘어가는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoForm은 input 화면을 가지고 있지만, title state는 가지고 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;title state는 App.jsx에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MemoForm이 input을 제대로 동작시키려면 두 가지가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;1. 현재 값을 받아야 한다.
2. 값이 바뀌었을 때 App의 state를 바꿀 함수도 받아야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 App은 MemoForm에게 값과 함수를 함께 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;title={title}
onTitleChange={(event) =&amp;gt; setTitle(event.target.value)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;title
&amp;rarr; 현재 값

onTitleChange
&amp;rarr; 새 값을 title state에 저장하는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MemoList 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 목록도 MemoList.jsx로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx에서는 이렇게 넘겨준다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;MemoList
  memos={memos}
  onEditMemo={handleEditMemo}
  onDeleteMemo={handleDeleteMemo}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 props의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;memos
&amp;rarr; 화면에 출력할 메모 배열

onEditMemo
&amp;rarr; 수정 버튼을 눌렀을 때 실행할 함수

onDeleteMemo
&amp;rarr; 삭제 버튼을 눌렀을 때 실행할 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 여기서 함수를 실행하는 것이 아니라 넘겨주는 것이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;onEditMemo={handleEditMemo}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다음처럼 쓰면 안 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;onEditMemo={handleEditMemo()}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쓰면 클릭했을 때 실행되는 것이 아니라, 렌더링되는 순간 함수가 바로 실행되어 버린다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MemoItem 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoList 안에서는 memos.map()을 사용해서 메모 하나하나를 MemoItem으로 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{memos.map((memo) =&amp;gt; (
  &amp;lt;MemoItem
    key={memo.id}
    memo={memo}
    onEditMemo={onEditMemo}
    onDeleteMemo={onDeleteMemo}
  /&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 MemoItem은 메모 하나를 담당한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;memo
&amp;rarr; 메모 하나의 데이터

onEditMemo
&amp;rarr; 수정 함수

onDeleteMemo
&amp;rarr; 삭제 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoItem에서는 버튼을 클릭했을 때 현재 메모의 id를 넣어서 함수를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; onEditMemo(memo.id)}&amp;gt;
  수정
&amp;lt;/button&amp;gt;

&amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; onDeleteMemo(memo.id)}&amp;gt;
  삭제
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하는 이유는 각 메모마다 id가 다르기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1번 메모 수정 버튼
&amp;rarr; onEditMemo(1)

2번 메모 수정 버튼
&amp;rarr; onEditMemo(2)

3번 메모 수정 버튼
&amp;rarr; onEditMemo(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 컴포넌트 흐름&lt;/h2&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;App
&amp;rarr; title, content, memos state를 가지고 있음
&amp;rarr; 추가/수정/삭제 함수를 가지고 있음

MemoForm
&amp;rarr; App에서 title, content, 입력 변경 함수, 추가 함수를 받음
&amp;rarr; 입력 폼 화면 담당

MemoList
&amp;rarr; App에서 memos, 수정 함수, 삭제 함수를 받음
&amp;rarr; 목록 전체 담당

MemoItem
&amp;rarr; MemoList에서 memo 하나와 수정/삭제 함수를 받음
&amp;rarr; 메모 카드 하나 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;App
 ├─ MemoForm
 └─ MemoList
      ├─ MemoItem
      ├─ MemoItem
      └─ MemoItem
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;props 흐름 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 가장 중요하게 본 것은 props의 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;부모가 state를 가진다.
자식은 props로 받아서 사용한다.
자식에서 이벤트가 발생하면 부모에게서 받은 함수를 실행한다.
부모의 state가 바뀐다.
화면이 다시 렌더링된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 MemoForm에서 중요한 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;App의 title state
&amp;rarr; MemoForm에 title props로 전달
&amp;rarr; input value={title}로 표시
&amp;rarr; 사용자가 입력
&amp;rarr; onTitleChange 실행
&amp;rarr; setTitle로 App의 title state 변경
&amp;rarr; 다시 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 값의 흐름을 이렇게 생각하면 된다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;데이터는 부모가 가지고 있고,
자식은 props로 받아서 화면에 사용한다.
자식이 데이터를 바꿔야 할 때는
부모에게서 받은 함수를 실행한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;컴포넌트 분리
= 화면을 역할별 파일로 나누는 작업

props
= 부모 컴포넌트가 자식 컴포넌트에게 넘겨주는 값 또는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx는 상태와 기능을 가지고 있고,&lt;br /&gt;MemoForm, MemoList, MemoItem은 각각 필요한 값과 함수를 props로 받아 화면을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/179</guid>
      <comments>https://woojoo-devlog.tistory.com/179#entry179comment</comments>
      <pubDate>Sat, 20 Jun 2026 09:43:06 +0900</pubDate>
    </item>
    <item>
      <title>#14 React에서 localStorage로 메모 저장하기</title>
      <link>https://woojoo-devlog.tistory.com/178</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 React 버전 메모장에 localStorage 저장 기능을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 메모를 추가, 수정, 삭제할 수 있었지만 새로고침하면 데이터가 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 메모가 React state인 memos 안에만 있었기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;const [memos, setMemos] = useState([])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저를 새로고침하면 React 앱이 다시 시작되고, state는 다시 초기값으로 돌아간다.&lt;br /&gt;즉, 새로고침하면 memos는 다시 빈 배열이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메모를 브라우저에 남겨두기 위해 localStorage를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useEffect 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage 작업을 처리하기 위해 useEffect를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 useState만 import하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import { useState } from 'react'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 useEffect를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import { useState, useEffect } from 'react'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState는 화면에 영향을 주는 데이터를 관리하는 기능이고,&lt;br /&gt;useEffect는 렌더링 이후에 실행할 작업을 처리하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 localStorage에서 데이터를 불러오거나 저장하는 작업에 useEffect를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 실행될 때 localStorage에서 불러오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 앱이 처음 열릴 때 localStorage에 저장된 메모를 가져오도록 했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const savedMemos = localStorage.getItem('memos')

  if (savedMemos !== null) {
    setMemos(JSON.parse(savedMemos))
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 앱이 처음 렌더링된 뒤 한 번 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 있는 빈 배열 []이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뜻은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;처음 렌더링된 뒤 한 번만 실행해라.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서 만들었던 loadMemos()와 비슷한 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;localStorage.getItem&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 메모를 가져올 때는 localStorage.getItem()을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const savedMemos = localStorage.getItem('memos')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 localStorage에서 memos라는 이름으로 저장된 값을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage에 값이 없으면 null이 나온다.&lt;br /&gt;그래서 바로 사용하지 않고 먼저 확인했다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (savedMemos !== null) {
  setMemos(JSON.parse(savedMemos))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 값이 있을 때만 setMemos()를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSON.parse가 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage는 배열이나 객체를 그대로 저장하지 못한다.&lt;br /&gt;문자열만 저장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저장할 때는 배열을 문자열로 바꿔야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JSON.stringify(memos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 꺼낼 때는 문자열을 다시 배열로 바꿔야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JSON.parse(savedMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 코드는,&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;setMemos(JSON.parse(savedMemos))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;localStorage에서 꺼낸 문자열을
JSON.parse로 배열로 바꾼 뒤
setMemos로 memos state에 넣는다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀어 쓰면 다음 코드와 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const parsedMemos = JSON.parse(savedMemos)
setMemos(parsedMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;memos가 바뀔 때마다 localStorage에 저장하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불러오기만 하면 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가, 수정, 삭제했을 때 변경된 memos를 다시 localStorage에 저장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 두 번째 useEffect를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  localStorage.setItem('memos', JSON.stringify(memos))
}, [memos])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 마지막에 있는 [memos]가 중요하다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;}, [memos])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뜻은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;memos state가 바뀔 때마다 이 useEffect를 실행해라.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메모를 추가하면 다음 순서로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;setMemos 실행
&amp;rarr; memos state 변경
&amp;rarr; useEffect 실행
&amp;rarr; localStorage 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제해도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;setMemos 실행
&amp;rarr; memos state 변경
&amp;rarr; useEffect 실행
&amp;rarr; localStorage 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정해도 같은 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;setMemos 실행
&amp;rarr; memos state 변경
&amp;rarr; useEffect 실행
&amp;rarr; localStorage 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;localStorage.setItem&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장할 때는 localStorage.setItem()을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;localStorage.setItem('memos', JSON.stringify(memos))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 현재 memos 배열을 localStorage에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 localStorage에는 문자열만 저장할 수 있으므로 JSON.stringify()를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JSON.stringify(memos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 배열을 문자열로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배열이 이렇게 있다면,&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[
  { id: 1, title: 'A', content: 'aaa' }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열로 바뀌면 이런 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;'[{&quot;id&quot;:1,&quot;title&quot;:&quot;A&quot;,&quot;content&quot;:&quot;aaa&quot;}]'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문자열이 localStorage에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useEffect의 역할 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 사용한 useEffect는 두 개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 useEffect는 앱이 처음 실행될 때 저장된 메모를 불러온다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const savedMemos = localStorage.getItem('memos')

  if (savedMemos !== null) {
    setMemos(JSON.parse(savedMemos))
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 useEffect는 memos가 바뀔 때마다 저장한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  localStorage.setItem('memos', JSON.stringify(memos))
}, [memos])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;useState
&amp;rarr; React가 기억해야 하는 화면 데이터 관리

useEffect
&amp;rarr; 렌더링 이후에 실행해야 하는 부가 작업 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage는 화면을 그리는 JSX가 아니라 브라우저 저장소와 관련된 작업이므로 useEffect에서 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로고침했을 때의 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 새로고침을 하면 다음 순서로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. React 앱이 다시 시작된다.
2. memos state는 처음에 빈 배열로 시작한다.
3. 화면이 한 번 렌더링된다.
4. 첫 번째 useEffect가 실행된다.
5. localStorage에서 저장된 문자열을 가져온다.
6. JSON.parse로 배열로 바꾼다.
7. setMemos로 memos state에 넣는다.
8. memos가 바뀌었으므로 React가 다시 렌더링한다.
9. 저장돼 있던 메모가 화면에 보인다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 새로고침해도 메모가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 추가, 수정, 삭제 후의 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가하면 다음처럼 동작한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;새 메모 객체 생성
&amp;rarr; setMemos로 memos state 변경
&amp;rarr; React가 다시 렌더링
&amp;rarr; 두 번째 useEffect 실행
&amp;rarr; 변경된 memos를 localStorage에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 수정해도 흐름은 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;수정된 새 배열 생성
&amp;rarr; setMemos로 memos state 변경
&amp;rarr; React가 다시 렌더링
&amp;rarr; 두 번째 useEffect 실행
&amp;rarr; 변경된 memos를 localStorage에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 삭제해도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;삭제된 새 배열 생성
&amp;rarr; setMemos로 memos state 변경
&amp;rarr; React가 다시 렌더링
&amp;rarr; 두 번째 useEffect 실행
&amp;rarr; 변경된 memos를 localStorage에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심은 memos state와 localStorage를 연결한 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;앱이 처음 열릴 때
&amp;rarr; localStorage에서 메모를 불러온다.

memos가 바뀔 때마다
&amp;rarr; localStorage에 다시 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 React 버전 메모장도 새로고침 후 데이터가 유지된다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/178</guid>
      <comments>https://woojoo-devlog.tistory.com/178#entry178comment</comments>
      <pubDate>Fri, 19 Jun 2026 15:52:55 +0900</pubDate>
    </item>
    <item>
      <title>#13 React에서 메모 삭제 기능 구현하기</title>
      <link>https://woojoo-devlog.tistory.com/177</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 React 버전 메모장에 삭제 기능을 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 JavaScript에서는 삭제 버튼을 누르면 배열에서 해당 메모를 제거하고, 직접 renderMemos()를 호출해서 화면을 다시 그렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 방식이 조금 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 화면을 직접 다시 그리는 함수를 호출하지 않는다.&lt;br /&gt;대신 state를 변경하면 React가 자동으로 화면을 다시 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 기능의 기본 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 삭제 기능의 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; 삭제할 메모 id 전달
&amp;rarr; confirm으로 삭제 여부 확인
&amp;rarr; memos 배열에서 해당 id를 가진 메모 제거
&amp;rarr; setMemos로 새 배열 저장
&amp;rarr; React가 자동으로 화면 다시 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 이런 흐름이었다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; memos 배열 변경
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 이렇게 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; memos 배열 변경
&amp;rarr; setMemos()
&amp;rarr; 자동 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 renderMemos() 같은 함수를 직접 호출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 삭제 기능을 담당하는 함수를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function handleDeleteMemo(id) {
  const isConfirmed = confirm('정말 삭제하시겠습니까?')

  if (!isConfirmed) {
    return
  }

  const nextMemos = memos.filter((memo) =&amp;gt; {
    return memo.id !== id
  })

  setMemos(nextMemos)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 삭제할 메모의 id를 매개변수로 받는다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function handleDeleteMemo(id) {
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 id는 어떤 메모를 삭제할지 구분하기 위한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;confirm으로 삭제 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제는 실수로 누를 수 있기 때문에 먼저 확인창을 띄웠다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;const isConfirmed = confirm('정말 삭제하시겠습니까?')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 확인을 누르면 true, 취소를 누르면 false가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (!isConfirmed) {
  return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소를 누른 경우에는 return으로 함수를 바로 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 아래에 있는 삭제 로직은 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;filter로 삭제할 메모 제외하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 삭제는 filter()를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const nextMemos = memos.filter((memo) =&amp;gt; {
  return memo.id !== id
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 부분이 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter()는 배열을 하나씩 돌면서 조건이 true인 요소만 새 배열에 남긴다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;return true  &amp;rarr; 남긴다
return false &amp;rarr; 제외한다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 조건은 다음처럼 읽을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;memo.id !== id
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;현재 검사 중인 memo의 id가
삭제하려는 id와 다르면 남긴다.
같으면 제외한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모 배열이 이렇게 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[
  { id: 1, title: 'A' },
  { id: 2, title: 'B' },
  { id: 3, title: 'C' }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id가 2인 메모를 삭제하려고 할 때 조건은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;memo.id !== 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검사 결과는 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;id 1 &amp;rarr; 1 !== 2 &amp;rarr; true  &amp;rarr; 남김
id 2 &amp;rarr; 2 !== 2 &amp;rarr; false &amp;rarr; 제외
id 3 &amp;rarr; 3 !== 2 &amp;rarr; true  &amp;rarr; 남김
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과 배열은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[
  { id: 1, title: 'A' },
  { id: 3, title: 'C' }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 배열을 nextMemos라고 이름 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;setMemos로 state 변경하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제된 새 배열을 만든 뒤에는 setMemos()를 호출했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;setMemos(nextMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 실행되면 React가 memos state를 새 배열로 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 state가 바뀌었기 때문에 React가 자동으로 컴포넌트를 다시 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;setMemos 실행
&amp;rarr; memos state 변경
&amp;rarr; React가 App 컴포넌트 다시 실행
&amp;rarr; memos.map 부분 다시 실행
&amp;rarr; 삭제된 메모가 빠진 목록이 화면에 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JavaScript에서 직접 호출하던 renderMemos()가 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 버튼 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 함수만 만들면 아직 화면에서 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메모 카드에 삭제 버튼을 추가하고, 클릭하면 해당 메모의 id를 넘기도록 했다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{memos.map((memo) =&amp;gt; (
  &amp;lt;div className=&quot;memo-card&quot; key={memo.id}&amp;gt;
    &amp;lt;h3&amp;gt;{memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; handleDeleteMemo(memo.id)}&amp;gt;
      삭제
    &amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 다음 코드다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;onClick={() =&amp;gt; handleDeleteMemo(memo.id)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 삭제 버튼을 클릭했을 때 handleDeleteMemo()를 실행한다.&lt;br /&gt;그리고 현재 메모의 id를 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;첫 번째 메모의 삭제 버튼 클릭
&amp;rarr; 첫 번째 memo.id 전달

두 번째 메모의 삭제 버튼 클릭
&amp;rarr; 두 번째 memo.id 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 바로 실행하면 안 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 이렇게 쓰고 싶을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;onClick={handleDeleteMemo(memo.id)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 쓰면 클릭했을 때 실행되는 것이 아니라, 렌더링되는 순간 바로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 함수로 감싸야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;onClick={() =&amp;gt; handleDeleteMemo(memo.id)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쓰면 React에게 다음과 같은 의미가 된다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;지금 실행하지 말고,
클릭했을 때 이 함수를 실행해라.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 이전에 addEventListener에서 함수를 등록했던 개념과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 전체 코드 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메모 목록 출력 부분은 다음과 같은 구조가 되었다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{memos.length === 0 &amp;amp;&amp;amp; (
  &amp;lt;p className=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
)}

{memos.map((memo) =&amp;gt; (
  &amp;lt;div className=&quot;memo-card&quot; key={memo.id}&amp;gt;
    &amp;lt;h3&amp;gt;{memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; handleDeleteMemo(memo.id)}&amp;gt;
      삭제
    &amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모가 없으면 빈 메시지를 보여준다.&lt;br /&gt;메모가 있으면 memos.map()으로 메모 카드를 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메모 카드에는 삭제 버튼이 있고, 삭제 버튼은 자기 메모의 id를 handleDeleteMemo()로 넘긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 단계에서 이해한 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 삭제 기능에서 가장 중요한 점은 React의 렌더링 흐름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 DOM을 직접 삭제하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript에서는 이런 식으로 생각했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; DOM 요소 삭제
&amp;rarr; 화면 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 이렇게 생각해야 한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; state 배열에서 데이터 제거
&amp;rarr; setMemos로 state 변경
&amp;rarr; React가 state를 기준으로 화면을 다시 그림
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 화면이 기준이 아니라 데이터가 기준이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;화면을 직접 지우는 것이 아니라
데이터에서 지우면
화면은 React가 알아서 바꾼다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 핵심은 삭제 기능 자체보다 React에서 삭제를 바라보는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript에서는 화면에서 직접 요소를 제거하거나, renderMemos()를 직접 호출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 memos state에서 삭제할 데이터를 제외한 새 배열을 만들고, setMemos()로 상태를 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;const nextMemos = memos.filter((memo) =&amp;gt; {
  return memo.id !== id
})

setMemos(nextMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 React가 변경된 memos state를 기준으로 화면을 다시 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 화면을 직접 수정하는 것이 아니라 &lt;b&gt;state를 수정해서 화면이 바뀌게 만든다&lt;/b&gt;.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/177</guid>
      <comments>https://woojoo-devlog.tistory.com/177#entry177comment</comments>
      <pubDate>Thu, 18 Jun 2026 16:51:58 +0900</pubDate>
    </item>
    <item>
      <title>#12 React 프로젝트 시작과 입력값 상태 관리</title>
      <link>https://woojoo-devlog.tistory.com/176</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript로 메모장 CRUD 기능을 구현한 뒤, 이번에는 같은 메모장을 React로 다시 만들어보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 목표는 메모 추가, 수정, 삭제까지 구현하는 것이 아니다.&lt;br /&gt;먼저 React 프로젝트를 만들고, 기존 HTML/CSS 메모장 화면을 React로 옮긴 뒤, 입력창 값을 React의 state로 관리하는 것까지 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 한 일은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;React 프로젝트 생성
Vite 기본 화면 제거
기존 메모장 화면을 JSX로 작성
제목 입력값을 state로 관리
내용 입력값을 state로 관리
onChange로 입력값 갱신
onClick으로 버튼 클릭 시 state 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트는 기존 JavaScript 파일을 지우지 않고, 별도 폴더에 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Memo Evolution/
  index.html
  style.css
  script.js
  react-memo/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트는 react-memo 폴더 안에 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite를 사용해서 React 프로젝트를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;npm create vite@latest react-memo -- --template react
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 React 프로젝트 폴더로 이동해서 패키지를 설치했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd react-memo
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버는 다음 명령어로 실행한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서는 보통 다음 주소로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 서버와 HMR&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm run dev를 실행하면 Vite 개발 서버가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버가 켜져 있는 상태에서 App.jsx나 App.css를 수정하고 저장하면 보통 브라우저에 자동으로 반영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 HMR이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;HMR = Hot Module Replacement
파일 저장 시 브라우저에 변경 사항을 빠르게 반영하는 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순히 컴포넌트나 CSS를 수정할 때마다 npm run dev를 다시 실행할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 다음 경우에는 재실행이 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;개발 서버를 꺼버린 경우
새 라이브러리를 설치한 경우
package.json을 수정한 경우
vite.config.js를 수정한 경우
개발 서버가 에러로 종료된 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vite 기본 코드 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Vite로 React 프로젝트를 만들면 기본 예제 화면이 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx에는 로고, 카운터 버튼, 문서 링크 같은 코드가 있었다.&lt;br /&gt;하지만 이번 프로젝트의 목표는 메모장을 React로 다시 만드는 것이므로 기본 코드는 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서 필요 없어진 것들은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const [count, setCount] = useState(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운터 예제도 제거했다.&lt;br /&gt;이번 단계에서는 메모장 화면과 입력 상태 관리만 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React에서 정적인 메모장 화면 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript 단계에서 작성했던 HTML 구조를 React의 JSX로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 컴포넌트는 기본적으로 함수 형태다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;function App() {
  return (
    ...
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 App.jsx는 메모장 화면 전체를 담당한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from 'react'
import './App.css'

function App() {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  function handleAddMemo() {
    console.log(title)
    console.log(content)
  }

  return (
    &amp;lt;div className=&quot;app&quot;&amp;gt;
      &amp;lt;div className=&quot;memo-form&quot;&amp;gt;
        &amp;lt;h2&amp;gt;새 메모 작성&amp;lt;/h2&amp;gt;

        &amp;lt;label htmlFor=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
        &amp;lt;input
          id=&quot;memo-title&quot;
          type=&quot;text&quot;
          placeholder=&quot;제목을 입력하세요&quot;
          value={title}
          onChange={(event) =&amp;gt; setTitle(event.target.value)}
        /&amp;gt;

        &amp;lt;label htmlFor=&quot;memo-content&quot;&amp;gt;내용&amp;lt;/label&amp;gt;
        &amp;lt;textarea
          id=&quot;memo-content&quot;
          placeholder=&quot;내용을 입력하세요&quot;
          value={content}
          onChange={(event) =&amp;gt; setContent(event.target.value)}
        &amp;gt;&amp;lt;/textarea&amp;gt;

        &amp;lt;button type=&quot;button&quot; onClick={handleAddMemo}&amp;gt;
          메모 추가
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;memo-list&quot;&amp;gt;
        &amp;lt;h2&amp;gt;메모 목록&amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}

export default App
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSX에서 HTML과 다른 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 사용하는 JSX는 HTML과 비슷하게 생겼지만, 완전히 같지는 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 class 대신 className을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서는 다음처럼 작성했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;app&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX에서는 이렇게 작성한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;app&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 label에서 사용하는 for도 JSX에서는 htmlFor로 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;label for=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX에서는 이렇게 쓴다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;label htmlFor=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 input처럼 닫는 태그가 없는 요소는 JSX에서 self-closing 형태로 작성한다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;&amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 기억할 JSX 차이는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class &amp;rarr; className
for &amp;rarr; htmlFor
input은 /&amp;gt;로 닫기
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useState로 입력값 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 입력값을 버튼 클릭 시점에 DOM에서 직접 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const title = titleInput.value.trim();
const content = contentInput.value.trim();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 입력값을 보통 state로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 제목과 내용을 각각 state로 만들었다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [title, setTitle] = useState('')
const [content, setContent] = useState('')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState('')는 초기값이 빈 문자열인 상태를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;title
= 현재 제목 입력값

setTitle
= title 값을 바꾸는 함수

content
= 현재 내용 입력값

setContent
= content 값을 바꾸는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setTitle, setContent는 내가 직접 정의한 함수가 아니라 useState가 만들어주는 상태 변경 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 직접 정할 수 있지만, 보통 관례상 set + 상태이름 형태로 작성한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [title, setTitle] = useState('')
const [content, setContent] = useState('')
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;input의 value 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 입력창의 값을 state와 연결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;input
  value={title}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음 의미다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;이 input에 보이는 값은 title state다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력창에 표시되는 값이 React의 title 상태를 따라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용 입력창도 마찬가지다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;textarea
  value={content}
&amp;gt;&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 입력창의 값은 각각 title, content 상태와 연결된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onChange로 state 갱신하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value만 연결하면 입력이 제대로 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력창의 값을 state와 연결했다면, 사용자가 입력할 때 state도 같이 바꿔줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 onChange를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;&amp;lt;input
  value={title}
  onChange={(event) =&amp;gt; setTitle(event.target.value)}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;사용자가 input에 글자를 입력한다.
onChange 이벤트가 실행된다.
event.target.value에 현재 입력값이 들어 있다.
setTitle(event.target.value)가 실행된다.
title state가 변경된다.
React가 다시 렌더링한다.
input에는 변경된 title 값이 표시된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용 입력창도 같은 방식이다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;textarea
  value={content}
  onChange={(event) =&amp;gt; setContent(event.target.value)}
&amp;gt;&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 value와 onChange를 함께 사용하는 입력 방식을 controlled input이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 용어보다 구조를 이해하는 것이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;value
= input이 보여줄 값

onChange
= 사용자가 입력할 때 state를 갱신하는 이벤트
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onClick으로 버튼 이벤트 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 버튼을 찾고 이벤트를 직접 붙였다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 JSX에서 버튼에 바로 이벤트를 연결한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot; onClick={handleAddMemo}&amp;gt;
  메모 추가
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handleAddMemo는 버튼을 클릭했을 때 실행될 함수다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function handleAddMemo() {
  console.log(title)
  console.log(content)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 버튼에 onClick을 연결하지 않아서 콘솔이 찍히지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 코드:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 버튼을 눌러도 아무 함수도 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 코드:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot; onClick={handleAddMemo}&amp;gt;
  메모 추가
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해야 버튼 클릭 시 handleAddMemo()가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JS와 React 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 가장 중요한 비교는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 DOM에서 값을 직접 읽었다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const title = titleInput.value;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 state로 값을 관리한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [title, setTitle] = useState('')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 버튼을 찾아 이벤트를 직접 붙였다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 JSX에서 이벤트를 연결한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button onClick={handleAddMemo}&amp;gt;
  메모 추가
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 입력값을 input.value로 읽었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;input.value
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 value와 onChange를 사용해 state와 연결한다.&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;&amp;lt;input value={title} onChange={...} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 DOM에서 값을 나중에 꺼내오기보다, 입력하는 순간 state로 값을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/176</guid>
      <comments>https://woojoo-devlog.tistory.com/176#entry176comment</comments>
      <pubDate>Tue, 16 Jun 2026 22:03:18 +0900</pubDate>
    </item>
    <item>
      <title>#11 React 시작하기</title>
      <link>https://woojoo-devlog.tistory.com/175</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript로 메모장 CRUD 기능을 구현한 뒤, 이제 같은 메모장을 React로 다시 만들어보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 React 단계의 목적은 새로운 기능을 추가하는 것이 아니다.&lt;br /&gt;기존에 만든 메모장 기능을 React 방식으로 다시 구현하면서, JavaScript와 React의 차이를 이해하는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JavaScript에서 했던 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서는 HTML 요소를 직접 가져오고, 직접 이벤트를 붙이고, 직접 화면을 다시 그렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 코드들을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;document.getElementById(...)
addEventListener(...)
document.createElement(...)
innerHTML
appendChild(...)
renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 직접 DOM을 조작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 배열이 바뀌면 직접 renderMemos()를 호출해서 화면을 다시 그렸다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;memos.push(newMemo);
saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 흐름을 이해하기에는 좋았다.&lt;br /&gt;하지만 기능이 많아질수록 DOM을 직접 다루는 코드가 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React에서는 무엇이 달라질까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 DOM을 직접 조작하는 방식에서 벗어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 직접 요소를 만들고 화면에 붙였다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const memoCard = document.createElement('div');
memoContainer.appendChild(memoCard);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 화면이 어떻게 생겨야 하는지를 JSX로 작성한다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;memo-card&quot;&amp;gt;
  &amp;lt;h3&amp;gt;{memo.title}&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터는 일반 변수 대신 state로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서는 이렇게 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 나중에 다음처럼 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;const [memos, setMemos] = useState([]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 useState를 깊게 다루지는 않았다.&lt;br /&gt;일단은 이렇게 이해했다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;memos
= 현재 메모 배열

setMemos
= memos 상태를 바꾸는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 중요한 점은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;state가 바뀌면 React가 화면을 다시 그린다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JavaScript에서 직접 만들었던 renderMemos() 역할을 React가 어느 정도 대신해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트는 기존 JavaScript 파일을 지우지 않고, 별도 폴더에 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Memo Evolution/
  index.html
  style.css
  script.js
  react-memo/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트는 react-memo 폴더 안에 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite를 사용해서 React 프로젝트를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;npm create vite@latest react-memo -- --template react
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 React 프로젝트 폴더로 이동해서 필요한 패키지를 설치했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd react-memo
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버는 다음 명령어로 실행한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서는 보통 다음 주소로 접속한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vite 개발 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm run dev로 Vite 개발 서버를 실행하면 React 앱을 로컬에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 서버를 실행한 뒤에는 App.jsx, App.css 같은 파일을 수정할 때마다 보통 자동으로 화면이 갱신된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 HMR이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;HMR = Hot Module Replacement
파일을 저장하면 브라우저에 바로 반영되는 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순히 App.jsx나 App.css를 수정할 때마다 매번 서버를 재실행할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 다음 경우에는 재실행이 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;npm run dev를 꺼버린 경우
새 라이브러리를 설치한 경우
package.json을 수정한 경우
vite.config.js를 수정한 경우
개발 서버가 에러로 종료된 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vite 기본 화면 제거하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 React 프로젝트를 만들면 Vite 기본 화면이 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx에는 React 로고, Vite 로고, 카운터 예제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 목표는 메모장을 다시 만드는 것이므로 기본 코드는 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 있던 예시 코드들은 필요 없었다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const [count, setCount] = useState(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 아직 useState를 쓰지 않는다.&lt;br /&gt;목표는 단순히 React에서 정적인 메모장 화면을 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React에서 정적인 메모장 화면 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 HTML에서 만들었던 메모장 구조를 React의 JSX로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-memo/src/App.jsx는 다음과 같은 구조로 만들었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import './App.css'

function App() {
  return (
    &amp;lt;div className=&quot;app&quot;&amp;gt;
      &amp;lt;div className=&quot;memo-form&quot;&amp;gt;
        &amp;lt;h2&amp;gt;새 메모 작성&amp;lt;/h2&amp;gt;

        &amp;lt;label htmlFor=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
        &amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; placeholder=&quot;제목을 입력하세요&quot; /&amp;gt;

        &amp;lt;label htmlFor=&quot;memo-content&quot;&amp;gt;내용&amp;lt;/label&amp;gt;
        &amp;lt;textarea id=&quot;memo-content&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;&amp;lt;/textarea&amp;gt;

        &amp;lt;button type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;memo-list&quot;&amp;gt;
        &amp;lt;h2&amp;gt;메모 목록&amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}

export default App
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 기능은 없다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;버튼 클릭 기능 없음
입력값 관리 없음
메모 추가 없음
localStorage 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계는 화면 구조만 React로 옮기는 것이 목표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML과 JSX의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 HTML과 비슷하게 생긴 JSX를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 완전히 같은 문법은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 만난 차이는 class와 for였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서는 다음처럼 쓴다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;app&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React JSX에서는 className을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;app&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 HTML에서는 label과 input을 연결할 때 for를 쓴다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;label for=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React JSX에서는 htmlFor를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;label htmlFor=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 JSX가 JavaScript 문법 위에서 동작하기 때문이다.&lt;br /&gt;class나 for는 JavaScript에서 이미 다른 의미로 사용되는 단어라서 React에서는 다른 이름을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나의 차이는 input 태그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서는 이렇게 써도 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JSX에서는 닫는 표시를 해주는 방식이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;&amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 기억할 JSX 차이는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;class &amp;rarr; className
for &amp;rarr; htmlFor
input은 self-closing으로 작성
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSS 옮기기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 JavaScript 메모장에서 사용하던 CSS를 React 프로젝트의 App.css로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 파일은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;react-memo/src/App.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite 기본 CSS는 메모장 프로젝트에 필요 없어서 제거하고, 기존 메모장 스타일을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;body {
  margin: 0;
  font-family: sans-serif;
  background-color: #f4f4f4;
}

.app {
  width: 500px;
  margin: 40px auto;
  padding: 24px;
  background-color: white;
}

.memo-form {
  margin-bottom: 32px;
}

.memo-form label {
  display: block;
  margin-top: 12px;
  margin-bottom: 6px;
}

button {
  margin-top: 12px;
  padding: 10px 16px;
  cursor: pointer;
}

.empty-message {
  color: #777;
}

.memo-form input,
.memo-form textarea {
  display: block;
  width: 100%;
  padding: 10px;
  box-sizing: border-box;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 React에서도 기존과 비슷한 정적 메모장 화면을 볼 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계는 React 기능 구현이 아니라 React 환경에 진입하는 단계였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;기존 HTML/CSS 메모장 화면을 React 프로젝트 안으로 옮겼다.
React에서는 JSX로 화면을 작성한다.
class는 className으로, for는 htmlFor로 쓴다.
아직 기능은 없고 정적인 화면만 만든 상태다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 useState를 사용해서 입력값과 메모 배열을 React 상태로 관리하겠다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/175</guid>
      <comments>https://woojoo-devlog.tistory.com/175#entry175comment</comments>
      <pubDate>Mon, 15 Jun 2026 20:48:09 +0900</pubDate>
    </item>
    <item>
      <title>#10 React로 넘어가기 전에 컴포넌트 이해하기</title>
      <link>https://woojoo-devlog.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript로 메모장의 기본 CRUD 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 구현한 기능은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;메모 추가
메모 목록 조회
메모 수정
메모 삭제
localStorage 저장
새로고침 후 데이터 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다음 단계에서는 기존 메모장 기능을 React로 다시 구현해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 React 프로젝트를 만들기 전에, 먼저 React에서 가장 기본이 되는 &lt;b&gt;컴포넌트&lt;/b&gt; 개념을 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트는 쉽게 말하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;화면의 한 부분을 담당하는 JavaScript 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 만든 메모장 화면을 보면 여러 영역으로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;전체 앱
메모 입력 영역
메모 목록 영역
메모 카드 하나
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript 단계에서는 HTML 안에 화면 구조가 전부 같이 들어 있었다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;app&quot;&amp;gt;
    &amp;lt;div class=&quot;memo-form&quot;&amp;gt;...&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;memo-list&quot;&amp;gt;...&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 이런 화면 구조를 함수 단위로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
    return (
        &amp;lt;div className=&quot;app&quot;&amp;gt;
            &amp;lt;MemoForm /&amp;gt;
            &amp;lt;MemoList /&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 각각의 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;App
= 전체 화면 컴포넌트

MemoForm
= 메모 작성 영역 컴포넌트

MemoList
= 메모 목록 영역 컴포넌트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React에서는 화면을 하나의 큰 HTML 덩어리로 보는 것이 아니라, 여러 컴포넌트를 조립해서 만드는 방식으로 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 컴포넌트로 나누나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 파일에 모든 UI를 다 작성하면 처음에는 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기능이 조금만 커져도 코드가 복잡해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모장만 봐도 다음 코드들이 한 곳에 섞일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력창 코드
버튼 코드
메모 목록 코드
수정 코드
삭제 코드
저장 코드
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 별문제가 없어 보이지만, 나중에는 어디가 입력 영역이고 어디가 목록 영역인지 구분하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트로 나누면 역할이 더 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;MemoForm
= 제목/내용 입력, 추가 버튼

MemoList
= 메모 목록 출력

MemoItem
= 메모 카드 하나, 수정/삭제 버튼
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 컴포넌트는 HTML을 기능별 조각으로 나누는 느낌이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 메모장을 React 컴포넌트로 나누면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메모장 구조를 React 컴포넌트로 나누면 다음과 같이 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;App
- 전체 상태 memos를 가지고 있음
- addMemo, editMemo, deleteMemo 함수 가지고 있음

MemoForm
- 제목/내용 입력
- 추가 버튼

MemoList
- memos 배열을 받아서 목록 출력

MemoItem
- 메모 하나 표시
- 수정/삭제 버튼
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;App
 ├─ MemoForm
 └─ MemoList
      ├─ MemoItem
      ├─ MemoItem
      └─ MemoItem
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 App이 전체 데이터를 가지고 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript에서는 memos 배열을 전역 변수처럼 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서도 처음에는 비슷하게 App 컴포넌트가 메모 목록 데이터를 가지고 시작하면 된다.&lt;br /&gt;그다음 MemoForm, MemoList, MemoItem으로 필요한 값을 내려보내는 식으로 구조를 잡을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React를 처음 시작할 때 모든 컴포넌트를 처음부터 나누려고 하면 오히려 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 App 컴포넌트 하나에 전체 기능을 작성해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. App 컴포넌트 하나에서 메모장 구현
2. MemoForm 컴포넌트 분리
3. MemoList 컴포넌트 분리
4. MemoItem 컴포넌트 분리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 완벽한 구조를 만들려고 하기보다, 먼저 동작하게 만들고 그다음에 역할별로 나누는 방식이 더 이해하기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vanilla JS와 React 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla JavaScript에서는 메모 카드를 만들 때 직접 HTML 요소를 생성했다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;const memoCard = document.createElement('div');

memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
`;

memoContainer.appendChild(memoCard);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JavaScript로 HTML 요소를 직접 만들고, 직접 화면에 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 컴포넌트 함수가 JSX를 반환한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function MemoItem() {
    return (
        &amp;lt;div className=&quot;memo-card&quot;&amp;gt;
            &amp;lt;h3&amp;gt;제목&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;내용&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 HTML처럼 생겼지만, 실제로는 JavaScript 안에서 작성하는 JSX 문법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Vanilla JS
= JS로 HTML 요소를 직접 만들고 DOM에 붙인다.

React
= 컴포넌트 함수가 JSX를 반환하고, React가 화면을 갱신한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 React를 배우면서 가장 먼저 익숙해져야 하는 부분이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;class와 className&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 HTML의 class 대신 className을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vanilla HTML에서는 이렇게 작성했다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React JSX에서는 이렇게 작성한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 어색하지만, JSX가 HTML 그대로가 아니라 JavaScript 안에서 사용하는 문법이기 때문에 className을 사용한다고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;컴포넌트는 화면 조각을 담당하는 함수다.
React 화면은 컴포넌트들을 조립해서 만든다.
처음에는 App 하나로 시작해도 된다.
나중에 MemoForm, MemoList, MemoItem으로 나누면 된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 단계에서는 React의 모든 개념을 한 번에 이해하려고 하기보다, 기존에 만든 메모장 화면을 어떻게 컴포넌트로 나눌 수 있는지만 먼저 잡으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터는 같은 메모장 기능을 React 방식으로 다시 구현하면서, 기존 JavaScript 방식과 어떤 점이 다른지 비교해볼 예정이다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/174</guid>
      <comments>https://woojoo-devlog.tistory.com/174#entry174comment</comments>
      <pubDate>Mon, 15 Jun 2026 10:25:15 +0900</pubDate>
    </item>
    <item>
      <title>#9 JavaScript 코드 구조 정리하기</title>
      <link>https://woojoo-devlog.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지 메모장에는 기본 CRUD 기능이 들어갔다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Create &amp;rarr; 메모 추가
Read   &amp;rarr; 메모 목록 보기
Update &amp;rarr; 메모 수정
Delete &amp;rarr; 메모 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 localStorage를 사용해서 새로고침 후에도 메모가 유지되도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 새로운 기능을 크게 추가하기보다는, 기존 코드를 더 이해하기 좋은 구조로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 목표는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가, 수정, 삭제 로직을 함수로 분리하기&lt;/li&gt;
&lt;li&gt;입력값 앞뒤 공백 처리하기&lt;/li&gt;
&lt;li&gt;삭제 전 확인창 추가하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 함수로 분리했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 버튼 이벤트 안에 모든 로직이 들어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 삭제 버튼 이벤트 안에서 바로 배열을 수정하고, 저장하고, 다시 렌더링했다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;deleteButton.addEventListener('click', function () {
    memos = memos.filter(function (item) {
        return item.id !== memo.id;
    });

    saveMemos();
    renderMemos();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식도 동작은 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드가 길어질수록 이벤트 안에 너무 많은 일이 들어가게 된다.&lt;br /&gt;이벤트 안에 추가, 수정, 삭제 로직이 그대로 들어 있으면 나중에 코드를 읽을 때 흐름을 파악하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 역할별로 함수를 나눴다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;addMemo()
= 메모 추가 담당

editMemo()
= 메모 수정 담당

deleteMemo()
= 메모 삭제 담당

renderMemos()
= 화면 렌더링 담당

saveMemos()
= localStorage 저장 담당

loadMemos()
= localStorage 불러오기 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나누면 각 함수가 어떤 일을 하는지 더 명확하게 보인다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 코드 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 script.js의 큰 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const titleInput = document.getElementById(&quot;memo-title&quot;);
const contentInput = document.getElementById(&quot;memo-content&quot;);
const addMemoButton = document.getElementById(&quot;add-memo-button&quot;);
const memoContainer = document.getElementById(&quot;memo-container&quot;);
const emptyMessage = document.getElementById(&quot;empty-message&quot;);

let memos = [];

function saveMemos() {
    ...
}

function loadMemos() {
    ...
}

function updateEmptyMessage() {
    ...
}

function deleteMemo(id) {
    ...
}

function editMemo(id) {
    ...
}

function addMemo(title, content) {
    ...
}

function renderMemos() {
    ...
}

addMemoButton.addEventListener('click', function () {
    ...
});

loadMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서부터 보면 흐름이 이전보다 조금 더 명확하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. HTML 요소 가져오기
2. 메모 데이터 배열 준비
3. 저장/불러오기 함수
4. 빈 메시지 처리 함수
5. 삭제/수정/추가 함수
6. 렌더링 함수
7. 이벤트 등록
8. 초기 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 로직 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 삭제 로직을 deleteMemo(id) 함수로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;function deleteMemo(id) {
    const isConfirmed = confirm(&quot;정말 삭제하시겠습니까?&quot;);

    if (!isConfirmed) {
        return;
    }

    memos = memos.filter(function (item) {
        return item.id !== id;
    });

    saveMemos();
    renderMemos();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 삭제할 메모의 id를 매개변수로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 버튼을 클릭했을 때는 해당 메모의 id를 넘겨준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;deleteButton.addEventListener('click', function () {
    deleteMemo(memo.id);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; deleteMemo(memo.id) 호출
&amp;rarr; confirm으로 삭제 여부 확인
&amp;rarr; 취소하면 return
&amp;rarr; 확인하면 memos 배열에서 해당 id 제거
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;confirm으로 삭제 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제는 실수로 누를 수 있기 때문에 확인창을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const isConfirmed = confirm(&quot;정말 삭제하시겠습니까?&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;confirm()은 사용자가 누른 버튼에 따라 값을 반환한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;확인 &amp;rarr; true
취소 &amp;rarr; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 취소를 누른 경우에는 바로 함수를 종료한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (!isConfirmed) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 사용자가 취소를 눌렀을 때 삭제 로직이 실행되지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 로직 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 로직은 editMemo(id) 함수로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;function editMemo(id) {
    const targetMemo = memos.find(function (item) {
        return item.id === id;
    });

    const newTitle = prompt(&quot;새 제목을 입력하세요&quot;, targetMemo.title.trim());
    const newContent = prompt(&quot;새 내용을 입력하세요&quot;, targetMemo.content.trim());

    if (newTitle === null || newContent === null) {
        return;
    }

    if (newTitle.trim() === '' || newContent.trim() === '') {
        alert(&quot;제목과 내용을 모두 입력하세요.&quot;);
        return;
    }

    memos = memos.map(function (item) {
        if (item.id === id) {
            return {
                id: item.id,
                title: newTitle.trim(),
                content: newContent.trim()
            };
        }

        return item;
    });

    saveMemos();
    renderMemos();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼을 클릭하면 해당 메모의 id를 넘긴다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;editButton.addEventListener('click', function () {
    editMemo(memo.id);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정할 메모 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;editMemo(id) 함수는 id만 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 기존 제목과 내용을 prompt에 보여주려면, 먼저 memos 배열에서 해당 id를 가진 메모를 찾아야 한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const targetMemo = memos.find(function (item) {
    return item.id === id;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;find()는 배열에서 조건에 맞는 첫 번째 요소를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 다음 조건에 맞는 메모를 찾는다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;item.id가 수정하려는 id와 같은 메모
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾은 메모를 targetMemo에 담고, 기존 제목과 내용을 prompt의 기본값으로 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const newTitle = prompt(&quot;새 제목을 입력하세요&quot;, targetMemo.title.trim());
const newContent = prompt(&quot;새 내용을 입력하세요&quot;, targetMemo.content.trim());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 취소 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 prompt에서 취소를 누르면 결과는 null이다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (newTitle === null || newContent === null) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 처리해서 취소 시에는 수정 작업을 중단한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 시 공백 입력 방지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 때 공백만 입력하는 것도 막았다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (newTitle.trim() === '' || newContent.trim() === '') {
    alert(&quot;제목과 내용을 모두 입력하세요.&quot;);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trim()은 문자열 앞뒤 공백을 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 값은 공백만 있는 문자열이다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;     &quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 trim()을 적용하면 빈 문자열이 된다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;&quot;     &quot;.trim()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공백만 입력한 경우도 빈 값으로 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장할 때도 앞뒤 공백이 제거된 값으로 저장했다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;title: newTitle.trim(),
content: newContent.trim()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;map으로 해당 메모 수정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 때는 map()을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;memos = memos.map(function (item) {
    if (item.id === id) {
        return {
            id: item.id,
            title: newTitle.trim(),
            content: newContent.trim()
        };
    }

    return item;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 memos 배열을 새 배열로 다시 만드는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 방식은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. memos 배열을 하나씩 확인한다.
2. 수정하려는 id와 같은 메모를 만나면 새 제목/내용을 가진 객체를 반환한다.
3. 수정 대상이 아니면 기존 item을 그대로 반환한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 전체 배열을 돌지만 실제로 바뀌는 것은 id가 같은 메모 하나뿐이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 로직 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 로직도 addMemo(title, content) 함수로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;function addMemo(title, content) {
    if (title === '' || content === '') {
        alert('제목과 내용을 모두 입력하세요.');
        return;
    }

    const newMemo = {
        id: Date.now(),
        title: title,
        content: content
    };

    memos.push(newMemo);
    saveMemos();
    renderMemos();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 제목과 내용을 받아서 새 메모 객체를 만든다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;const newMemo = {
    id: Date.now(),
    title: title,
    content: content
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 memos 배열에 추가한다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;memos.push(newMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 localStorage에 저장하고 화면을 다시 그린다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 버튼 이벤트 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 버튼 이벤트는 이전보다 짧아졌다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    const title = titleInput.value.trim();
    const content = contentInput.value.trim();

    addMemo(title, content);

    titleInput.value = '';
    contentInput.value = '';
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값을 가져올 때 trim()을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const title = titleInput.value.trim();
const content = contentInput.value.trim();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음부터 앞뒤 공백을 제거한 값을 addMemo()에 넘긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;trim을 추가와 수정 둘 다 적용한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 추가할 때만 trim()을 하면 충분할 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 추가와 수정은 서로 다른 시점에 입력값을 받는다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;추가할 때 입력값
&amp;rarr; titleInput.value, contentInput.value

수정할 때 입력값
&amp;rarr; prompt에서 받은 newTitle, newContent
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 추가할 때 한 번 trim()했다고 해서, 수정할 때 입력한 값까지 자동으로 정리되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 수정할 때도 별도로 trim()을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;newTitle.trim()
newContent.trim()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;추가 입력값은 추가 시점에 trim
수정 입력값은 수정 시점에 trim
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;renderMemos의 역할 유지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;renderMemos()는 여전히 화면을 다시 그리는 역할이다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;function renderMemos() {
    memoContainer.innerHTML = '';

    memos.forEach(function (memo) {
        ...
    });

    updateEmptyMessage();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 데이터를 직접 추가하거나 수정하거나 삭제하지 않는다.&lt;br /&gt;현재 memos 배열을 기준으로 화면을 다시 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 메모 카드를 만들면서 각 카드의 수정/삭제 버튼에 이벤트를 연결한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;editButton.addEventListener('click', function () {
    editMemo(memo.id);
});

deleteButton.addEventListener('click', function () {
    deleteMemo(memo.id);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이벤트 안에는 복잡한 로직이 직접 들어가지 않고, 각각의 함수 호출만 남았다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;수정 버튼 클릭 &amp;rarr; editMemo(memo.id)
삭제 버튼 클릭 &amp;rarr; deleteMemo(memo.id)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 단계에서 정리된 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계 후 전체 구조는 이전보다 명확해졌다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;addMemo()
&amp;rarr; 메모 추가 담당

editMemo()
&amp;rarr; 메모 수정 담당

deleteMemo()
&amp;rarr; 메모 삭제 담당

saveMemos()
&amp;rarr; localStorage 저장 담당

loadMemos()
&amp;rarr; localStorage 불러오기 담당

renderMemos()
&amp;rarr; 화면 렌더링 담당

updateEmptyMessage()
&amp;rarr; 빈 목록 메시지 처리 담당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 함수가 맡은 역할이 분리되면서, 코드를 읽을 때 흐름을 따라가기 쉬워졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 완성 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메모장은 다음 기능을 가진다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;메모 추가
메모 목록 조회
메모 수정
메모 삭제
삭제 전 확인
공백 입력 방지
줄바꿈 유지
localStorage 저장
새로고침 후 데이터 유지
함수 단위 코드 정리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/173</guid>
      <comments>https://woojoo-devlog.tistory.com/173#entry173comment</comments>
      <pubDate>Mon, 15 Jun 2026 09:11:43 +0900</pubDate>
    </item>
    <item>
      <title>#8 메모 수정 기능 구현</title>
      <link>https://woojoo-devlog.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 메모장 CRUD 기능 중 Update, 즉 수정 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 다음 기능이 가능했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모 추가&lt;/li&gt;
&lt;li&gt;메모 목록 출력&lt;/li&gt;
&lt;li&gt;메모 삭제&lt;/li&gt;
&lt;li&gt;localStorage 저장 및 불러오기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 작성한 메모를 수정할 수는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 메모 카드에 수정 버튼을 추가하고, 사용자가 기존 제목과 내용을 바꿀 수 있도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 버튼 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 메모 카드 안에 삭제 버튼만 있었다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 기능을 만들기 위해 버튼을 하나 더 추가했다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button class=&quot;edit-button&quot; type=&quot;button&quot;&amp;gt;수정&amp;lt;/button&amp;gt;
    &amp;lt;button class=&quot;delete-button&quot; type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼이 두 개가 되었기 때문에 각각 구분할 수 있도록 class를 붙였다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;&amp;lt;button class=&quot;edit-button&quot; type=&quot;button&quot;&amp;gt;수정&amp;lt;/button&amp;gt;
&amp;lt;button class=&quot;delete-button&quot; type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JavaScript에서 각각의 버튼을 따로 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const editButton = memoCard.querySelector('.edit-button');
const deleteButton = memoCard.querySelector('.delete-button');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존처럼 memoCard.querySelector('button')만 사용하면 첫 번째 버튼 하나만 찾게 된다.&lt;br /&gt;그래서 수정 버튼과 삭제 버튼을 명확히 구분하기 위해 각각 다른 클래스를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 버튼 이벤트 등록하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼을 클릭하면 기존 제목과 내용을 수정할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼은 각 메모 카드가 만들어질 때 함께 생성된다.&lt;br /&gt;그래서 수정 버튼 이벤트도 renderMemos() 안에서 메모 카드를 만들 때 같이 등록했다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;editButton.addEventListener('click', function () {
    // 수정 처리
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이벤트 함수는 사용자가 해당 메모 카드의 수정 버튼을 눌렀을 때 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 값을 보여주고 새 값 입력받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 값을 입력받기 위해 prompt()를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const newTitle = prompt(&quot;새 제목을 입력하세요&quot;, memo.title);
const newContent = prompt(&quot;새 내용을 입력하세요&quot;, memo.content);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prompt()의 두 번째 인자는 기본값이다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;prompt(&quot;새 제목을 입력하세요&quot;, memo.title);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하면 prompt 창이 열릴 때 기존 제목이 입력창에 미리 들어가 있다.&lt;br /&gt;내용도 마찬가지로 기존 내용을 기본값으로 보여준다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;prompt(&quot;새 내용을 입력하세요&quot;, memo.content);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 수정 버튼을 누르면 기존 제목과 기존 내용이 먼저 보이고, 사용자는 그 값을 바꿔서 입력할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취소 처리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 prompt에서 취소를 누를 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prompt에서 취소를 누르면 결과값은 null이다.&lt;br /&gt;그래서 다음 조건을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (newTitle === null || newContent === null) {
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 사용자가 제목이나 내용 입력 중 하나라도 취소하면 수정 작업을 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 return은 현재 클릭 이벤트 함수를 끝내는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;취소 누름
&amp;rarr; 수정하지 않음
&amp;rarr; 저장하지 않음
&amp;rarr; 화면 다시 그리지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈 값 방지하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 때 제목이나 내용을 빈 값으로 두는 것도 막았다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (newTitle === '' || newContent === '') {
    alert(&quot;제목과 내용을 모두 입력하세요.&quot;);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목이나 내용 중 하나라도 빈 문자열이면 alert를 띄우고 수정 작업을 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 완전히 빈 문자열만 막는다.&lt;br /&gt;나중에는 trim()을 사용해서 공백만 입력한 경우도 막을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (newTitle.trim() === '' || newContent.trim() === '') {
    alert(&quot;제목과 내용을 모두 입력하세요.&quot;);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;map으로 해당 메모 수정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수정 기능의 핵심은 memos.map()이다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;memos = memos.map(function (item) {
    if (item.id === memo.id) {
        return {
            id: item.id,
            title: newTitle,
            content: newContent
        };
    }

    return item;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 부분이 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼은 메모 하나의 버튼인데, 왜 배열 전체를 도는지 의문이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 현재 메모의 원본 데이터가 memos 배열이기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 보이는 메모 카드는 원본이 아니라, memos 배열을 보고 그린 결과다.&lt;br /&gt;따라서 메모를 수정하려면 화면의 글자만 바꾸는 것이 아니라 memos 배열 안의 해당 메모 데이터를 바꿔야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map()은 배열을 돌면서 새로운 배열을 만드는 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드에서는 다음처럼 동작한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;1. memos 배열을 하나씩 확인한다.
2. 현재 item의 id가 수정하려는 memo.id와 같으면 새 제목/내용을 가진 객체를 반환한다.
3. id가 다르면 기존 item을 그대로 반환한다.
4. 그 결과로 새 배열을 만들고 다시 memos에 넣는다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 배열이 이렇게 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;memos = [
    { id: 1, title: 'A', content: '내용 A' },
    { id: 2, title: 'B', content: '내용 B' },
    { id: 3, title: 'C', content: '내용 C' }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id가 2인 메모를 수정한다면 다음처럼 처리된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;id 1 &amp;rarr; 수정 대상 아님 &amp;rarr; 그대로 반환
id 2 &amp;rarr; 수정 대상 맞음 &amp;rarr; 새 제목/내용으로 바꾼 객체 반환
id 3 &amp;rarr; 수정 대상 아님 &amp;rarr; 그대로 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 배열은 이렇게 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;memos = [
    { id: 1, title: 'A', content: '내용 A' },
    { id: 2, title: '새 제목', content: '새 내용' },
    { id: 3, title: 'C', content: '내용 C' }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 배열을 돌지만 실제로 바뀌는 것은 id가 같은 메모 하나뿐이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 후 저장하고 다시 그리기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에서 메모를 수정한 뒤에는 두 가지 작업이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 수정된 memos 배열을 localStorage에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;saveMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 새로고침 후에도 수정된 내용이 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 화면을 다시 그린다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 화면은 이전 메모 내용을 보여주고 있기 때문에, 수정된 memos 배열을 기준으로 다시 렌더링해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 기본 흐름은 계속 동일하다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;데이터 변경
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가할 때도 같은 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;memos.push(newMemo);
saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제할 때도 같은 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;memos = memos.filter(...);
saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정할 때도 같은 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;memos = memos.map(...);
saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 추가, 삭제, 수정 모두 같은 구조를 가진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;수정 이벤트 전체 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 버튼 이벤트 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;editButton.addEventListener('click', function () {
    const newTitle = prompt(&quot;새 제목을 입력하세요&quot;, memo.title);
    const newContent = prompt(&quot;새 내용을 입력하세요&quot;, memo.content);

    if (newTitle === null || newContent === null) {
        return;
    }

    if (newTitle === '' || newContent === '') {
        alert(&quot;제목과 내용을 모두 입력하세요.&quot;);
        return;
    }

    memos = memos.map(function (item) {
        if (item.id === memo.id) {
            return {
                id: item.id,
                title: newTitle,
                content: newContent
            };
        }

        return item;
    });

    saveMemos();
    renderMemos();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/172</guid>
      <comments>https://woojoo-devlog.tistory.com/172#entry172comment</comments>
      <pubDate>Sat, 13 Jun 2026 15:09:04 +0900</pubDate>
    </item>
    <item>
      <title>#7 localStorage로 메모 저장하기</title>
      <link>https://woojoo-devlog.tistory.com/171</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 새로고침해도 메모가 사라지지 않도록 localStorage를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 메모를 memos 배열로 관리했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가하면 배열에 저장되고, 삭제하면 배열에서 제거되는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 배열은 브라우저 메모리 안에만 존재한다.&lt;br /&gt;그래서 페이지를 새로고침하면 JavaScript가 처음부터 다시 실행되고, memos 배열도 다시 빈 배열이 된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이전 단계에서는 메모 추가와 삭제는 가능했지만 새로고침하면 모든 메모가 사라지는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 이 문제를 해결하기 위해 localStorage를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;localStorage란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localStorage는 브라우저에 데이터를 저장할 수 있는 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말하면 브라우저 안에 있는 작은 저장소라고 볼 수 있다.&lt;br /&gt;그래서 페이지를 새로고침해도 저장된 데이터가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 중요한 특징이 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;localStorage는 문자열만 저장할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메모 하나는 객체 형태다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;const newMemo = {
    id: Date.now(),
    title: title,
    content: content
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메모 목록은 이런 객체들이 들어 있는 배열이다.&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;[
    {
        id: 1,
        title: '첫 번째 메모',
        content: '내용 1'
    },
    {
        id: 2,
        title: '두 번째 메모',
        content: '내용 2'
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 localStorage에는 배열이나 객체를 그대로 저장할 수 없다.&lt;br /&gt;문자열로 바꿔서 저장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장할 때는 JSON.stringify()를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JSON.stringify(memos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다시 꺼낼 때는 문자열을 배열로 복원해야 한다.&lt;br /&gt;이때는 JSON.parse()를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;JSON.parse(savedMemos)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;저장할 때:
배열 &amp;rarr; JSON.stringify &amp;rarr; 문자열 &amp;rarr; localStorage

불러올 때:
localStorage 문자열 &amp;rarr; JSON.parse &amp;rarr; 배열
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 저장 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 memos 배열을 localStorage에 저장하는 함수를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function saveMemos() {
    localStorage.setItem('memos', JSON.stringify(memos));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`localStorage.setItem()`은 데이터를 저장할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;localStorage.setItem('memos', JSON.stringify(memos));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 'memos'는 저장소에서 사용할 이름이다.&lt;br /&gt;즉, 현재 메모 배열을 memos라는 이름으로 저장한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`JSON.stringify(memos)`는 배열을 문자열로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 saveMemos() 함수는 다음 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;현재 memos 배열을 문자열로 변환해서 localStorage에 저장한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;saveMemos는 언제 호출해야 하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;saveMemos()는 memos 배열이 바뀐 직후에 호출해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 memos 배열이 바뀌는 순간은 두 곳이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 메모 추가 후
2. 메모 삭제 후
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가할 때는 새 메모를 배열에 넣는다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;memos.push(newMemo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 직후 저장한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;memos.push(newMemo);
saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 삭제할 때는 filter()로 삭제할 메모를 제외한 새 배열을 만든다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;memos = memos.filter(function (item) {
    return item.id !== memo.id;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때도 배열이 바뀌었으므로 바로 저장해야 한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;memos = memos.filter(function (item) {
    return item.id !== memo.id;
});

saveMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;메모 추가
&amp;rarr; memos 배열 변경
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()

메모 삭제
&amp;rarr; memos 배열 변경
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열만 바꾸고 saveMemos()를 호출하지 않으면 화면에서는 바뀐 것처럼 보이지만, 새로고침했을 때 이전 저장 상태로 돌아갈 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 불러오기 함수 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장만 해서는 부족하다.&lt;br /&gt;페이지가 다시 열렸을 때 저장된 메모를 불러와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 loadMemos() 함수를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function loadMemos() {
    const savedMemos = localStorage.getItem('memos');

    if (savedMemos !== null) {
        memos = JSON.parse(savedMemos);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 localStorage에서 memos라는 이름으로 저장된 값을 꺼낸다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const savedMemos = localStorage.getItem('memos');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 저장된 값이 없다면 null이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조건문으로 저장된 값이 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (savedMemos !== null) {
    memos = JSON.parse(savedMemos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 값이 있을 때만 `JSON.parse()`를 실행한다.&lt;br /&gt;JSON.parse(savedMemos)는 문자열로 저장된 메모 데이터를 다시 배열로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 배열을 다시 memos에 넣는다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memos = JSON.parse(savedMemos);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 loadMemos()의 역할은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;localStorage에서 저장된 메모를 꺼낸다.
저장된 메모가 있으면 배열로 복원한다.
복원한 배열을 memos에 넣는다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 저장된 데이터가 없을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 페이지를 방문하면 localStorage에는 아직 저장된 메모가 없을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 경우 다음 코드의 결과는 null이다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const savedMemos = localStorage.getItem('memos');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 조건문이 실행되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (savedMemos !== null) {
    memos = JSON.parse(savedMemos);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 memos는 처음 선언한 빈 배열 그대로 유지된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 renderMemos()가 실행되면 빈 배열을 기준으로 화면을 그린다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;메모 없음
&amp;rarr; 메모 카드 생성 안 함
&amp;rarr; &quot;아직 작성된 메모가 없습니다.&quot; 표시
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음 방문했을 때도 오류 없이 빈 메모장 화면이 나온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;loadMemos 호출 위치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loadMemos()는 페이지가 처음 열릴 때 한 번만 실행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 맨 아래에서 다음 순서로 호출했다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;loadMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 localStorage에서 저장된 메모를 불러와 memos 배열에 넣는다.&lt;br /&gt;그다음 renderMemos()로 화면을 그린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 renderMemos()를 먼저 실행하면 아직 저장된 데이터가 memos 배열에 들어오기 전이므로, 화면에 저장된 메모가 보이지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 순서는 다음처럼 잡았다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. loadMemos()
2. renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 코드 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 script.js의 핵심 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let memos = [];

function saveMemos() {
    localStorage.setItem('memos', JSON.stringify(memos));
}

function loadMemos() {
    const savedMemos = localStorage.getItem('memos');

    if (savedMemos !== null) {
        memos = JSON.parse(savedMemos);
    }
}

function renderMemos() {
    // memos 배열을 기준으로 화면 다시 그리기
}

addMemoButton.addEventListener('click', function () {
    // 입력값 가져오기
    // 새 메모 객체 만들기

    memos.push(newMemo);
    saveMemos();
    renderMemos();

    // 입력창 비우기
});

loadMemos();
renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지가 처음 열릴 때&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;loadMemos()
&amp;rarr; localStorage에서 저장된 메모를 가져온다.
&amp;rarr; JSON.parse로 배열로 변환한다.
&amp;rarr; memos 배열에 넣는다.

renderMemos()
&amp;rarr; memos 배열을 기준으로 화면을 그린다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모를 추가할 때&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;사용자가 제목과 내용 입력
&amp;rarr; 메모 추가 버튼 클릭
&amp;rarr; newMemo 객체 생성
&amp;rarr; memos 배열에 추가
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&amp;rarr; 입력창 비우기
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모를 삭제할 때&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;삭제 버튼 클릭
&amp;rarr; memos 배열에서 해당 메모 제거
&amp;rarr; saveMemos()
&amp;rarr; renderMemos()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메모 추가와 삭제가 단순히 화면에만 반영되는 것이 아니라, localStorage에도 같이 반영된다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/171</guid>
      <comments>https://woojoo-devlog.tistory.com/171#entry171comment</comments>
      <pubDate>Sat, 13 Jun 2026 12:46:51 +0900</pubDate>
    </item>
    <item>
      <title>#6 메모 내용 줄바꿈 처리하기</title>
      <link>https://woojoo-devlog.tistory.com/170</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 메모 내용을 입력하면서 생긴 작은 문제를 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 textarea에서는 줄바꿈이 잘 입력되는데, 메모 목록에 출력하면 줄바꿈이 사라지는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모 내용을 이렇게 입력했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;첫 번째 줄
두 번째 줄
세 번째 줄
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모 카드에서는 다음처럼 한 줄로 보였다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;첫 번째 줄 두 번째 줄 세 번째 줄
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 JavaScript에서 입력값을 잘못 가져오는 문제라고 생각했다.&lt;br /&gt;하지만 확인해보니 입력값 자체가 사라진 것은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 메모 내용을 출력하는 HTML 태그의 기본 동작에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메모 내용은 &amp;lt;p&amp;gt; 태그 안에 출력하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;textarea에서는 엔터를 누르면 줄바꿈이 그대로 보인다.&lt;br /&gt;하지만 그 값을 &amp;lt;p&amp;gt; 태그 안에 넣으면 브라우저는 줄바꿈을 그대로 보여주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HTML에 이렇게 작성해도,&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;
첫 번째 줄
두 번째 줄
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서는 보통 다음처럼 보인다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;첫 번째 줄 두 번째 줄
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 HTML 텍스트 요소는 여러 공백과 줄바꿈을 하나의 공백처럼 처리하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이번 문제는 다음과 같이 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;textarea에서 줄바꿈이 사라진 것이 아니다.
JavaScript가 값을 잘못 가져온 것도 아니다.
p 태그가 기본적으로 줄바꿈을 그대로 보여주지 않는 것이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &amp;lt;p&amp;gt; 태그를 그대로 사용하고, CSS에서 줄바꿈을 유지하도록 처리했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-card p {
    white-space: pre-wrap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;white-space: pre-wrap;을 사용하면 사용자가 입력한 줄바꿈을 화면에 반영할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재 구조는 그대로 유지했다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 CSS에 다음 코드를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-card p {
    white-space: pre-wrap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수정하니 textarea에서 입력한 줄바꿈이 메모 카드에서도 그대로 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pre 태그와의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방법으로는 &amp;lt;pre&amp;gt; 태그를 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;pre&amp;gt;${memo.content}&amp;lt;/pre&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pre 태그는 기본적으로 줄바꿈과 공백을 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 pre 태그는 일반 문단이라기보다는 코드나 서식이 있는 텍스트를 보여주는 느낌이 강하다.&lt;br /&gt;메모장 본문에는 일반 문단처럼 보이는 &amp;lt;p&amp;gt; 태그를 유지하고, CSS로 줄바꿈만 처리하는 방식이 더 자연스럽다고 판단했다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/170</guid>
      <comments>https://woojoo-devlog.tistory.com/170#entry170comment</comments>
      <pubDate>Thu, 11 Jun 2026 23:36:50 +0900</pubDate>
    </item>
    <item>
      <title>#5 메모데이터를 배열로 관리</title>
      <link>https://woojoo-devlog.tistory.com/169</link>
      <description>&lt;h1&gt;메모 데이터를 배열로 관리하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 메모를 추가하면 바로 HTML 요소를 만들고 화면에 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 단순했다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;입력값 &amp;rarr; HTML 요소 생성 &amp;rarr; 화면에 바로 추가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방식은 메모 데이터가 따로 관리되지 않는다는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 메모가 보이기는 하지만, JavaScript 입장에서는 &amp;ldquo;현재 어떤 메모들이 있는지&amp;rdquo;를 명확하게 관리하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 단계에서는 메모 데이터를 배열로 관리하도록 구조를 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력값 &amp;rarr; 메모 객체 생성 &amp;rarr; memos 배열에 저장 &amp;rarr; 배열을 기준으로 화면 다시 그리기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이제 메모의 원본은 화면이 아니라 memos 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 배열로 관리해야 하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 메모를 추가할 때 바로 화면에 붙이는 방식이 더 단순해 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 삭제, 수정, 저장 기능으로 넘어가려면 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 질문이 생긴다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;삭제할 때 어떤 메모를 지울 것인가?
수정할 때 어떤 메모를 수정할 것인가?
localStorage에는 무엇을 저장할 것인가?
나중에 서버나 DB에는 어떤 데이터를 보낼 것인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML은 화면에 보여주기 위한 결과물이다.&lt;br /&gt;데이터 자체를 관리하기에는 적절하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메모를 먼저 JavaScript 데이터로 관리하기로 했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 배열 안에 메모 객체들이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모가 2개라면 데이터는 이런 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;[
    {
        id: 1,
        title: '첫 번째 메모',
        content: '내용 1'
    },
    {
        id: 2,
        title: '두 번째 메모',
        content: '내용 2'
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 화면은 이 배열을 기준으로 그려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 작성한 script.js는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const titleInput = document.getElementById(&quot;memo-title&quot;);
const contentInput = document.getElementById(&quot;memo-content&quot;);
const addMemoButton = document.getElementById(&quot;add-memo-button&quot;);
const memoContainer = document.getElementById(&quot;memo-container&quot;);
const emptyMessage = document.getElementById(&quot;empty-message&quot;);

let memos = [];

function updateEmptyMessage() {
    if (memos.length === 0) {
        emptyMessage.style.display = 'block';
    } else {
        emptyMessage.style.display = 'none';
    }
}

function renderMemos() {
    memoContainer.innerHTML = '';

    memos.forEach(function (memo) {
        const memoCard = document.createElement('div');
        memoCard.className = 'memo-card';

        memoCard.innerHTML = `
            &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
            &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
        `;

        const deleteButton = memoCard.querySelector('button');

        deleteButton.addEventListener('click', function () {
            memos = memos.filter(function (item) {
                return item.id !== memo.id;
            });

            renderMemos();
        });

        memoContainer.appendChild(memoCard);
    });

    updateEmptyMessage();
}

addMemoButton.addEventListener('click', function () {
    const title = titleInput.value.trim();
    const content = contentInput.value.trim();

    if (title === '' || content === '') {
        alert('제목과 내용을 모두 입력하세요.');
        return;
    }

    const newMemo = {
        id: Date.now(),
        title: title,
        content: content
    };

    memos.push(newMemo);

    renderMemos();

    titleInput.value = '';
    contentInput.value = '';
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;renderMemos 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 가장 중요한 함수는 renderMemos()다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;function renderMemos() {
    memoContainer.innerHTML = '';

    memos.forEach(function (memo) {
        ...
    });

    updateEmptyMessage();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 renderMemos()를 단순히 화면에 메모를 보여주는 함수라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 더 정확히는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;renderMemos()
= 현재 memos 배열 상태와 화면을 똑같이 맞추는 함수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에 메모가 2개 있으면 화면에도 2개가 보여야 한다.&lt;br /&gt;배열이 비어 있으면 화면에서도 메모가 보이면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 화면은 memos 배열을 보고 다시 만들어지는 결과물이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 화면을 먼저 비우는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;renderMemos() 안에는 다음 코드가 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memoContainer.innerHTML = '';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 코드가 메모를 전부 삭제하는 것처럼 보여서 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 코드는 데이터를 삭제하는 코드가 아니다.&lt;br /&gt;memos 배열은 그대로 두고, 화면에 이미 그려져 있던 메모 카드만 지운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 renderMemos()가 기존 화면에 계속 추가하는 함수가 아니라, 현재 배열 상태를 기준으로 화면을 다시 만드는 함수이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배열에 A, B가 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memos = [A, B];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 화면에 A가 그려져 있는 상태에서 화면을 비우지 않고 A, B를 다시 붙이면 화면은 이렇게 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A A B
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 중복된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 화면을 비운다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memoContainer.innerHTML = '';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 현재 memos 배열을 기준으로 다시 그린다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;A B
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제할 때도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에는 A, B, C가 있고, 배열에서 B를 삭제해 memos = [A, C]가 되었다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 비우지 않고 다시 그리면 기존 화면 위에 A, C가 추가될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A B C A C
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 renderMemos()는 항상 다음 순서로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 기존 화면을 비운다.
2. 현재 memos 배열에 있는 메모만 다시 그린다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forEach로 배열을 하나씩 그리기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 비운 뒤에는 memos 배열을 순회한다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;memos.forEach(function (memo) {
    ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach는 배열 안의 요소를 하나씩 꺼내서 처리하는 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 배열이 이렇게 있다면,&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;memos = [
    { id: 1, title: 'A', content: '내용 A' },
    { id: 2, title: 'B', content: '내용 B' }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 반복에서는 memo에 첫 번째 객체가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;{ id: 1, title: 'A', content: '내용 A' }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 반복에서는 memo에 두 번째 객체가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;{ id: 2, title: 'B', content: '내용 B' }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, memo는 배열 전체가 아니라 현재 처리 중인 메모 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메모마다 새 메모 카드를 만들고, 화면에 붙인다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const memoCard = document.createElement('div');
memoCard.className = 'memo-card';

memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${memo.title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${memo.content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;

memoContainer.appendChild(memoCard);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 기능 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 삭제 버튼을 누르면 화면의 메모 카드를 직접 제거하는 방식으로 생각했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;memoCard.remove();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제 메모의 원본은 화면이 아니라 memos 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 삭제도 화면에서 먼저 하는 것이 아니라, 배열에서 먼저 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;deleteButton.addEventListener('click', function () {
    memos = memos.filter(function (item) {
        return item.id !== memo.id;
    });

    renderMemos();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 현재 삭제하려는 메모와 id가 다른 메모만 남기는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 삭제할 메모를 제외한 새 배열을 만들고, 그 배열로 memos를 다시 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 배열이 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;memos = [
    { id: 1, title: 'A' },
    { id: 2, title: 'B' },
    { id: 3, title: 'C' }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B의 삭제 버튼을 눌렀다면 삭제하려는 id는 2다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;return item.id !== 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 메모를 검사하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;id 1 !== 2 &amp;rarr; true  &amp;rarr; 남김
id 2 !== 2 &amp;rarr; false &amp;rarr; 제외
id 3 !== 2 &amp;rarr; true  &amp;rarr; 남김
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 새 배열은 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;[
    { id: 1, title: 'A' },
    { id: 3, title: 'C' }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 새 배열을 다시 memos에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memos = memos.filter(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 바꾼 뒤에는 화면도 다시 맞춰야 한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;renderMemos();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;renderMemos()가 실행되면 기존 화면을 비우고, 현재 memos 배열에 남아 있는 메모만 다시 그린다.&lt;br /&gt;그래서 화면에서도 삭제된 메모가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈 메시지도 배열 기준으로 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 화면에 있는 메모 카드 개수를 기준으로 빈 메시지를 관리할 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제 메모의 원본은 memos 배열이다.&lt;br /&gt;그래서 빈 메시지도 배열 기준으로 판단하는 것이 더 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function updateEmptyMessage() {
    if (memos.length === 0) {
        emptyMessage.style.display = 'block';
    } else {
        emptyMessage.style.display = 'none';
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memos.length가 0이면 메모가 없는 상태다.&lt;br /&gt;그래서 &amp;ldquo;아직 작성된 메모가 없습니다.&amp;rdquo; 문구를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 memos.length가 1 이상이면 메모가 있는 상태이므로 빈 메시지를 숨긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 추가 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 추가 버튼을 누르면 다음 흐름으로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    const title = titleInput.value.trim();
    const content = contentInput.value.trim();

    if (title === '' || content === '') {
        alert('제목과 내용을 모두 입력하세요.');
        return;
    }

    const newMemo = {
        id: Date.now(),
        title: title,
        content: content
    };

    memos.push(newMemo);

    renderMemos();

    titleInput.value = '';
    contentInput.value = '';
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 제목과 내용 입력값을 가져온다.
2. 앞뒤 공백을 제거한다.
3. 빈 값이면 alert를 띄우고 중단한다.
4. 새 메모 객체를 만든다.
5. memos 배열에 새 메모를 추가한다.
6. renderMemos()로 화면을 다시 그린다.
7. 입력창을 비운다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Date.now()는 메모마다 고유한 id를 만들기 위해 사용했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;id: Date.now()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시간을 숫자로 반환하기 때문에 간단한 고유값으로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 서버와 DB를 사용하면 이 id는 데이터베이스에서 생성하는 값으로 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 가장 중요한 변화는 메모의 기준이 바뀐 것이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;이전: 화면에 추가된 HTML이 기준
현재: memos 배열이 기준
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 화면은 데이터를 직접 관리하는 곳이 아니라, 배열을 보여주는 결과물에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 정리한 내용은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모를 객체로 만들기&lt;/li&gt;
&lt;li&gt;메모 객체를 배열에 저장하기&lt;/li&gt;
&lt;li&gt;배열을 기준으로 화면 다시 그리기&lt;/li&gt;
&lt;li&gt;renderMemos() 함수로 화면 렌더링 분리하기&lt;/li&gt;
&lt;li&gt;forEach()로 배열 순회하기&lt;/li&gt;
&lt;li&gt;filter()로 특정 메모 삭제하기&lt;/li&gt;
&lt;li&gt;삭제 후 다시 렌더링하기&lt;/li&gt;
&lt;li&gt;빈 메시지도 배열 기준으로 관리하기&lt;/li&gt;
&lt;li&gt;Date.now()로 임시 id 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 단계의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 메모를 배열로 관리하지만, 아직 브라우저 메모리에만 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 새로고침하면 memos 배열이 다시 빈 배열로 초기화된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let memos = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 화면에 추가한 메모는 새로고침 후 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 부족한 점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로고침하면 메모가 사라진다.&lt;/li&gt;
&lt;li&gt;localStorage 저장 기능이 없다.&lt;/li&gt;
&lt;li&gt;수정 기능이 없다.&lt;/li&gt;
&lt;li&gt;삭제할 때 확인 메시지가 없다.&lt;/li&gt;
&lt;li&gt;메모 데이터가 서버나 DB에 저장되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 localStorage를 사용해서 메모 데이터를 브라우저에 저장할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 메모를 배열에만 저장하고 있기 때문에 새로고침하면 사라진다.&lt;br /&gt;다음에는 memos 배열을 localStorage에 저장하고, 페이지가 다시 열릴 때 저장된 메모를 불러오는 방식으로 바꿔볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 데이터의 기준을 배열로 옮겼기 때문에, 다음 단계에서 localStorage 저장으로 넘어가기가 훨씬 쉬워졌다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/169</guid>
      <comments>https://woojoo-devlog.tistory.com/169#entry169comment</comments>
      <pubDate>Thu, 11 Jun 2026 23:20:44 +0900</pubDate>
    </item>
    <item>
      <title>#4 입력한 메모를 화면에 추가하기</title>
      <link>https://woojoo-devlog.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서는 JavaScript 파일을 연결하고, 메모 추가 버튼을 눌렀을 때 입력값이 콘솔에 출력되는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 콘솔 출력에서 끝내지 않고, 사용자가 입력한 제목과 내용을 실제 화면의 메모 목록에 추가하도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 localStorage 저장은 하지 않았다.&lt;br /&gt;따라서 새로고침하면 추가한 메모는 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 목표는 &lt;b&gt;브라우저 화면 안에서 메모를 동적으로 추가하는 흐름을 이해하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 작성했던 코드의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 입력값을 변수에 미리 담아두고, 그 값을 이용해서 메모를 추가하려고 했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const titleInput = document.getElementById(&quot;memo-title&quot;);
const contentInput = document.getElementById(&quot;memo-content&quot;);
const addMemoButton = document.getElementById(&quot;add-memo-button&quot;);
const memoContainer = document.getElementById(&quot;memo-container&quot;);
const emptyMessage = document.getElementById(&quot;empty-message&quot;);

const title = titleInput.value;
const content = contentInput.value;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게 작성하면 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`titleInput.value`와 `contentInput.value`는 페이지가 처음 로드될 때 실행된다.&lt;br /&gt;페이지가 처음 열렸을 때는 사용자가 아직 아무것도 입력하지 않은 상태이므로, title과 content에는 빈 문자열이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 입력값은 페이지가 열릴 때 가져오는 것이 아니라 &lt;b&gt;버튼을 클릭한 순간&lt;/b&gt; 가져와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 흐름은 이렇게 되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;페이지 열림
&amp;rarr; 사용자가 제목과 내용 입력
&amp;rarr; 메모 추가 버튼 클릭
&amp;rarr; 그 순간 input과 textarea의 현재 값 읽기
&amp;rarr; 메모 카드 생성
&amp;rarr; 화면에 추가
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;return 위치 오류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 빈 값 검사를 이벤트 함수 밖에 작성했다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;if (title === '' || content === '') {
    alert('제목과 내용을 모두 입력하세요.');
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하니 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;'return' outside function definition
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return은 함수 안에서만 사용할 수 있다.&lt;br /&gt;따라서 빈 값 검사도 버튼 클릭 이벤트 함수 안으로 넣어야 했다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    if (title === '' || content === '') {
        alert('제목과 내용을 모두 입력하세요.');
        return;
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 return은 제목이나 내용이 비어 있을 때, 그 아래 메모 추가 코드를 실행하지 않고 함수를 끝내는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 생성 코드 위치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 만드는 코드도 처음에는 클릭 이벤트 밖에 작성하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모는 페이지가 열릴 때 만들어지는 것이 아니라, 사용자가 버튼을 클릭했을 때 만들어져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음 작업들은 모두 클릭 이벤트 함수 안에 있어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력값 가져오기&lt;/li&gt;
&lt;li&gt;빈 값 검사하기&lt;/li&gt;
&lt;li&gt;새 div 만들기&lt;/li&gt;
&lt;li&gt;메모 카드 내용 넣기&lt;/li&gt;
&lt;li&gt;화면에 추가하기&lt;/li&gt;
&lt;li&gt;입력창 비우기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 정리하면서, 이벤트 기반 코드의 흐름을 조금 더 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문자열 안에 변수 넣기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 카드의 HTML을 만들 때도 한 가지 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이렇게 작성했다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;memoCard.innerHTML = '' +
    '&amp;lt;h3&amp;gt;${title}&amp;lt;/h3&amp;gt;' +
    '&amp;lt;p&amp;gt;${content}&amp;lt;/p&amp;gt;' +
    '&amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 작은따옴표 안에서는 ${title}과 ${content}가 변수로 해석되지 않는다.&lt;br /&gt;그냥 글자 그대로 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 값을 문자열 안에 넣으려면 백틱을 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 템플릿 리터럴이라고 한다.&lt;br /&gt;문자열 안에 변수 값을 넣을 때 훨씬 편하게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 정리한 script.js 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const titleInput = document.getElementById(&quot;memo-title&quot;);
const contentInput = document.getElementById(&quot;memo-content&quot;);
const addMemoButton = document.getElementById(&quot;add-memo-button&quot;);
const memoContainer = document.getElementById(&quot;memo-container&quot;);
const emptyMessage = document.getElementById(&quot;empty-message&quot;);

addMemoButton.addEventListener('click', function () {
    const title = titleInput.value;
    const content = contentInput.value;

    console.log('메모 추가 버튼 클릭');
    console.log(title);
    console.log(content);

    if (title === '' || content === '') {
        alert('제목과 내용을 모두 입력하세요.');
        return;
    }

    const memoCard = document.createElement('div');
    memoCard.className = 'memo-card';

    memoCard.innerHTML = `
        &amp;lt;h3&amp;gt;${title}&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;${content}&amp;lt;/p&amp;gt;
        &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
    `;

    memoContainer.appendChild(memoCard);

    emptyMessage.style.display = 'none';

    titleInput.value = '';
    contentInput.value = '';
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;1. HTML 요소들을 JavaScript로 가져온다.
2. 메모 추가 버튼에 click 이벤트를 등록한다.
3. 사용자가 버튼을 클릭한다.
4. 클릭한 순간 제목과 내용 값을 가져온다.
5. 제목이나 내용이 비어 있으면 alert를 띄우고 함수를 종료한다.
6. 값이 있으면 새 div 요소를 만든다.
7. 새 div에 memo-card 클래스를 붙인다.
8. innerHTML로 메모 카드 내부 구조를 만든다.
9. memoContainer 안에 새 메모 카드를 추가한다.
10. 메모가 생겼으므로 emptyMessage를 숨긴다.
11. 입력창을 빈 값으로 초기화한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새 요소 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 처음으로 JavaScript를 이용해 HTML 요소를 직접 만들었다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const memoCard = document.createElement('div');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 새로운 div 요소를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 HTML 파일에 적혀 있던 요소가 아니라, JavaScript가 실행 중에 새로 만드는 요소다.&lt;br /&gt;사용자가 메모를 추가할 때마다 새로운 메모 카드가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스 붙이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 만든 div에는 memo-card 클래스를 붙였다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;memoCard.className = 'memo-card';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 다음과 같은 요소가 만들어진다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;memo-card&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 클래스를 붙이면 나중에 CSS에서 .memo-card를 선택해서 메모 카드 스타일을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 카드 내용 넣기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 카드 내부에는 innerHTML을 사용해서 제목, 내용, 삭제 버튼을 넣었다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;memoCard.innerHTML = `
    &amp;lt;h3&amp;gt;${title}&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;${content}&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 화면에는 다음과 같은 구조가 추가된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;memo-card&quot;&amp;gt;
    &amp;lt;h3&amp;gt;사용자가 입력한 제목&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;사용자가 입력한 내용&amp;lt;/p&amp;gt;
    &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면에 추가하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 만든 메모 카드는 memoContainer 안에 추가했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;memoContainer.appendChild(memoCard);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 HTML에는 비어 있는 메모 목록 영역이 있었다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div id=&quot;memo-container&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가하면 이 영역 안에 새 메모 카드가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div id=&quot;memo-container&quot;&amp;gt;
    &amp;lt;div class=&quot;memo-card&quot;&amp;gt;
        &amp;lt;h3&amp;gt;제목&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;내용&amp;lt;/p&amp;gt;
        &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈 메시지 숨기기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 화면에는 메모가 없기 때문에 안내 문구가 보인다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;p id=&quot;empty-message&quot; class=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모가 하나라도 추가되면 이 문구는 필요하지 않다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;emptyMessage.style.display = 'none';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 emptyMessage 요소의 display 값을 none으로 바꿔서 화면에서 숨긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력창 비우기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모를 추가한 뒤에는 입력창을 다시 비워주었다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;titleInput.value = '';
contentInput.value = '';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 메모를 추가한 뒤 바로 다음 메모를 입력할 수 있는 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/168</guid>
      <comments>https://woojoo-devlog.tistory.com/168#entry168comment</comments>
      <pubDate>Wed, 10 Jun 2026 20:18:25 +0900</pubDate>
    </item>
    <item>
      <title>#3 JavaScript 연결하고 버튼 클릭 감지</title>
      <link>https://woojoo-devlog.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML과 CSS로 정적인 메모장 화면을 만든 뒤, 이제 JavaScript를 연결하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 아직 메모를 실제로 추가하지는 않았다.&lt;br /&gt;먼저 브라우저에서 사용자가 입력한 값을 JavaScript로 가져올 수 있는지, 그리고 버튼 클릭을 감지할 수 있는지를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 메모 추가 기능을 만들기보다, 먼저 JavaScript가 HTML 요소를 제대로 다룰 수 있는지 확인하는 것을 목표로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 파일 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Memo Evolution/
  index.html
  style.css
  script.js
  .gitignore
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 script.js 파일을 새로 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JavaScript에서 사용할 요소 정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript로 기능을 만들려면 먼저 어떤 HTML 요소를 다룰지 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 메모장에서는 다음 요소들이 필요했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제목 입력창&lt;/li&gt;
&lt;li&gt;내용 입력창&lt;/li&gt;
&lt;li&gt;메모 추가 버튼&lt;/li&gt;
&lt;li&gt;메모가 들어갈 영역&lt;/li&gt;
&lt;li&gt;빈 목록 안내 메시지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 HTML 요소에 id를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; placeholder=&quot;제목을 입력하세요&quot;&amp;gt;

&amp;lt;textarea id=&quot;memo-content&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;&amp;lt;/textarea&amp;gt;

&amp;lt;button id=&quot;add-memo-button&quot; type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;

&amp;lt;p id=&quot;empty-message&quot; class=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;

&amp;lt;div id=&quot;memo-container&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS에서는 주로 class를 사용해서 여러 요소에 같은 스타일을 적용했다.&lt;br /&gt;반면 JavaScript에서는 특정 요소 하나를 정확히 가져와야 할 때 id를 사용하면 이해하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 제목 입력창은 다음처럼 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const titleInput = document.getElementById('memo-title');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 문서에서 id가 memo-title인 요소를 찾아서 titleInput이라는 변수에 담는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JavaScript 파일 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script.js 파일을 만든 뒤, index.html에 연결했다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;script.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 태그는 body가 끝나기 직전에 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 요소들이 먼저 만들어진 뒤 JavaScript가 실행되도록 하기 위해서다.&lt;br /&gt;만약 JavaScript가 너무 먼저 실행되면, 아직 만들어지지 않은 HTML 요소를 찾으려고 해서 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML 요소 가져오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script.js에서는 먼저 JavaScript에서 사용할 요소들을 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;const titleInput = document.getElementById('memo-title');
const contentInput = document.getElementById('memo-content');
const addMemoButton = document.getElementById('add-memo-button');
const memoContainer = document.getElementById('memo-container');
const emptyMessage = document.getElementById('empty-message');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 모든 HTML 요소를 다 가져올 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서 실제로 사용할 요소만 가져오면 된다.&lt;br /&gt;이번에는 값을 읽거나, 클릭 이벤트를 붙이거나, 나중에 화면 내용을 바꿀 요소만 변수에 담았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버튼 클릭 감지하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 가져온 뒤에는 클릭 이벤트를 등록했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;addMemoButton.addEventListener('click', function () {
    console.log('메모 추가 버튼 클릭');
    console.log(titleInput.value);
    console.log(contentInput.value);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addEventListener()는 특정 요소에서 이벤트가 발생했을 때 실행할 함수를 등록하는 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드는 다음처럼 해석할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;메모 추가 버튼을 클릭하면
콘솔에 &quot;메모 추가 버튼 클릭&quot;을 출력하고
제목 입력창의 현재 값과
내용 입력창의 현재 값을 출력한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 인자인 'click'은 브라우저가 정해둔 이벤트 이름이다.&lt;br /&gt;두 번째 인자로 전달한 함수는 클릭이 발생했을 때 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 함수를 콜백 함수라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력값 가져오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력창에 적힌 값은 .value로 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;console.log(titleInput.value);
console.log(contentInput.value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;titleInput은 제목 입력창 요소이고, contentInput은 내용 입력창 요소다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 값은 각각 다음 위치에 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;titleInput.value
contentInput.value
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, input이나 textarea의 현재 입력값은 .value로 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 index.html을 열고 개발자 도구의 Console을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목과 내용을 입력한 뒤 메모 추가 버튼을 누르면 다음 내용이 출력된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;메모 추가 버튼 클릭
입력한 제목
입력한 내용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 다음을 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;script.js가 HTML에 정상적으로 연결됐다.&lt;/li&gt;
&lt;li&gt;버튼 클릭 이벤트가 정상적으로 동작한다.&lt;/li&gt;
&lt;li&gt;JavaScript에서 input 값을 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;JavaScript에서 textarea 값을 읽을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 화면에 메모가 추가되지는 않는다.&lt;br /&gt;하지만 메모 추가 기능을 만들기 위한 기본 연결은 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/167</guid>
      <comments>https://woojoo-devlog.tistory.com/167#entry167comment</comments>
      <pubDate>Wed, 10 Jun 2026 14:00:09 +0900</pubDate>
    </item>
    <item>
      <title>#2 CSS로 메모장 화면 꾸미기</title>
      <link>https://woojoo-devlog.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계에서는 HTML로 메모장 화면의 기본 구조를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 style.css를 작성해서 화면이 조금 더 메모장처럼 보이도록 스타일을 적용했다.&lt;br /&gt;아직 JavaScript 기능은 없기 때문에 메모 추가 버튼을 눌러도 실제로 메모가 추가되지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계의 목표는 기능 구현이 아니라, HTML로 만든 요소들을 보기 좋게 배치하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 파일 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Memo Evolution/
  index.html
  style.css
  .gitignore
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSS 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 작성한 CSS는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;body {
    margin: 0;
    font-family: sans-serif;
    background-color: #f4f4f4;
}

.app {
    width: 500px;
    margin: 40px auto;
    padding: 24px;
    background-color: white;
}

.memo-form {
    margin-bottom: 32px;
}

.memo-form label {
    display: block;
    margin-top: 12px;
    margin-bottom: 6px;
}

.memo-form input,
.memo-form textarea {
    display: block;
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
}

.memo-form textarea {
    min-height: 120px;
    resize: vertical;
}

.memo-form button {
    display: block;
    width: 100%;
    margin-top: 16px;
}

button {
    margin-top: 12px;
    padding: 10px 16px;
    cursor: pointer;
}

.empty-message {
    color: #777;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 전체 스타일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 body에 기본 스타일을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;body {
    margin: 0;
    font-family: sans-serif;
    background-color: #f4f4f4;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 기본적으로 body에 약간의 여백을 가지고 있다.&lt;br /&gt;그래서 margin: 0;으로 기본 여백을 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글꼴은 sans-serif로 지정했고, 전체 배경은 연한 회색으로 설정했다.&lt;br /&gt;배경을 회색으로 두면 가운데에 있는 흰색 메모장 영역이 더 잘 구분된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모장 영역 가운데 배치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 메모장 화면은 .app 영역이 감싸고 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.app {
    width: 500px;
    margin: 40px auto;
    padding: 24px;
    background-color: white;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서는 .app에 너비를 주고, 화면 가운데에 배치했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;width: 500px;로 메모장 박스의 크기를 정했고, margin: 40px auto;로 위아래 여백을 주면서 좌우 가운데 정렬이 되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;padding: 24px;은 박스 안쪽 여백이다.&lt;br /&gt;이 값을 주지 않으면 입력창과 제목이 박스 가장자리에 붙어 보여서 답답하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 영역 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML만 작성했을 때는 label, input, textarea, button이 화면에서 정돈되어 보이지 않았다.&lt;br /&gt;그래서 메모 작성 영역 안의 요소들을 위에서 아래로 자연스럽게 배치했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-form label {
    display: block;
    margin-top: 12px;
    margin-bottom: 6px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;label을 block으로 바꿔서 입력창 위에 따로 표시되도록 했다.&lt;br /&gt;이렇게 하면 &amp;ldquo;제목&amp;rdquo;과 제목 입력창, &amp;ldquo;내용&amp;rdquo;과 내용 입력창의 관계가 더 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-form input,
.memo-form textarea {
    display: block;
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input과 textarea는 같은 너비로 맞췄다.&lt;br /&gt;width: 100%;를 사용해서 부모 영역의 가로 폭을 모두 사용하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 box-sizing: border-box;를 같이 사용했다.&lt;br /&gt;처음에는 width: 100%와 padding을 같이 쓰면 입력창이 부모 영역보다 커질 수 있다는 점이 헷갈렸다.&lt;br /&gt;box-sizing: border-box;를 사용하면 padding까지 포함해서 전체 너비를 계산하므로 입력창이 밖으로 삐져나가지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내용 입력창 크기 조정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 내용은 제목보다 길게 작성될 수 있으므로 textarea에는 최소 높이를 지정했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-form textarea {
    min-height: 120px;
    resize: vertical;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min-height: 120px;로 내용 입력창이 너무 작아 보이지 않게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 resize: vertical;을 사용해서 사용자가 세로 방향으로만 크기를 조절할 수 있게 했다.&lt;br /&gt;가로 방향까지 조절할 수 있으면 전체 레이아웃이 어색해질 수 있기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버튼 정렬&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 화면을 확인했을 때 메모 추가 버튼이 입력창과 따로 노는 느낌이 있었다.&lt;br /&gt;그래서 버튼도 입력창과 같은 폭으로 맞췄다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.memo-form button {
    display: block;
    width: 100%;
    margin-top: 16px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 제목 입력창, 내용 입력창, 메모 추가 버튼이 같은 폭으로 정렬되어 훨씬 안정적으로 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼에는 공통 스타일도 추가했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;button {
    margin-top: 12px;
    padding: 10px 16px;
    cursor: pointer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;padding을 넣어 버튼 안쪽 여백을 확보했고, cursor: pointer;로 버튼 위에 마우스를 올렸을 때 클릭 가능한 요소처럼 보이게 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈 메모 메시지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 JavaScript 기능이 없기 때문에 메모 목록에는 실제 데이터가 표시되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p class=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문구는 안내 메시지에 가까우므로 일반 텍스트보다 조금 흐리게 보이도록 했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.empty-message {
    color: #777;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IHevB/dJMcahktdQO/1k1i1w1lRal5kCYD9sSevk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IHevB/dJMcahktdQO/1k1i1w1lRal5kCYD9sSevk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IHevB/dJMcahktdQO/1k1i1w1lRal5kCYD9sSevk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIHevB%2FdJMcahktdQO%2F1k1i1w1lRal5kCYD9sSevk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1354&quot; height=&quot;1096&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML만 작성했을 때는 요소들이 단순히 위에서 아래로 나열된 느낌이었다.&lt;br /&gt;CSS를 적용하니 같은 HTML 구조라도 훨씬 화면답게 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 입력창과 버튼의 너비를 맞추는 것만으로도 화면이 훨씬 정리된 느낌이 들었다.&lt;br /&gt;아직 기능은 없지만, 사용자가 실제로 메모를 작성할 수 있는 화면에 가까워졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 정적인 화면이었다면, 다음 단계부터는 사용자의 동작에 따라 화면이 바뀌는 구조를 만들어볼 예정이다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/166</guid>
      <comments>https://woojoo-devlog.tistory.com/166#entry166comment</comments>
      <pubDate>Wed, 10 Jun 2026 08:37:16 +0900</pubDate>
    </item>
    <item>
      <title>#1 HTML로 메모장 화면 뼈대 만들기</title>
      <link>https://woojoo-devlog.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 파일 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 구조는 단순하게 구성했다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Memo Evolution/
  index.html
  style.css
  .gitignore
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 IntelliJ에서 Java 프로젝트를 만들면서 src, .idea, .iml 같은 파일들이 생성됐다.&lt;br /&gt;하지만 현재 단계는 HTML/CSS 학습이 목적이므로, 불필요한 Java/IDE 설정 파일은 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작성한 `index.html`은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;Memo Evolution&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;app&quot;&amp;gt;

    &amp;lt;div class=&quot;memo-form&quot;&amp;gt;
        &amp;lt;h2&amp;gt;새 메모 작성&amp;lt;/h2&amp;gt;

        &amp;lt;label for=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
        &amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; placeholder=&quot;제목을 입력하세요&quot;&amp;gt;

        &amp;lt;label for=&quot;memo-content&quot;&amp;gt;내용&amp;lt;/label&amp;gt;
        &amp;lt;textarea id=&quot;memo-content&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;&amp;lt;/textarea&amp;gt;

        &amp;lt;button type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;memo-list&quot;&amp;gt;
        &amp;lt;h2&amp;gt;메모 목록&amp;lt;/h2&amp;gt;
        &amp;lt;p class=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 구조 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 문서의 기본 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;!doctype html&amp;gt;은 이 문서가 HTML5 문서라는 것을 브라우저에 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lang=&quot;ko&quot;는 이 페이지의 주 언어가 한국어라는 뜻이다.&lt;br /&gt;처음에는 en으로 작성했지만, 페이지 내용이 한국어이므로 ko로 수정하는 것이 더 적절하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 문자 인코딩을 UTF-8로 설정한다.&lt;br /&gt;한글이 깨지지 않도록 하기 위한 설정이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 모바일 화면에서도 페이지가 적절한 크기로 보이도록 설정한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 HTML 파일과 CSS 파일을 연결한다.&lt;br /&gt;아직 CSS는 화면을 꾸미는 역할만 하고, 기능을 만들지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 화면을 감싸는 영역&lt;/h2&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;app&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;div는 여러 요소를 묶기 위한 태그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 메모장 전체 화면을 하나의 영역으로 감싸기 위해 app 클래스를 사용했다.&lt;br /&gt;나중에 CSS에서 .app을 선택해서 전체 너비, 여백, 배경 등을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 작성 영역&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;memo-form&quot;&amp;gt;
    &amp;lt;h2&amp;gt;새 메모 작성&amp;lt;/h2&amp;gt;

    &amp;lt;label for=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
    &amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; placeholder=&quot;제목을 입력하세요&quot;&amp;gt;

    &amp;lt;label for=&quot;memo-content&quot;&amp;gt;내용&amp;lt;/label&amp;gt;
    &amp;lt;textarea id=&quot;memo-content&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;&amp;lt;/textarea&amp;gt;

    &amp;lt;button type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 영역은 사용자가 새 메모를 작성하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2는 영역 제목이다. 여기서는 &amp;ldquo;새 메모 작성&amp;rdquo;이라는 제목을 표시한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;label for=&quot;memo-title&quot;&amp;gt;제목&amp;lt;/label&amp;gt;
&amp;lt;input id=&quot;memo-title&quot; type=&quot;text&quot; placeholder=&quot;제목을 입력하세요&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;label은 입력창이 어떤 값을 입력받는지 설명하는 태그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;label의 for=&quot;memo-title&quot;과 input의 id=&quot;memo-title&quot;이 연결되어 있다.&lt;br /&gt;이렇게 연결하면 사용자가 &amp;ldquo;제목&amp;rdquo;이라는 글자를 클릭해도 해당 입력창이 선택된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;textarea id=&quot;memo-content&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;textarea는 여러 줄의 텍스트를 입력할 때 사용한다.&lt;br /&gt;메모 내용은 한 줄보다 길어질 수 있으므로 input이 아니라 textarea를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot;&amp;gt;메모 추가&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼은 아직 동작하지 않는다.&lt;br /&gt;현재는 HTML 구조만 만드는 단계이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type=&quot;button&quot;을 지정한 이유는 나중에 버튼이 불필요하게 폼 제출처럼 동작하지 않게 하기 위해서다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모 목록 영역&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;memo-list&quot;&amp;gt;
    &amp;lt;h2&amp;gt;메모 목록&amp;lt;/h2&amp;gt;
    &amp;lt;p class=&quot;empty-message&quot;&amp;gt;아직 작성된 메모가 없습니다.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 영역은 작성된 메모들이 표시될 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 JavaScript 기능이 없기 때문에 실제 메모가 추가되지는 않는다.&lt;br /&gt;그래서 더미 데이터를 넣기보다는 &amp;ldquo;아직 작성된 메모가 없습니다.&amp;rdquo;라는 빈 목록 메시지를 보여주도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 JavaScript 단계에서는 사용자가 입력한 메모를 이 영역에 동적으로 추가하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 기능은 없지만, HTML 구조를 먼저 잡아두면 이후 CSS와 JavaScript를 붙일 때 훨씬 이해하기 쉽다.&lt;/p&gt;</description>
      <category>Project/Memo Evolution</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/165</guid>
      <comments>https://woojoo-devlog.tistory.com/165#entry165comment</comments>
      <pubDate>Tue, 9 Jun 2026 22:21:52 +0900</pubDate>
    </item>
    <item>
      <title>#0 메모장 프로젝트 시작</title>
      <link>https://woojoo-devlog.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 개발 공부를 하면서 HTML, CSS, JavaScript, React, Java, Servlet, Spring, Spring Boot를 따로따로 배우고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 각각의 기술을 따로 공부하다 보니, 이 기술들이 실제로 어떻게 연결되는지 직접 확인해보고 싶어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 간단한 메모장 프로젝트를 만들어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 HTML과 CSS로 정적인 메모장 화면을 만들고, JavaScript로 메모를 추가하고 삭제하는 기능을 붙일 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 React로 화면을 다시 구성해보고, Java와 Servlet/JSP를 이용해 서버에서 요청을 처리하는 방식도 공부할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음에는 JDBC로 데이터베이스를 연결하고, Spring과 Spring Boot로 같은 기능을 다시 구현해볼 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 메모장 기능을 여러 방식으로 구현해보면서, 웹 개발 기술들이 어떤 역할을 하는지 이해하는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행 과정은 블로그에 하나씩 기록할 예정이다.&lt;/p&gt;</description>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/164</guid>
      <comments>https://woojoo-devlog.tistory.com/164#entry164comment</comments>
      <pubDate>Tue, 9 Jun 2026 22:15:32 +0900</pubDate>
    </item>
    <item>
      <title>[KOSTA 가산] 웹 애플리케이션 개발 프로젝트 기반 Full-Stack 개발자 양성과정 후기</title>
      <link>https://woojoo-devlog.tistory.com/159</link>
      <description>&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;웹 개발을 공부하면서 가장 필요하다고 느낀 부분은 &lt;b&gt;전체 흐름에 대한 이해&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;Java, Spring, 데이터베이스, HTML, CSS, JavaScript 등을 각각 공부한 적은 있었지만, 막상 하나의 웹 애플리케이션을 만들려고 하면 각 기술이 어떤 역할을 하고 어떻게 연결되는지 명확하게 정리되지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;411&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드에서 요청을 보내고, 백엔드에서 비즈니스 로직을 처리하고, 데이터베이스에 데이터를 저장한 뒤 다시 화면에 결과를 보여주는 흐름을 실제 프로젝트를 통해 경험해보고 싶었다.&lt;/p&gt;
&lt;p data-end=&quot;621&quot; data-start=&quot;512&quot; data-ke-size=&quot;size16&quot;&gt;그 과정에서 KOSTA 가산의 웹 애플리케이션 개발 프로젝트 기반 Full-Stack 개발자 양성과정을 알게 되었고, 단순 이론 수업이 아니라 프로젝트 중심으로 진행된다는 점에서 지원하게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;621&quot; data-start=&quot;512&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;621&quot; data-start=&quot;512&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;643&quot; data-start=&quot;628&quot; data-section-id=&quot;dcianc&quot; data-ke-size=&quot;size26&quot;&gt;교육과정에서 배운 내용&lt;/h2&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;645&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 웹 애플리케이션 개발에 필요한 기초부터 프로젝트 구현까지 단계적으로 진행되는 과정이다.&lt;/p&gt;
&lt;p data-end=&quot;870&quot; data-start=&quot;701&quot; data-ke-size=&quot;size16&quot;&gt;초반에는 HTML, CSS, JavaScript와 같은 웹 기초를 학습하고, 이후 Java, 데이터베이스, Servlet/JSP, Spring Boot, React까지 이어진다. 단순히 문법을 배우는 데서 끝나는 것이 아니라, 웹 서비스가 실제로 어떤 구조로 동작하는지 이해하는 데 초점이 맞춰져 있다.&lt;/p&gt;
&lt;p data-end=&quot;1015&quot; data-start=&quot;872&quot; data-ke-size=&quot;size16&quot;&gt;특히 Java 수업은 이후 Spring Boot를 이해하는 데 중요한 기반이 된다.&lt;br /&gt;객체지향 개념, 클래스와 객체, 상속, 인터페이스, 컬렉션, 예외 처리 등 기본적인 내용부터 다시 정리하면서 Java 기반 백엔드 개발에 필요한 기초를 다질 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1149&quot; data-start=&quot;1017&quot; data-ke-size=&quot;size16&quot;&gt;이후 Servlet/JSP 수업에서는 웹 요청과 응답의 흐름을 직접 다루게 된다.&lt;br /&gt;브라우저에서 요청이 들어오고, 서버가 요청을 처리한 뒤 응답을 반환하는 구조를 코드로 확인하면서 웹 애플리케이션의 기본 동작 방식을 이해할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1292&quot; data-start=&quot;1151&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 수업에서는 이전에 배운 Java, 데이터베이스, 웹 구조가 하나로 연결된다.&lt;br /&gt;Controller, Service, Repository 구조를 통해 역할을 분리하고, 데이터베이스와 연동하여 실제 서비스를 구현하는 방식을 학습했다.&lt;/p&gt;
&lt;p data-end=&quot;1292&quot; data-start=&quot;1151&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1292&quot; data-start=&quot;1151&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1319&quot; data-start=&quot;1299&quot; data-section-id=&quot;q1cfot&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트 중심으로 진행되는 과정&lt;/h2&gt;
&lt;p data-end=&quot;1341&quot; data-start=&quot;1321&quot; data-ke-size=&quot;size16&quot;&gt;이 과정의 핵심은 프로젝트 경험이다.&lt;/p&gt;
&lt;p data-end=&quot;1481&quot; data-start=&quot;1343&quot; data-ke-size=&quot;size16&quot;&gt;수업에서 배운 내용을 바탕으로 직접 서비스를 기획하고 구현하는 과정이 포함되어 있다. 프로젝트는 단순히 정해진 기능을 따라 만드는 방식이 아니라, 주제 선정부터 화면 구성, 데이터베이스 설계, 기능 구현, 테스트, 발표까지 직접 진행하는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;1541&quot; data-start=&quot;1483&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 가장 크게 배운 점은 &lt;b&gt;기능 구현보다 흐름을 이해하는 것이 중요하다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-end=&quot;1687&quot; data-start=&quot;1543&quot; data-ke-size=&quot;size16&quot;&gt;하나의 기능을 만들 때도 단순히 버튼을 누르면 결과가 나오는 것이 아니라, 화면에서 어떤 요청을 보내는지, 백엔드에서는 어떤 데이터를 받아 검증하는지, 데이터베이스에는 어떤 형태로 저장되는지, 다시 프론트엔드에는 어떤 응답을 내려주는지를 함께 고려해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1758&quot; data-start=&quot;1689&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 프론트엔드와 백엔드가 분리되어 있더라도 결국 하나의 서비스 흐름 안에서 연결되어 있다는 점을 체감할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1908&quot; data-start=&quot;1760&quot; data-ke-size=&quot;size16&quot;&gt;또한 팀 프로젝트를 통해 협업 방식도 경험할 수 있었다.&lt;br /&gt;기능을 나누어 개발할 때는 API 명세, 데이터 구조, 화면 흐름을 맞추는 과정이 중요하다. 각자 맡은 부분만 구현하는 것이 아니라, 서로의 코드와 작업 흐름을 이해해야 전체 서비스가 정상적으로 동작한다.&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1910&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 오류도 많이 발생한다.&lt;br /&gt;화면에서는 정상적으로 요청을 보냈지만 서버에서 값이 제대로 전달되지 않거나, 데이터베이스 설계가 부족해 기능 구현이 어려워지는 경우도 있다. 이러한 문제를 해결하는 과정에서 디버깅, 원인 분석, 팀원 간 커뮤니케이션의 중요성을 배울 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1910&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2073&quot; data-start=&quot;1910&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2095&quot; data-start=&quot;2080&quot; data-section-id=&quot;2ufvmh&quot; data-ke-size=&quot;size26&quot;&gt;수업 방식과 학습 환경&lt;/h2&gt;
&lt;p data-end=&quot;2201&quot; data-start=&quot;2097&quot; data-ke-size=&quot;size16&quot;&gt;수업은 기초 개념을 설명한 뒤 실습을 통해 직접 확인하는 방식으로 진행된다.&lt;br /&gt;단순히 설명을 듣는 것에서 끝나는 것이 아니라, 직접 코드를 작성하고 실행해보면서 개념을 이해하는 구조다.&lt;/p&gt;
&lt;p data-end=&quot;2352&quot; data-start=&quot;2203&quot; data-ke-size=&quot;size16&quot;&gt;황연주 강사님은 수업 중간중간 수강생들이 내용을 따라오고 있는지 확인하며 진행한다.&lt;br /&gt;개념 설명뿐만 아니라 실제 개발에서는 어떤 식으로 사용되는지, 프로젝트에서는 어떤 점을 주의해야 하는지도 함께 설명해주기 때문에 단순 문법 학습보다 실무 흐름을 이해하는 데 도움이 된다.&lt;/p&gt;
&lt;p data-end=&quot;2484&quot; data-start=&quot;2354&quot; data-ke-size=&quot;size16&quot;&gt;다만 교육과정에서 배우는 내용이 많기 때문에 수업 시간만으로 모든 내용을 완전히 이해하기는 어렵다.&lt;br /&gt;Java, 데이터베이스, 웹 기초, Spring Boot, React, 프로젝트까지 짧은 기간 안에 다루기 때문에 복습은 필수다.&lt;/p&gt;
&lt;p data-end=&quot;2628&quot; data-start=&quot;2486&quot; data-ke-size=&quot;size16&quot;&gt;수업 후 배운 내용을 다시 정리하고, 프로젝트에서 직접 적용해보는 시간이 있어야 과정의 효과가 커진다.&lt;br /&gt;결국 이 과정은 수업을 듣는 것만으로 완성되는 과정이라기보다, 수업과 복습, 프로젝트 경험이 함께 쌓일 때 가장 많은 것을 얻을 수 있는 과정이다.&lt;/p&gt;
&lt;p data-end=&quot;2628&quot; data-start=&quot;2486&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2628&quot; data-start=&quot;2486&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2649&quot; data-start=&quot;2635&quot; data-section-id=&quot;1frnazm&quot; data-ke-size=&quot;size26&quot;&gt;과정을 통해 얻은 점&lt;/h2&gt;
&lt;p data-end=&quot;2690&quot; data-start=&quot;2651&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 웹 애플리케이션 개발의 전체 흐름을 정리할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2828&quot; data-start=&quot;2692&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 각각의 기술을 따로 공부하는 느낌이 강했다.&lt;br /&gt;HTML, CSS, JavaScript는 화면을 만드는 기술이고, Java와 Spring은 서버를 만드는 기술이며, 데이터베이스는 데이터를 저장하는 기술이라고만 막연하게 이해하고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2901&quot; data-start=&quot;2830&quot; data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트를 진행하면서 이 기술들이 각각 따로 존재하는 것이 아니라 하나의 서비스 안에서 연결된다는 점을 이해하게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;3017&quot; data-start=&quot;2903&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드는 사용자의 입력을 받고 요청을 보내는 역할을 한다.&lt;br /&gt;백엔드는 요청을 받아 필요한 로직을 처리하고 데이터를 관리한다.&lt;br /&gt;데이터베이스는 서비스에 필요한 데이터를 저장하고 조회하는 역할을 한다.&lt;/p&gt;
&lt;p data-end=&quot;3177&quot; data-start=&quot;3019&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 직접 경험하면서 백엔드 개발자로서 어떤 부분을 더 깊게 공부해야 하는지도 명확해졌다.&lt;br /&gt;단순히 기능을 구현하는 것을 넘어, 데이터가 어떻게 이동하는지, 요청과 응답이 어떤 구조로 처리되는지, 서비스의 상태가 어떻게 관리되는지를 고민하는 것이 중요하다는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;3177&quot; data-start=&quot;3019&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3177&quot; data-start=&quot;3019&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3202&quot; data-start=&quot;3184&quot; data-section-id=&quot;r962gg&quot; data-ke-size=&quot;size26&quot;&gt;이런 사람에게 추천하는 과정&lt;/h2&gt;
&lt;p data-end=&quot;3307&quot; data-start=&quot;3204&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 웹 개발을 처음 시작하는 사람에게도 도움이 되는 과정이다.&lt;br /&gt;기초부터 단계적으로 진행되기 때문에 비전공자도 꾸준히 복습하며 따라간다면 웹 개발의 전체 흐름을 잡을 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3468&quot; data-start=&quot;3309&quot; data-ke-size=&quot;size16&quot;&gt;또한 이미 개발 공부를 어느 정도 해본 사람에게도 의미가 있다.&lt;br /&gt;각 기술을 따로 공부했지만 실제 프로젝트 경험이 부족한 사람, 프론트엔드와 백엔드가 어떻게 연결되는지 명확히 이해하고 싶은 사람, Spring Boot 기반 백엔드 개발 흐름을 경험하고 싶은 사람에게 적합한 과정이다.&lt;/p&gt;
&lt;p data-end=&quot;3563&quot; data-start=&quot;3470&quot; data-ke-size=&quot;size16&quot;&gt;다만 수업량이 적은 과정은 아니기 때문에 스스로 공부하는 시간이 필요하다.&lt;br /&gt;수업을 듣고 끝내는 것이 아니라, 배운 내용을 정리하고 직접 구현해보는 태도가 중요하다.&lt;/p&gt;
&lt;p data-end=&quot;3563&quot; data-start=&quot;3470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3563&quot; data-start=&quot;3470&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3576&quot; data-start=&quot;3570&quot; data-section-id=&quot;1h9nj85&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-end=&quot;3663&quot; data-start=&quot;3578&quot; data-ke-size=&quot;size16&quot;&gt;KOSTA 가산 웹 애플리케이션 개발 프로젝트 기반 Full-Stack 개발자 양성과정은 웹 개발의 기초부터 프로젝트 구현까지 경험할 수 있는 과정이다.&lt;/p&gt;
&lt;p data-end=&quot;3770&quot; data-start=&quot;3665&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 Java와 Spring Boot 기반 백엔드 개발 흐름을 이해할 수 있었고, 프론트엔드와 백엔드, 데이터베이스가 하나의 서비스 안에서 어떻게 연결되는지도 정리할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;3849&quot; data-start=&quot;3772&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 프로젝트를 직접 진행하면서 기능 구현뿐만 아니라 설계, 협업, 문제 해결 과정까지 경험할 수 있었다는 점이 가장 큰 의미가 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;3958&quot; data-start=&quot;3851&quot; data-ke-size=&quot;size16&quot;&gt;웹 개발을 공부하고 있지만 전체 흐름이 잘 잡히지 않는 사람, 프로젝트 경험을 통해 실력을 키우고 싶은 사람, 백엔드 개발자로 성장하기 위한 기반을 만들고 싶은 사람에게 추천할 만한 과정이다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/159</guid>
      <comments>https://woojoo-devlog.tistory.com/159#entry159comment</comments>
      <pubDate>Mon, 18 May 2026 15:24:06 +0900</pubDate>
    </item>
    <item>
      <title>조회 결과를 Optional로 받는 이유</title>
      <link>https://woojoo-devlog.tistory.com/124</link>
      <description>&lt;p data-end=&quot;106&quot; data-start=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;106&quot; data-start=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 사용하다 보면 findById() 같은 조회 메서드의 반환 타입이 Optional&amp;lt;T&amp;gt;인 것을 자주 보게 된다.&lt;/p&gt;
&lt;p data-end=&quot;158&quot; data-start=&quot;108&quot; data-ke-size=&quot;size16&quot;&gt;처음 보면&lt;br /&gt;&amp;ldquo;그냥 엔티티를 바로 주면 되는 것 아닌가?&amp;rdquo;&lt;br /&gt;라는 생각이 들 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;242&quot; data-start=&quot;160&quot; data-ke-size=&quot;size16&quot;&gt;하지만 조회라는 작업은 본질적으로 &lt;b&gt;결과가 있을 수도 있고, 없을 수도 있는 작업&lt;/b&gt;이다.&lt;br /&gt;바로 이 점 때문에 Optional이 필요하다.&lt;/p&gt;
&lt;p data-end=&quot;242&quot; data-start=&quot;160&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;270&quot; data-start=&quot;249&quot; data-section-id=&quot;5qq0k6&quot; data-ke-size=&quot;size26&quot;&gt;조회는 항상 성공하는 것이 아니다&lt;/h2&gt;
&lt;p data-end=&quot;311&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스 조회는 개발자가 기대한 대로 항상 값을 찾는 것이 아니다.&lt;/p&gt;
&lt;p data-end=&quot;339&quot; data-start=&quot;313&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 경우를 생각할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;435&quot; data-start=&quot;341&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;370&quot; data-start=&quot;341&quot; data-section-id=&quot;1snz6c7&quot;&gt;사용자가 존재하지 않는 게시글 번호를 요청한 경우&lt;/li&gt;
&lt;li data-end=&quot;391&quot; data-start=&quot;371&quot; data-section-id=&quot;18p4w7g&quot;&gt;이미 삭제된 데이터를 조회한 경우&lt;/li&gt;
&lt;li data-end=&quot;410&quot; data-start=&quot;392&quot; data-section-id=&quot;k1aggm&quot;&gt;잘못된 파라미터가 전달된 경우&lt;/li&gt;
&lt;li data-end=&quot;435&quot; data-start=&quot;411&quot; data-section-id=&quot;1qsbgw2&quot;&gt;아직 저장되지 않은 식별자로 조회한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;463&quot; data-start=&quot;437&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 조회 결과가 없을 수밖에 없다.&lt;/p&gt;
&lt;p data-end=&quot;499&quot; data-start=&quot;465&quot; data-ke-size=&quot;size16&quot;&gt;즉, 조회 메서드는 구조적으로 다음 두 가지 가능성을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;521&quot; data-start=&quot;501&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;501&quot; data-section-id=&quot;eyexzw&quot;&gt;값을 찾았다&lt;/li&gt;
&lt;li data-end=&quot;521&quot; data-start=&quot;510&quot; data-section-id=&quot;ioqz4n&quot;&gt;값을 찾지 못했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;560&quot; data-start=&quot;523&quot; data-ke-size=&quot;size16&quot;&gt;문제는 이 &amp;ldquo;값이 없을 수 있음&amp;rdquo;을 코드에서 어떻게 표현하느냐이다.&lt;/p&gt;
&lt;p data-end=&quot;560&quot; data-start=&quot;523&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;560&quot; data-start=&quot;523&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;587&quot; data-start=&quot;567&quot; data-section-id=&quot;167u4hk&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;예전 방식: null로 표현&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;626&quot; data-start=&quot;589&quot; data-ke-size=&quot;size16&quot;&gt;과거에는 조회 결과가 없으면 null을 반환하는 방식이 흔했다.&lt;/p&gt;
&lt;p data-end=&quot;660&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 단순해 보이지만, 실제로는 많은 문제를 만든다.&lt;/p&gt;
&lt;h3 data-end=&quot;685&quot; data-start=&quot;662&quot; data-section-id=&quot;1k0o1ey&quot; data-ke-size=&quot;size23&quot;&gt;1. null 체크를 깜빡하기 쉽다&lt;/h3&gt;
&lt;p data-end=&quot;714&quot; data-start=&quot;687&quot; data-ke-size=&quot;size16&quot;&gt;조회한 객체를 바로 사용하는 순간 문제가 생긴다.&lt;/p&gt;
&lt;pre id=&quot;code_1775266735740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Board board = boardRepository.findSomething(...);
System.out.println(board.getTitle());&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조회 결과가 없어서 board가 null이라면, 이 코드는 실행 중에 `NullPointerException`을 발생시킨다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;942&quot; data-start=&quot;892&quot; data-ke-size=&quot;size16&quot;&gt;즉, 문제는 &amp;ldquo;값이 없다&amp;rdquo;는 사실이 아니라&lt;br /&gt;그 사실을 개발자가 놓치기 쉽다는 데 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;973&quot; data-start=&quot;944&quot; data-section-id=&quot;1q3p71v&quot; data-ke-size=&quot;size23&quot;&gt;2. 코드만 보고는 위험한 지점을 알기 어렵다&lt;/h3&gt;
&lt;p data-end=&quot;1057&quot; data-start=&quot;975&quot; data-ke-size=&quot;size16&quot;&gt;반환 타입이 그냥 Board라면, 이 값이 항상 존재하는 값인지, 아니면 null일 수도 있는 값인지 메서드 시그니처만 보고는 알기 어렵다.&lt;/p&gt;
&lt;p data-end=&quot;1097&quot; data-start=&quot;1059&quot; data-ke-size=&quot;size16&quot;&gt;이 말은 곧, &lt;b&gt;위험한 값인데도 안전한 값처럼 보인다&lt;/b&gt;는 뜻이다.&lt;/p&gt;
&lt;h2 data-end=&quot;1143&quot; data-start=&quot;1104&quot; data-section-id=&quot;e14dtz&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Optional은 &amp;ldquo;값이 없을 수도 있다&amp;rdquo;를 타입으로 표현한다&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1196&quot; data-start=&quot;1145&quot; data-ke-size=&quot;size16&quot;&gt;Optional&amp;lt;T&amp;gt;는 단순한 포장 객체가 아니다.&lt;br /&gt;이 타입의 핵심 목적은 다음이다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1224&quot; data-start=&quot;1198&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1224&quot; data-start=&quot;1200&quot; data-ke-size=&quot;size16&quot;&gt;이 값은 있을 수도 있고, 없을 수도 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1314&quot; data-start=&quot;1226&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1314&quot; data-start=&quot;1226&quot; data-ke-size=&quot;size16&quot;&gt;즉, Optional&amp;lt;Board&amp;gt;는&lt;br /&gt;&amp;ldquo;Board가 반드시 있다&amp;rdquo;가 아니라&lt;br /&gt;&amp;ldquo;Board가 존재할 수도 있고, 비어 있을 수도 있다&amp;rdquo;는 의미를 갖는다.&lt;/p&gt;
&lt;p data-end=&quot;1350&quot; data-start=&quot;1316&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 조회 결과의 불확실성이 코드에 명확하게 드러난다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1775266872770&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Optional&amp;lt;Board&amp;gt; result = boardRepository.findById(bno);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1451&quot; data-start=&quot;1421&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄은 단순한 문법이 아니라 다음 의미를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1516&quot; data-start=&quot;1453&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1463&quot; data-start=&quot;1453&quot; data-section-id=&quot;1wijmgs&quot;&gt;조회를 시도했다&lt;/li&gt;
&lt;li data-end=&quot;1478&quot; data-start=&quot;1464&quot; data-section-id=&quot;fa2kvs&quot;&gt;결과가 있을 수도 있다&lt;/li&gt;
&lt;li data-end=&quot;1489&quot; data-start=&quot;1479&quot; data-section-id=&quot;xm1jdm&quot;&gt;없을 수도 있다&lt;/li&gt;
&lt;li data-end=&quot;1516&quot; data-start=&quot;1490&quot; data-section-id=&quot;7wdio1&quot;&gt;따라서 바로 쓰지 말고 확인하고 다뤄야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1536&quot; data-start=&quot;1523&quot; data-section-id=&quot;1qz5eir&quot; data-ke-size=&quot;size26&quot;&gt;왜 이것이 중요한가&lt;/h2&gt;
&lt;p data-end=&quot;1614&quot; data-start=&quot;1538&quot; data-ke-size=&quot;size16&quot;&gt;Optional을 사용하는 가장 큰 이유는&lt;br /&gt;&lt;b&gt;조회 결과가 없을 수 있다는 사실을 개발자가 무시하지 못하게 만들기 위해서&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1681&quot; data-start=&quot;1616&quot; data-ke-size=&quot;size16&quot;&gt;즉, 단순히 null을 감추는 것이 아니라&lt;br /&gt;&amp;ldquo;이 값은 안전하게 처리해야 한다&amp;rdquo;는 신호를 타입 자체로 주는 것이다.&lt;/p&gt;
&lt;h3 data-end=&quot;1707&quot; data-start=&quot;1683&quot; data-section-id=&quot;10zx5f8&quot; data-ke-size=&quot;size23&quot;&gt;장점 1. API의 의도가 명확해진다&lt;/h3&gt;
&lt;p data-end=&quot;1729&quot; data-start=&quot;1709&quot; data-ke-size=&quot;size16&quot;&gt;메서드 시그니처만 봐도 알 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775266907668&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Optional&amp;lt;Board&amp;gt; findById(Long id)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1798&quot; data-start=&quot;1778&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드는 다음을 명확히 전달한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1849&quot; data-start=&quot;1800&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1819&quot; data-start=&quot;1800&quot; data-section-id=&quot;7gre9c&quot;&gt;조회 결과가 비어 있을 수 있다&lt;/li&gt;
&lt;li data-end=&quot;1849&quot; data-start=&quot;1820&quot; data-section-id=&quot;8iq4c5&quot;&gt;호출하는 쪽에서 반드시 그 가능성을 고려해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1888&quot; data-start=&quot;1851&quot; data-ke-size=&quot;size16&quot;&gt;반환 타입이 그냥 Board였다면 이런 의도가 드러나지 않는다.&lt;/p&gt;
&lt;h3 data-end=&quot;1916&quot; data-start=&quot;1890&quot; data-section-id=&quot;1id6ql4&quot; data-ke-size=&quot;size23&quot;&gt;장점 2. null 실수를 줄일 수 있다&lt;/h3&gt;
&lt;p data-end=&quot;1954&quot; data-start=&quot;1918&quot; data-ke-size=&quot;size16&quot;&gt;Optional은 개발자에게 결과 처리 방식을 선택하게 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2028&quot; data-start=&quot;1956&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1969&quot; data-start=&quot;1956&quot; data-section-id=&quot;oh0n03&quot;&gt;값이 있으면 사용한다&lt;/li&gt;
&lt;li data-end=&quot;1986&quot; data-start=&quot;1970&quot; data-section-id=&quot;ist49g&quot;&gt;값이 없으면 예외를 던진다&lt;/li&gt;
&lt;li data-end=&quot;2005&quot; data-start=&quot;1987&quot; data-section-id=&quot;1c66b&quot;&gt;값이 없으면 기본값을 사용한다&lt;/li&gt;
&lt;li data-end=&quot;2028&quot; data-start=&quot;2006&quot; data-section-id=&quot;twy332&quot;&gt;값이 있을 때만 특정 로직을 수행한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2070&quot; data-start=&quot;2030&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;없을 때 어떻게 할 것인가&amp;rdquo;를 코드 작성 시점에 고민하게 만든다.&lt;/p&gt;
&lt;h3 data-end=&quot;2104&quot; data-start=&quot;2072&quot; data-section-id=&quot;g0fndu&quot; data-ke-size=&quot;size23&quot;&gt;장점 3. 예외 상황을 더 명확하게 처리할 수 있다&lt;/h3&gt;
&lt;p data-end=&quot;2132&quot; data-start=&quot;2106&quot; data-ke-size=&quot;size16&quot;&gt;조회 결과가 없을 때의 대응은 상황마다 다르다.&lt;/p&gt;
&lt;p data-end=&quot;2140&quot; data-start=&quot;2134&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2227&quot; data-start=&quot;2142&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2164&quot; data-start=&quot;2142&quot; data-section-id=&quot;mdjzq5&quot;&gt;상세 조회에서는 예외를 던질 수 있다&lt;/li&gt;
&lt;li data-end=&quot;2198&quot; data-start=&quot;2165&quot; data-section-id=&quot;kpa477&quot;&gt;수정 화면에서는 존재하지 않는 데이터라고 안내할 수 있다&lt;/li&gt;
&lt;li data-end=&quot;2227&quot; data-start=&quot;2199&quot; data-section-id=&quot;vf84ub&quot;&gt;선택적 조회에서는 그냥 빈 값으로 넘길 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2262&quot; data-start=&quot;2229&quot; data-ke-size=&quot;size16&quot;&gt;Optional은 이런 흐름을 자연스럽게 표현하기 좋다.&lt;/p&gt;
&lt;p data-end=&quot;2262&quot; data-start=&quot;2229&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2262&quot; data-start=&quot;2229&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2300&quot; data-start=&quot;2269&quot; data-section-id=&quot;ud73mn&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;JPA에서 Optional이 특히 잘 맞는 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2380&quot; data-start=&quot;2302&quot; data-ke-size=&quot;size16&quot;&gt;JPA에서 자주 하는 작업 중 하나가 &lt;b&gt;식별자 기반 조회&lt;/b&gt;이다.&lt;br /&gt;그런데 식별자로 조회한다고 해서 항상 데이터가 존재하는 것은 아니다.&lt;/p&gt;
&lt;p data-end=&quot;2438&quot; data-start=&quot;2382&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글 번호 100L로 조회를 요청했더라도, 실제 DB에는 그 데이터가 없을 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2551&quot; data-start=&quot;2440&quot; data-ke-size=&quot;size16&quot;&gt;이 상황에서 JPA가 반환 타입을 Optional&amp;lt;T&amp;gt;로 제공하는 것은 매우 자연스럽다.&lt;br /&gt;왜냐하면 이것은 예외적인 상황이 아니라 &lt;b&gt;조회라는 작업 자체가 원래 갖고 있는 속성&lt;/b&gt;이기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;2577&quot; data-start=&quot;2553&quot; data-ke-size=&quot;size16&quot;&gt;즉, JPA는 다음처럼 말하고 있는 셈이다.&lt;/p&gt;
&lt;blockquote data-end=&quot;2625&quot; data-start=&quot;2579&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2625&quot; data-start=&quot;2581&quot; data-ke-size=&quot;size16&quot;&gt;조회 결과가 없을 수도 있으니, 호출하는 쪽에서 그 가능성을 고려해서 처리하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2661&quot; data-start=&quot;2632&quot; data-section-id=&quot;1ear2hj&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;2661&quot; data-start=&quot;2632&quot; data-section-id=&quot;1ear2hj&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Optional이 유도하는 올바른 처리 방식&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2730&quot; data-start=&quot;2663&quot; data-ke-size=&quot;size16&quot;&gt;Optional을 받으면 개발자는 결과를 바로 꺼내 쓰기보다,&lt;br /&gt;먼저 &amp;ldquo;없을 수도 있다&amp;rdquo;는 전제에서 처리하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;2752&quot; data-start=&quot;2732&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 방식이 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;2772&quot; data-start=&quot;2754&quot; data-section-id=&quot;63sa03&quot; data-ke-size=&quot;size23&quot;&gt;1. 없으면 예외를 던진다&lt;/h3&gt;
&lt;p data-end=&quot;2820&quot; data-start=&quot;2774&quot; data-ke-size=&quot;size16&quot;&gt;상세 조회, 수정, 삭제 같은 작업에서는 데이터가 반드시 있어야 하는 경우가 많다.&lt;/p&gt;
&lt;pre id=&quot;code_1775266956533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Board board = boardRepository.findById(bno)
.orElseThrow(() -&amp;gt; new RuntimeException(&quot;게시글이 존재하지 않습니다.&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2997&quot; data-start=&quot;2948&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은&lt;br /&gt;&amp;ldquo;없으면 조용히 넘어가지 말고 명확하게 실패하라&amp;rdquo;&lt;br /&gt;는 의도를 표현한다.&lt;/p&gt;
&lt;h3 data-end=&quot;3024&quot; data-start=&quot;3004&quot; data-section-id=&quot;1rdsfyv&quot; data-ke-size=&quot;size23&quot;&gt;2. 없으면 기본값을 사용한다&lt;/h3&gt;
&lt;p data-end=&quot;3068&quot; data-start=&quot;3026&quot; data-ke-size=&quot;size16&quot;&gt;경우에 따라서는 조회 결과가 없어도 기본 객체나 대체 값을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1775267138098&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Board board = boardRepository.findById(bno)
.orElse(new Board());&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다만 이 방식은 실제로 기본값이 의미가 있을 때만 사용해야 한다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;3220&quot; data-start=&quot;3200&quot; data-section-id=&quot;7rcuyp&quot; data-ke-size=&quot;size23&quot;&gt;3. 값이 있을 때만 동작한다&lt;/h3&gt;
&lt;p data-end=&quot;3265&quot; data-start=&quot;3222&quot; data-ke-size=&quot;size16&quot;&gt;조회 결과가 있으면 처리하고, 없으면 아무 작업도 하지 않는 방식도 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1775267160345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boardRepository.findById(bno)
.ifPresent(board -&amp;gt; {
System.out.println(board.getTitle());
});&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;3473&quot; data-start=&quot;3407&quot; data-section-id=&quot;pqbl4j&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;중요한 점: Optional은 &amp;ldquo;무조건 쓰는 것&amp;rdquo;이 아니라 &amp;ldquo;조회 결과가 비어 있을 수 있을 때 의미가 있다&amp;rdquo;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3475&quot; data-ke-size=&quot;size16&quot;&gt;여기서 오해하면 안 되는 부분이 있다.&lt;/p&gt;
&lt;p data-end=&quot;3580&quot; data-start=&quot;3498&quot; data-ke-size=&quot;size16&quot;&gt;Optional은 모든 곳에 무조건 붙이는 도구가 아니다.&lt;br /&gt;특히 JPA에서 &lt;b&gt;조회 결과가 없을 수 있는 상황&lt;/b&gt;을 표현할 때 의미가 크다.&lt;/p&gt;
&lt;p data-end=&quot;3594&quot; data-start=&quot;3582&quot; data-ke-size=&quot;size16&quot;&gt;즉, 핵심은 다음이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3694&quot; data-start=&quot;3596&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3639&quot; data-start=&quot;3596&quot; data-section-id=&quot;1ir6asc&quot;&gt;조회 결과가 반드시 있다고 보장할 수 없다면 Optional이 적절하다&lt;/li&gt;
&lt;li data-end=&quot;3694&quot; data-start=&quot;3640&quot; data-section-id=&quot;1osn40j&quot;&gt;결과가 없을 수 있다는 사실을 API 수준에서 드러내고 싶을 때 Optional이 적절하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3765&quot; data-start=&quot;3696&quot; data-ke-size=&quot;size16&quot;&gt;반대로 말하면,&lt;br /&gt;항상 값이 있어야 하는 내부 로직이나 필드까지 무조건 Optional로 감싸는 것은 바람직하지 않다.&lt;/p&gt;
&lt;p data-end=&quot;3765&quot; data-start=&quot;3696&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3765&quot; data-start=&quot;3696&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3810&quot; data-start=&quot;3772&quot; data-section-id=&quot;1wbpjyz&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Optional을 사용하는 목적은 문법이 아니라 설계에 있다&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3867&quot; data-start=&quot;3812&quot; data-ke-size=&quot;size16&quot;&gt;많은 초보 개발자는 Optional을 그냥&lt;br /&gt;&amp;ldquo;null 대신 쓰는 문법&amp;rdquo;&lt;br /&gt;정도로 이해한다.&lt;/p&gt;
&lt;p data-end=&quot;3895&quot; data-start=&quot;3869&quot; data-ke-size=&quot;size16&quot;&gt;하지만 더 중요한 것은 문법보다 설계 의도이다.&lt;/p&gt;
&lt;p data-end=&quot;3941&quot; data-start=&quot;3897&quot; data-ke-size=&quot;size16&quot;&gt;Optional은 단순한 포장지가 아니라, 다음을 코드에 드러내는 도구이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4003&quot; data-start=&quot;3943&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3959&quot; data-start=&quot;3943&quot; data-section-id=&quot;dmkzcc&quot;&gt;이 조회는 실패할 수 있다&lt;/li&gt;
&lt;li data-end=&quot;3974&quot; data-start=&quot;3960&quot; data-section-id=&quot;18wvo3a&quot;&gt;결과가 없을 수도 있다&lt;/li&gt;
&lt;li data-end=&quot;4003&quot; data-start=&quot;3975&quot; data-section-id=&quot;58eeax&quot;&gt;호출하는 쪽은 그 가능성을 반드시 처리해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4056&quot; data-start=&quot;4005&quot; data-ke-size=&quot;size16&quot;&gt;즉, Optional은&lt;br /&gt;&lt;b&gt;조회 결과의 불확실성을 숨기지 않고 드러내는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;4096&quot; data-start=&quot;4058&quot; data-ke-size=&quot;size16&quot;&gt;이 점에서 Optional은 JPA의 조회 메서드와 잘 어울린다.&lt;/p&gt;
&lt;p data-end=&quot;4096&quot; data-start=&quot;4058&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4096&quot; data-start=&quot;4058&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;4108&quot; data-start=&quot;4103&quot; data-section-id=&quot;1melx8&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-end=&quot;4147&quot; data-start=&quot;4110&quot; data-ke-size=&quot;size16&quot;&gt;JPA에서 조회 결과를 Optional로 받는 이유는 단순하다.&lt;/p&gt;
&lt;p data-end=&quot;4175&quot; data-start=&quot;4149&quot; data-ke-size=&quot;size16&quot;&gt;조회는 항상 값을 찾는 작업이 아니기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;4279&quot; data-start=&quot;4177&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 없을 수도 있는데, 그 상황을 그냥 null로 넘겨버리면&lt;br /&gt;개발자는 그 위험을 놓치기 쉽고, 결국 NullPointerException 같은 문제로 이어질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;4310&quot; data-start=&quot;4281&quot; data-ke-size=&quot;size16&quot;&gt;반면 Optional은 다음을 명확하게 보여준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4383&quot; data-start=&quot;4312&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4328&quot; data-start=&quot;4312&quot; data-section-id=&quot;9vidwu&quot;&gt;조회 결과가 없을 수 있다&lt;/li&gt;
&lt;li data-end=&quot;4354&quot; data-start=&quot;4329&quot; data-section-id=&quot;1ncw2al&quot;&gt;그 가능성을 호출하는 쪽에서 처리해야 한다&lt;/li&gt;
&lt;li data-end=&quot;4383&quot; data-start=&quot;4355&quot; data-section-id=&quot;1x7v91a&quot;&gt;null을 무심코 사용하는 실수를 줄일 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4465&quot; data-start=&quot;4385&quot; data-ke-size=&quot;size16&quot;&gt;즉, JPA에서 Optional을 쓰는 이유는&lt;br /&gt;편의 때문이 아니라 &lt;b&gt;조회 결과의 불확실성을 더 안전하고 명확하게 다루기 위해서&lt;/b&gt;이다.&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/124</guid>
      <comments>https://woojoo-devlog.tistory.com/124#entry124comment</comments>
      <pubDate>Sat, 4 Apr 2026 10:46:44 +0900</pubDate>
    </item>
    <item>
      <title>Bootstrap이란?</title>
      <link>https://woojoo-devlog.tistory.com/122</link>
      <description>&lt;p data-end=&quot;192&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;192&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;웹 개발을 처음 시작하면 HTML은 어느 정도 금방 익숙해진다.&lt;br /&gt;하지만 대부분 사람들이 막히는 지점은 바로 &lt;b&gt;CSS(디자인)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;254&quot; data-start=&quot;194&quot; data-ke-size=&quot;size16&quot;&gt;버튼 하나, 카드 하나, 레이아웃 하나를 만들려고 해도&lt;br /&gt;생각보다 고려해야 할 요소가 너무 많기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;이때 등장하는 것이 바로 &lt;b&gt;부트스트랩(Bootstrap)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;314&quot; data-start=&quot;300&quot;&gt;부트스트랩이란 무엇인가&lt;/h1&gt;
&lt;p data-end=&quot;374&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 HTML, CSS, JavaScript로 만들어진&lt;br /&gt;&lt;b&gt;프론트엔드 UI 프레임워크&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;383&quot; data-start=&quot;376&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면,&lt;/p&gt;
&lt;blockquote data-end=&quot;438&quot; data-start=&quot;385&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;438&quot; data-start=&quot;387&quot; data-ke-size=&quot;size16&quot;&gt;웹페이지에서 자주 사용하는 디자인 요소들을&lt;br /&gt;미리 만들어 놓은 &amp;ldquo;디자인 도구 모음&amp;rdquo;이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;451&quot; data-start=&quot;440&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;451&quot; data-start=&quot;440&quot; data-ke-size=&quot;size16&quot;&gt;라고 이해하면 된다.&lt;/p&gt;
&lt;p data-end=&quot;474&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 것들이 포함되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;541&quot; data-start=&quot;476&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;476&quot;&gt;버튼&lt;/li&gt;
&lt;li data-end=&quot;493&quot; data-start=&quot;481&quot;&gt;입력창 (form)&lt;/li&gt;
&lt;li data-end=&quot;501&quot; data-start=&quot;494&quot;&gt;카드 UI&lt;/li&gt;
&lt;li data-end=&quot;507&quot; data-start=&quot;502&quot;&gt;테이블&lt;/li&gt;
&lt;li data-end=&quot;517&quot; data-start=&quot;508&quot;&gt;네비게이션 바&lt;/li&gt;
&lt;li data-end=&quot;523&quot; data-start=&quot;518&quot;&gt;모달창&lt;/li&gt;
&lt;li data-end=&quot;541&quot; data-start=&quot;524&quot;&gt;레이아웃 시스템 (grid)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;566&quot; data-start=&quot;548&quot;&gt;원래 웹 디자인이 왜 어려운가&lt;/h1&gt;
&lt;p data-end=&quot;613&quot; data-start=&quot;568&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩을 이해하려면 먼저&lt;br /&gt;&amp;ldquo;왜 원래 웹 디자인이 어려운지&amp;rdquo;를 알아야 한다.&lt;/p&gt;
&lt;p data-end=&quot;637&quot; data-start=&quot;615&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 버튼 하나를 만든다고 해보자.&lt;/p&gt;
&lt;p data-end=&quot;659&quot; data-start=&quot;639&quot; data-ke-size=&quot;size16&quot;&gt;단순히 HTML만 쓰면 이렇게 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;`&amp;lt;button&amp;gt;&lt;/span&gt;&lt;span&gt;저장&lt;/span&gt;&lt;span&gt;&amp;lt;/button&amp;gt;`&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;728&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;이건 그냥 기본 버튼이다.&lt;br /&gt;디자인이 전혀 되어 있지 않다.&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;730&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통은 CSS를 직접 작성해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;818&quot; data-start=&quot;756&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;764&quot; data-start=&quot;756&quot;&gt;배경색 설정&lt;/li&gt;
&lt;li data-end=&quot;773&quot; data-start=&quot;765&quot;&gt;글자색 설정&lt;/li&gt;
&lt;li data-end=&quot;782&quot; data-start=&quot;774&quot;&gt;테두리 제거&lt;/li&gt;
&lt;li data-end=&quot;790&quot; data-start=&quot;783&quot;&gt;여백 설정&lt;/li&gt;
&lt;li data-end=&quot;799&quot; data-start=&quot;791&quot;&gt;둥근 모서리&lt;/li&gt;
&lt;li data-end=&quot;810&quot; data-start=&quot;800&quot;&gt;hover 효과&lt;/li&gt;
&lt;li data-end=&quot;818&quot; data-start=&quot;811&quot;&gt;클릭 효과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;848&quot; data-start=&quot;820&quot; data-ke-size=&quot;size16&quot;&gt;즉 버튼 하나에도 여러 요소를 직접 만들어야 한다.&lt;/p&gt;
&lt;p data-end=&quot;848&quot; data-start=&quot;820&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;848&quot; data-start=&quot;820&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;879&quot; data-start=&quot;855&quot;&gt;부트스트랩은 이 문제를 어떻게 해결하는가&lt;/h1&gt;
&lt;p data-end=&quot;902&quot; data-start=&quot;881&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 이 과정을 완전히 바꾼다.&lt;/p&gt;
&lt;p data-end=&quot;956&quot; data-start=&quot;904&quot; data-ke-size=&quot;size16&quot;&gt;개발자가 직접 CSS를 작성하는 대신&lt;br /&gt;&lt;b&gt;이미 만들어진 디자인을 가져다 쓰는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;958&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 버튼을 부트스트랩으로 만들면 이렇게 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;`&amp;lt;button&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;btn btn-primary&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;저장&lt;/span&gt;&lt;span&gt;&amp;lt;/button&amp;gt;`&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1057&quot; data-start=&quot;1049&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄만으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1117&quot; data-start=&quot;1059&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1068&quot; data-start=&quot;1059&quot;&gt;색이 입혀지고&lt;/li&gt;
&lt;li data-end=&quot;1079&quot; data-start=&quot;1069&quot;&gt;여백이 정리되고&lt;/li&gt;
&lt;li data-end=&quot;1096&quot; data-start=&quot;1080&quot;&gt;hover 효과가 적용되고&lt;/li&gt;
&lt;li data-end=&quot;1117&quot; data-start=&quot;1097&quot;&gt;전체적으로 깔끔한 버튼이 완성된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1120&quot; data-start=&quot;1119&quot; data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;blockquote data-end=&quot;1177&quot; data-start=&quot;1122&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1177&quot; data-start=&quot;1124&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;디자인을 직접 만드는 것&amp;rdquo;이 아니라&lt;br /&gt;&amp;ldquo;이미 만들어진 디자인을 선택해서 사용하는 것&amp;rdquo;이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;1210&quot; data-start=&quot;1184&quot;&gt;&amp;ldquo;쉽게 디자인할 수 있다&amp;rdquo;는 말의 진짜 의미&lt;/h1&gt;
&lt;p data-end=&quot;1225&quot; data-start=&quot;1212&quot; data-ke-size=&quot;size16&quot;&gt;많이 오해하는 부분이다.&lt;/p&gt;
&lt;p data-end=&quot;1268&quot; data-start=&quot;1227&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩이 있다고 해서&lt;br /&gt;자동으로 멋진 UI가 만들어지는 것은 아니다.&lt;/p&gt;
&lt;p data-end=&quot;1285&quot; data-start=&quot;1270&quot; data-ke-size=&quot;size16&quot;&gt;정확한 의미는 다음과 같다.&lt;/p&gt;
&lt;h2 data-end=&quot;1311&quot; data-start=&quot;1287&quot; data-ke-size=&quot;size26&quot;&gt;1. CSS를 직접 많이 안 써도 된다&lt;/h2&gt;
&lt;p data-end=&quot;1336&quot; data-start=&quot;1312&quot; data-ke-size=&quot;size16&quot;&gt;자주 사용하는 스타일이 이미 준비되어 있다.&lt;/p&gt;
&lt;h2 data-end=&quot;1358&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size26&quot;&gt;2. 개발 속도가 매우 빨라진다&lt;/h2&gt;
&lt;p data-end=&quot;1385&quot; data-start=&quot;1359&quot; data-ke-size=&quot;size16&quot;&gt;버튼, 카드, 폼 등을 몇 줄로 만들 수 있다.&lt;/p&gt;
&lt;h2 data-end=&quot;1411&quot; data-start=&quot;1387&quot; data-ke-size=&quot;size26&quot;&gt;3. 기본적인 디자인 퀄리티가 보장된다&lt;/h2&gt;
&lt;p data-end=&quot;1435&quot; data-start=&quot;1412&quot; data-ke-size=&quot;size16&quot;&gt;완전히 깨진 화면이 나올 확률이 줄어든다.&lt;/p&gt;
&lt;h2 data-end=&quot;1457&quot; data-start=&quot;1437&quot; data-ke-size=&quot;size26&quot;&gt;4. 반응형 웹이 기본 지원된다&lt;/h2&gt;
&lt;p data-end=&quot;1483&quot; data-start=&quot;1458&quot; data-ke-size=&quot;size16&quot;&gt;화면 크기에 따라 자동으로 레이아웃이 바뀐다.&lt;/p&gt;
&lt;p data-end=&quot;1543&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;쉽다&amp;rdquo;는 것은&lt;br /&gt;&lt;b&gt;디자인 자체가 쉬운 것이 아니라,&lt;br /&gt;디자인 작업량이 줄어든다&lt;/b&gt;는 의미이다.&lt;/p&gt;
&lt;p data-end=&quot;1543&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1543&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;1572&quot; data-start=&quot;1550&quot;&gt;부트스트랩의 핵심 구조: 클래스 조합&lt;/h1&gt;
&lt;p data-end=&quot;1621&quot; data-start=&quot;1574&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 기본적으로&lt;br /&gt;&lt;b&gt;클래스 이름을 조합해서 디자인을 적용하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1639&quot; data-start=&quot;1623&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1785&quot; data-start=&quot;1641&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1662&quot; data-start=&quot;1641&quot;&gt;btn &amp;rarr; 버튼 기본 스타일&lt;/li&gt;
&lt;li data-end=&quot;1688&quot; data-start=&quot;1663&quot;&gt;btn-primary &amp;rarr; 파란 버튼&lt;/li&gt;
&lt;li data-end=&quot;1717&quot; data-start=&quot;1689&quot;&gt;form-control &amp;rarr; 입력창 스타일&lt;/li&gt;
&lt;li data-end=&quot;1736&quot; data-start=&quot;1718&quot;&gt;card &amp;rarr; 카드 UI&lt;/li&gt;
&lt;li data-end=&quot;1756&quot; data-start=&quot;1737&quot;&gt;table &amp;rarr; 표 스타일&lt;/li&gt;
&lt;li data-end=&quot;1785&quot; data-start=&quot;1757&quot;&gt;container &amp;rarr; 전체 레이아웃 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1788&quot; data-start=&quot;1787&quot; data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;blockquote data-end=&quot;1814&quot; data-start=&quot;1790&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1814&quot; data-start=&quot;1792&quot; data-ke-size=&quot;size16&quot;&gt;HTML + 클래스 조합 = 디자인 완성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1824&quot; data-start=&quot;1816&quot; data-ke-size=&quot;size16&quot;&gt;이라는 구조다.&lt;/p&gt;
&lt;p data-end=&quot;1824&quot; data-start=&quot;1816&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1824&quot; data-start=&quot;1816&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;1850&quot; data-start=&quot;1831&quot;&gt;카드 UI도 쉽게 만들 수 있다&lt;/h1&gt;
&lt;p data-end=&quot;1909&quot; data-start=&quot;1852&quot; data-ke-size=&quot;size16&quot;&gt;카드 UI는 일반적으로 만들기 꽤 번거로운 요소다.&lt;br /&gt;하지만 부트스트랩에서는 간단하게 만들 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1774920964048&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;card&quot; style=&quot;width: 18rem;&quot;&amp;gt;
  &amp;lt;div class=&quot;card-body&quot;&amp;gt;
    &amp;lt;h5 class=&quot;card-title&quot;&amp;gt;상품 제목&amp;lt;/h5&amp;gt;
    &amp;lt;p class=&quot;card-text&quot;&amp;gt;상품 설명이다&amp;lt;/p&amp;gt;
    &amp;lt;a href=&quot;#&quot; class=&quot;btn btn-primary&quot;&amp;gt;구매하기&amp;lt;/a&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2138&quot; data-start=&quot;2131&quot; data-ke-size=&quot;size16&quot;&gt;이 코드만으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2185&quot; data-start=&quot;2140&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2150&quot; data-start=&quot;2140&quot;&gt;카드 박스 생성&lt;/li&gt;
&lt;li data-end=&quot;2161&quot; data-start=&quot;2151&quot;&gt;내부 여백 정리&lt;/li&gt;
&lt;li data-end=&quot;2173&quot; data-start=&quot;2162&quot;&gt;텍스트 구조 정리&lt;/li&gt;
&lt;li data-end=&quot;2185&quot; data-start=&quot;2174&quot;&gt;버튼 스타일 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2200&quot; data-start=&quot;2187&quot; data-ke-size=&quot;size16&quot;&gt;까지 한 번에 해결된다.&lt;/p&gt;
&lt;p data-end=&quot;2200&quot; data-start=&quot;2187&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2200&quot; data-start=&quot;2187&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;2236&quot; data-start=&quot;2207&quot;&gt;레이아웃도 쉽게 만들 수 있다 (Grid 시스템)&lt;/h1&gt;
&lt;p data-end=&quot;2268&quot; data-start=&quot;2238&quot; data-ke-size=&quot;size16&quot;&gt;웹 개발에서 가장 어려운 부분 중 하나는 레이아웃이다.&lt;/p&gt;
&lt;p data-end=&quot;2275&quot; data-start=&quot;2270&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2324&quot; data-start=&quot;2277&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2293&quot; data-start=&quot;2277&quot;&gt;왼쪽 메뉴 / 오른쪽 본문&lt;/li&gt;
&lt;li data-end=&quot;2306&quot; data-start=&quot;2294&quot;&gt;카드 3개씩 한 줄&lt;/li&gt;
&lt;li data-end=&quot;2324&quot; data-start=&quot;2307&quot;&gt;화면 크기에 따라 자동 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2356&quot; data-start=&quot;2326&quot; data-ke-size=&quot;size16&quot;&gt;이런 것들을 직접 구현하려면 CSS를 꽤 알아야 한다.&lt;/p&gt;
&lt;p data-end=&quot;2402&quot; data-start=&quot;2358&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 이를 위해 &lt;b&gt;그리드 시스템(Grid System)&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774921298265&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;row&quot;&amp;gt;
  &amp;lt;div class=&quot;col-4&quot;&amp;gt;왼쪽&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;col-8&quot;&amp;gt;오른쪽&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2545&quot; data-start=&quot;2503&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 한 줄을 12칸으로 나누어&lt;br /&gt;쉽게 레이아웃을 구성할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2545&quot; data-start=&quot;2503&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2545&quot; data-start=&quot;2503&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;2570&quot; data-start=&quot;2552&quot;&gt;반응형 웹도 자동으로 지원된다&lt;/h1&gt;
&lt;p data-end=&quot;2618&quot; data-start=&quot;2572&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩의 큰 장점 중 하나는&lt;br /&gt;&lt;b&gt;반응형 웹이 기본으로 지원된다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;2626&quot; data-start=&quot;2620&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre id=&quot;code_1774921408829&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;col-md-6 col-lg-4&quot;&amp;gt;
    내용
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 작성하면&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2736&quot; data-start=&quot;2695&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2713&quot; data-start=&quot;2695&quot;&gt;모바일에서는 한 줄 전체 사용&lt;/li&gt;
&lt;li data-end=&quot;2725&quot; data-start=&quot;2714&quot;&gt;태블릿에서는 2칸&lt;/li&gt;
&lt;li data-end=&quot;2736&quot; data-start=&quot;2726&quot;&gt;PC에서는 3칸&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2761&quot; data-start=&quot;2738&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 자동으로 레이아웃이 변경된다.&lt;/p&gt;
&lt;p data-end=&quot;2792&quot; data-start=&quot;2763&quot; data-ke-size=&quot;size16&quot;&gt;즉 복잡한 미디어 쿼리를 직접 작성하지 않아도 된다.&lt;/p&gt;
&lt;p data-end=&quot;2792&quot; data-start=&quot;2763&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2792&quot; data-start=&quot;2763&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;2810&quot; data-start=&quot;2799&quot;&gt;부트스트랩의 한계&lt;/h1&gt;
&lt;p data-end=&quot;2834&quot; data-start=&quot;2812&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 매우 편하지만 단점도 있다.&lt;/p&gt;
&lt;h2 data-end=&quot;2856&quot; data-start=&quot;2836&quot; data-ke-size=&quot;size26&quot;&gt;1. 디자인이 비슷해질 수 있다&lt;/h2&gt;
&lt;p data-end=&quot;2878&quot; data-start=&quot;2857&quot; data-ke-size=&quot;size16&quot;&gt;많은 사이트가 같은 스타일을 사용한다.&lt;/p&gt;
&lt;h2 data-end=&quot;2904&quot; data-start=&quot;2880&quot; data-ke-size=&quot;size26&quot;&gt;2. 개성 있는 디자인에는 한계가 있다&lt;/h2&gt;
&lt;p data-end=&quot;2930&quot; data-start=&quot;2905&quot; data-ke-size=&quot;size16&quot;&gt;세밀한 커스터마이징은 결국 CSS가 필요하다.&lt;/p&gt;
&lt;h2 data-end=&quot;2956&quot; data-start=&quot;2932&quot; data-ke-size=&quot;size26&quot;&gt;3. &amp;ldquo;부트스트랩 느낌&amp;rdquo;이 날 수 있다&lt;/h2&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2957&quot; data-ke-size=&quot;size16&quot;&gt;기본 스타일을 그대로 쓰면 티가 난다.&lt;/p&gt;
&lt;p data-end=&quot;2989&quot; data-start=&quot;2980&quot; data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3043&quot; data-start=&quot;2991&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3017&quot; data-start=&quot;2991&quot;&gt;기본 구조는 부트스트랩으로 빠르게 만들고&lt;/li&gt;
&lt;li data-end=&quot;3043&quot; data-start=&quot;3018&quot;&gt;필요한 부분만 직접 CSS로 수정하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3055&quot; data-start=&quot;3045&quot; data-ke-size=&quot;size16&quot;&gt;을 많이 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;3055&quot; data-start=&quot;3045&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3055&quot; data-start=&quot;3045&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3078&quot; data-start=&quot;3062&quot;&gt;초보자에게 특히 좋은 이유&lt;/h1&gt;
&lt;p data-end=&quot;3101&quot; data-start=&quot;3080&quot; data-ke-size=&quot;size16&quot;&gt;초보자는 보통 CSS에서 많이 막힌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3131&quot; data-start=&quot;3103&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3107&quot; data-start=&quot;3103&quot;&gt;정렬&lt;/li&gt;
&lt;li data-end=&quot;3112&quot; data-start=&quot;3108&quot;&gt;여백&lt;/li&gt;
&lt;li data-end=&quot;3117&quot; data-start=&quot;3113&quot;&gt;크기&lt;/li&gt;
&lt;li data-end=&quot;3123&quot; data-start=&quot;3118&quot;&gt;반응형&lt;/li&gt;
&lt;li data-end=&quot;3131&quot; data-start=&quot;3124&quot;&gt;위치 잡기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3149&quot; data-start=&quot;3133&quot; data-ke-size=&quot;size16&quot;&gt;이런 부분이 어렵기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;3169&quot; data-start=&quot;3151&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 이 부담을 줄여준다.&lt;/p&gt;
&lt;p data-end=&quot;3177&quot; data-start=&quot;3171&quot; data-ke-size=&quot;size16&quot;&gt;즉 초보자는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3239&quot; data-start=&quot;3179&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3196&quot; data-start=&quot;3179&quot;&gt;HTML 구조를 만든다&lt;/li&gt;
&lt;li data-end=&quot;3216&quot; data-start=&quot;3197&quot;&gt;부트스트랩 클래스를 붙인다&lt;/li&gt;
&lt;li data-end=&quot;3239&quot; data-start=&quot;3217&quot;&gt;어느 정도 완성된 UI를 얻는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;3257&quot; data-start=&quot;3241&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름으로 개발할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3257&quot; data-start=&quot;3241&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3257&quot; data-start=&quot;3241&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3274&quot; data-start=&quot;3264&quot;&gt;비유로 이해하기&lt;/h1&gt;
&lt;p data-end=&quot;3321&quot; data-start=&quot;3276&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩이 없는 경우는&lt;br /&gt;나무와 도구를 가지고 직접 가구를 만드는 것과 같다.&lt;/p&gt;
&lt;p data-end=&quot;3365&quot; data-start=&quot;3323&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩이 있는 경우는&lt;br /&gt;이미 만들어진 가구 부품을 조립하는 것과 같다.&lt;/p&gt;
&lt;p data-end=&quot;3406&quot; data-start=&quot;3367&quot; data-ke-size=&quot;size16&quot;&gt;즉 완전히 자동은 아니지만&lt;br /&gt;훨씬 빠르고 쉽게 결과를 만들 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3406&quot; data-start=&quot;3367&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3406&quot; data-start=&quot;3367&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3417&quot; data-start=&quot;3413&quot;&gt;정리&lt;/h1&gt;
&lt;p data-end=&quot;3523&quot; data-start=&quot;3419&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 웹에서 자주 사용하는 UI 요소들을 미리 만들어 놓은 프레임워크이다.&lt;br /&gt;개발자는 CSS를 처음부터 작성하지 않고, 준비된 클래스들을 조합해서 화면을 빠르게 구성할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3596&quot; data-start=&quot;3525&quot; data-ke-size=&quot;size16&quot;&gt;그래서 부트스트랩은 디자인을 대신 해주는 도구라기보다,&lt;br /&gt;&lt;b&gt;디자인 작업을 크게 줄여주는 도구&lt;/b&gt;라고 이해하는 것이 정확하다.&lt;/p&gt;
&lt;p data-end=&quot;3596&quot; data-start=&quot;3525&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3596&quot; data-start=&quot;3525&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3611&quot; data-start=&quot;3603&quot;&gt;한 줄 정리&lt;/h1&gt;
&lt;p data-end=&quot;3710&quot; data-start=&quot;3613&quot; data-ke-size=&quot;size16&quot;&gt;부트스트랩은 웹 UI를 구성할 때 자주 쓰는 디자인과 레이아웃을 미리 만들어 둔 프레임워크라서, CSS를 직접 다 작성하지 않아도 빠르고 쉽게 화면을 구성할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/122</guid>
      <comments>https://woojoo-devlog.tistory.com/122#entry122comment</comments>
      <pubDate>Tue, 31 Mar 2026 10:44:38 +0900</pubDate>
    </item>
    <item>
      <title>스프링 빈(Bean) 등록 방식 정리</title>
      <link>https://woojoo-devlog.tistory.com/121</link>
      <description>&lt;p data-end=&quot;197&quot; data-start=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 가장 중요한 개념 중 하나는 &lt;b&gt;빈(Bean)&lt;/b&gt;이다.&lt;br /&gt;빈은 단순한 자바 객체가 아니라, &lt;b&gt;스프링 컨테이너가 생성하고 관리하는 객체&lt;/b&gt;를 의미한다.&lt;/p&gt;
&lt;p data-end=&quot;249&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;즉, 객체는 그냥 new로 만든 것이고,&lt;br /&gt;빈은 스프링이 대신 만들어서 관리하는 객체다.&lt;/p&gt;
&lt;p data-end=&quot;249&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;270&quot; data-start=&quot;256&quot;&gt;빈은 어떻게 등록하는가&lt;/h1&gt;
&lt;p data-end=&quot;321&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 빈을 등록하는 방법은 하나가 아니다.&lt;br /&gt;대표적으로 다음 3가지 방식이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;388&quot; data-start=&quot;323&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;343&quot; data-start=&quot;323&quot;&gt;컴포넌트 스캔 (자동 등록)&lt;/li&gt;
&lt;li data-end=&quot;367&quot; data-start=&quot;344&quot;&gt;@Bean을 이용한 수동 등록&lt;/li&gt;
&lt;li data-end=&quot;388&quot; data-start=&quot;368&quot;&gt;XML &amp;lt;bean&amp;gt; 등록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;404&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;중요한 포인트는 이것이다.&lt;/p&gt;
&lt;blockquote data-end=&quot;465&quot; data-start=&quot;406&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;465&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;모든 빈을 하나의 방식으로 통일하는 것이 아니라,&lt;br /&gt;상황에 따라 적절한 방식을 선택해서 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;492&quot; data-start=&quot;472&quot;&gt;컴포넌트 스캔 (자동 등록)&lt;/h1&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;가장 많이 사용하는 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774914887520&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TodoService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774914897608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class TodoRepository {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774914906252&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class TodoController {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;685&quot; data-start=&quot;677&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 클래스에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;746&quot; data-start=&quot;687&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;701&quot; data-start=&quot;687&quot;&gt;@Component&lt;/li&gt;
&lt;li data-end=&quot;714&quot; data-start=&quot;702&quot;&gt;@Service&lt;/li&gt;
&lt;li data-end=&quot;730&quot; data-start=&quot;715&quot;&gt;@Repository&lt;/li&gt;
&lt;li data-end=&quot;746&quot; data-start=&quot;731&quot;&gt;@Controller&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;802&quot; data-start=&quot;748&quot; data-ke-size=&quot;size16&quot;&gt;같은 어노테이션을 붙이면,&lt;br /&gt;스프링이 시작될 때 해당 클래스를 자동으로 찾아서 빈으로 등록한다.&lt;/p&gt;
&lt;p data-end=&quot;828&quot; data-start=&quot;804&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 &lt;b&gt;컴포넌트 스캔&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;h2 data-end=&quot;840&quot; data-start=&quot;835&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;916&quot; data-start=&quot;842&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;868&quot; data-start=&quot;842&quot;&gt;개발자가 직접 빈을 등록하지 않아도 된다&lt;/li&gt;
&lt;li data-end=&quot;891&quot; data-start=&quot;869&quot;&gt;클래스에 역할이 명확하게 드러난다&lt;/li&gt;
&lt;li data-end=&quot;916&quot; data-start=&quot;892&quot;&gt;프로젝트 규모가 커져도 관리가 편하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;934&quot; data-start=&quot;923&quot; data-ke-size=&quot;size26&quot;&gt;언제 사용하는가&lt;/h2&gt;
&lt;p data-end=&quot;968&quot; data-start=&quot;936&quot; data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔은 &lt;b&gt;내가 직접 만든 클래스&lt;/b&gt;에 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;972&quot; data-start=&quot;970&quot; data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1028&quot; data-start=&quot;974&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;985&quot; data-start=&quot;974&quot;&gt;서비스 클래스&lt;/li&gt;
&lt;li data-end=&quot;999&quot; data-start=&quot;986&quot;&gt;리포지토리 클래스&lt;/li&gt;
&lt;li data-end=&quot;1012&quot; data-start=&quot;1000&quot;&gt;컨트롤러 클래스&lt;/li&gt;
&lt;li data-end=&quot;1028&quot; data-start=&quot;1013&quot;&gt;비즈니스 로직 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1076&quot; data-start=&quot;1030&quot; data-ke-size=&quot;size16&quot;&gt;이유는 간단하다.&lt;br /&gt;&lt;b&gt;내 코드이기 때문에 어노테이션을 직접 붙일 수 있기 때문&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1076&quot; data-start=&quot;1030&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1076&quot; data-start=&quot;1030&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;1103&quot; data-start=&quot;1083&quot;&gt;@Bean으로 수동 등록&lt;/h1&gt;
&lt;p data-end=&quot;1138&quot; data-start=&quot;1105&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 자동이 아니라 &lt;b&gt;직접 빈을 등록하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774914978071&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
  	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1298&quot; data-start=&quot;1285&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 다음 의미다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1336&quot; data-start=&quot;1300&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1336&quot; data-start=&quot;1302&quot; data-ke-size=&quot;size16&quot;&gt;&quot;이 객체는 내가 직접 생성하니까, 스프링이 빈으로 등록해라&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1383&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1383&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size16&quot;&gt;즉 @Bean은&lt;br /&gt;&lt;b&gt;메서드가 반환하는 객체를 빈으로 등록하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-end=&quot;1395&quot; data-start=&quot;1390&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1467&quot; data-start=&quot;1397&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1423&quot; data-start=&quot;1397&quot;&gt;객체 생성 과정을 개발자가 직접 제어한다&lt;/li&gt;
&lt;li data-end=&quot;1442&quot; data-start=&quot;1424&quot;&gt;세부 설정을 넣을 수 있다&lt;/li&gt;
&lt;li data-end=&quot;1467&quot; data-start=&quot;1443&quot;&gt;외부 라이브러리 객체 등록에 유리하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;1485&quot; data-start=&quot;1474&quot; data-ke-size=&quot;size26&quot;&gt;언제 사용하는가&lt;/h2&gt;
&lt;h3 data-end=&quot;1519&quot; data-start=&quot;1501&quot; data-ke-size=&quot;size23&quot;&gt;1. 외부 라이브러리 객체&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774915080495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ModelMapper, ObjectMapper, PasswordEncoder&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1633&quot; data-start=&quot;1576&quot; data-ke-size=&quot;size16&quot;&gt;이런 클래스는 내가 만든 것이 아니다.&lt;br /&gt;따라서 @Service 같은 어노테이션을 붙일 수 없다.&lt;/p&gt;
&lt;h3 data-end=&quot;1660&quot; data-start=&quot;1640&quot; data-ke-size=&quot;size23&quot;&gt;2. 생성 과정이 복잡한 객체&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774915106653&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ObjectMapper objectMapper() {
    ObjectMapper om = new ObjectMapper();
    om.findAndRegisterModules();
    return om;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1856&quot; data-start=&quot;1810&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 단순 new가 아니라&lt;br /&gt;추가 설정이 필요한 경우 @Bean이 적합하다.&lt;/p&gt;
&lt;h3 data-end=&quot;1891&quot; data-start=&quot;1863&quot; data-ke-size=&quot;size23&quot;&gt;3. 생성 방식을 명확히 제어하고 싶은 경우&lt;/h3&gt;
&lt;p data-end=&quot;1927&quot; data-start=&quot;1893&quot; data-ke-size=&quot;size16&quot;&gt;객체 생성 로직을 코드로 명확하게 드러내고 싶을 때 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;1927&quot; data-start=&quot;1893&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1927&quot; data-start=&quot;1893&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;1954&quot; data-start=&quot;1934&quot;&gt;XML &amp;lt;bean&amp;gt; 등록&lt;/h1&gt;
&lt;p data-end=&quot;1966&quot; data-start=&quot;1956&quot; data-ke-size=&quot;size16&quot;&gt;전통적인 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774915146423&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;bean id=&quot;dataSource&quot; class=&quot;com.zaxxer.hikari.HikariDataSource&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2072&quot; data-start=&quot;2048&quot; data-ke-size=&quot;size16&quot;&gt;XML 파일에 직접 빈을 등록하는 방식이다.&lt;/p&gt;
&lt;h2 data-end=&quot;2084&quot; data-start=&quot;2079&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2172&quot; data-start=&quot;2086&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2117&quot; data-start=&quot;2086&quot;&gt;어노테이션이나 자바 코드가 아닌 XML로 설정한다&lt;/li&gt;
&lt;li data-end=&quot;2143&quot; data-start=&quot;2118&quot;&gt;예전 스프링 프로젝트에서 많이 사용했다&lt;/li&gt;
&lt;li data-end=&quot;2172&quot; data-start=&quot;2144&quot;&gt;현재는 레거시 프로젝트에서 주로 볼 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;2190&quot; data-start=&quot;2179&quot; data-ke-size=&quot;size26&quot;&gt;언제 사용하는가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2277&quot; data-start=&quot;2192&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2214&quot; data-start=&quot;2192&quot;&gt;기존 프로젝트가 XML 기반일 때&lt;/li&gt;
&lt;li data-end=&quot;2255&quot; data-start=&quot;2215&quot;&gt;MyBatis, DataSource 같은 설정이 XML에 있을 때&lt;/li&gt;
&lt;li data-end=&quot;2277&quot; data-start=&quot;2256&quot;&gt;레거시 코드 유지가 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;2306&quot; data-start=&quot;2284&quot;&gt;왜 세 가지 방식을 섞어서 사용하는가&lt;/h1&gt;
&lt;p data-end=&quot;2317&quot; data-start=&quot;2308&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 이것이다.&lt;/p&gt;
&lt;blockquote data-end=&quot;2345&quot; data-start=&quot;2319&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2345&quot; data-start=&quot;2321&quot; data-ke-size=&quot;size16&quot;&gt;각 방식이 잘 맞는 대상이 다르기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2368&quot; data-start=&quot;2352&quot; data-ke-size=&quot;size26&quot;&gt;자동 스캔이 적합한 경우&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774917473818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TodoService&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2459&quot; data-start=&quot;2417&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2430&quot; data-start=&quot;2417&quot;&gt;내가 만든 클래스&lt;/li&gt;
&lt;li data-end=&quot;2442&quot; data-start=&quot;2431&quot;&gt;역할이 명확함&lt;/li&gt;
&lt;li data-end=&quot;2459&quot; data-start=&quot;2443&quot;&gt;특별한 생성 로직 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;2484&quot; data-start=&quot;2466&quot; data-ke-size=&quot;size26&quot;&gt;@Bean이 적합한 경우&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774917483750&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ModelMapper modelMapper()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2572&quot; data-start=&quot;2538&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2553&quot; data-start=&quot;2538&quot;&gt;외부 라이브러리 객체&lt;/li&gt;
&lt;li data-end=&quot;2572&quot; data-start=&quot;2554&quot;&gt;생성 과정에 설정이 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;2593&quot; data-start=&quot;2579&quot; data-ke-size=&quot;size26&quot;&gt;XML이 적합한 경우&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774917520883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;bean id=&quot;dataSource&quot; ... /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2676&quot; data-start=&quot;2636&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2650&quot; data-start=&quot;2636&quot;&gt;기존 프로젝트 설정&lt;/li&gt;
&lt;li data-end=&quot;2676&quot; data-start=&quot;2651&quot;&gt;DB, MyBatis 같은 인프라 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;2711&quot; data-start=&quot;2683&quot;&gt;모든 빈을 @Bean으로 등록하면 안 되나?&lt;/h1&gt;
&lt;p data-end=&quot;2730&quot; data-start=&quot;2713&quot; data-ke-size=&quot;size16&quot;&gt;가능하다. 하지만 비효율적이다.&lt;/p&gt;
&lt;p data-end=&quot;2734&quot; data-start=&quot;2732&quot; data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774917547834&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public TodoService todoService() {
	return new TodoService();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2847&quot; data-start=&quot;2822&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 모든 클래스를 등록할 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;2864&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;하지만 문제는 다음과 같다.&lt;/p&gt;
&lt;p data-end=&quot;2864&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2864&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;참고로 @Bean은 해당 메소드의 실행 결과로 반환된 객체를 스프링의 빈(Bean)으로 등록시키는 역할을 한다.&lt;/p&gt;
&lt;p data-end=&quot;2864&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2877&quot; data-start=&quot;2871&quot; data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;h3 data-end=&quot;2897&quot; data-start=&quot;2879&quot; data-ke-size=&quot;size23&quot;&gt;1. 코드가 너무 많아진다&lt;/h3&gt;
&lt;p data-end=&quot;2927&quot; data-start=&quot;2898&quot; data-ke-size=&quot;size16&quot;&gt;클래스가 많아질수록 설정 코드가 폭발적으로 증가한다.&lt;/p&gt;
&lt;h3 data-end=&quot;2950&quot; data-start=&quot;2934&quot; data-ke-size=&quot;size23&quot;&gt;2. 가독성이 떨어진다&lt;/h3&gt;
&lt;p data-end=&quot;2973&quot; data-start=&quot;2951&quot; data-ke-size=&quot;size16&quot;&gt;클래스만 봐서는 역할이 드러나지 않는다.&lt;/p&gt;
&lt;h3 data-end=&quot;2997&quot; data-start=&quot;2980&quot; data-ke-size=&quot;size23&quot;&gt;3. 대부분의 방식과 다르다&lt;/h3&gt;
&lt;p data-end=&quot;3025&quot; data-start=&quot;2998&quot; data-ke-size=&quot;size16&quot;&gt;실무에서는 대부분 자동 스캔을 기본으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3042&quot; data-start=&quot;3032&quot;&gt;정리&lt;/h1&gt;
&lt;p data-end=&quot;3065&quot; data-start=&quot;3044&quot; data-ke-size=&quot;size16&quot;&gt;실무에서는 보통 다음 기준으로 나눈다.&lt;/p&gt;
&lt;p data-end=&quot;3065&quot; data-start=&quot;3044&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3082&quot; data-start=&quot;3072&quot; data-ke-size=&quot;size26&quot;&gt;컴포넌트 스캔&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3125&quot; data-start=&quot;3084&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3098&quot; data-start=&quot;3084&quot;&gt;Controller&lt;/li&gt;
&lt;li data-end=&quot;3110&quot; data-start=&quot;3099&quot;&gt;Service&lt;/li&gt;
&lt;li data-end=&quot;3125&quot; data-start=&quot;3111&quot;&gt;Repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3144&quot; data-start=&quot;3127&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;비즈니스 로직 클래스&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;3161&quot; data-start=&quot;3151&quot; data-ke-size=&quot;size26&quot;&gt;@Bean&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3215&quot; data-start=&quot;3163&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3178&quot; data-start=&quot;3163&quot;&gt;ModelMapper&lt;/li&gt;
&lt;li data-end=&quot;3195&quot; data-start=&quot;3179&quot;&gt;ObjectMapper&lt;/li&gt;
&lt;li data-end=&quot;3215&quot; data-start=&quot;3196&quot;&gt;PasswordEncoder&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3239&quot; data-start=&quot;3217&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;외부 라이브러리 + 설정 객체&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;3252&quot; data-start=&quot;3246&quot; data-ke-size=&quot;size26&quot;&gt;XML&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3290&quot; data-start=&quot;3254&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3268&quot; data-start=&quot;3254&quot;&gt;DataSource&lt;/li&gt;
&lt;li data-end=&quot;3290&quot; data-start=&quot;3269&quot;&gt;SqlSessionFactory&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3310&quot; data-start=&quot;3292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;레거시 / 인프라 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3310&quot; data-start=&quot;3292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3310&quot; data-start=&quot;3292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3324&quot; data-start=&quot;3317&quot;&gt;최종 결론&lt;/h1&gt;
&lt;p data-end=&quot;3357&quot; data-start=&quot;3326&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서는 모든 빈을 하나의 방식으로 등록하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3443&quot; data-start=&quot;3359&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3390&quot; data-start=&quot;3359&quot;&gt;자동 등록이 가능한 것은 컴포넌트 스캔을 사용한다&lt;/li&gt;
&lt;li data-end=&quot;3422&quot; data-start=&quot;3391&quot;&gt;직접 설정이 필요한 것은 @Bean을 사용한다&lt;/li&gt;
&lt;li data-end=&quot;3443&quot; data-start=&quot;3423&quot;&gt;기존 설정은 XML을 유지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3445&quot; data-ke-size=&quot;size16&quot;&gt;즉 중요한 것은 방식이 아니라,&lt;br /&gt;&lt;b&gt;객체의 성격에 맞는 등록 방법을 선택하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3496&quot; data-start=&quot;3445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;3511&quot; data-start=&quot;3503&quot;&gt;정리&lt;/h1&gt;
&lt;h1 data-end=&quot;3511&quot; data-start=&quot;3503&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;스프링 빈 등록은 하나로 통일하는 것이 아니라,&lt;/span&gt;&lt;/h1&gt;
&lt;p data-end=&quot;3588&quot; data-start=&quot;3515&quot; data-ke-size=&quot;size16&quot;&gt;자동 스캔, @Bean, XML을 상황에 맞게 나누어 사용하는 구조이다.&lt;/p&gt;</description>
      <category>Backend/Spring</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/121</guid>
      <comments>https://woojoo-devlog.tistory.com/121#entry121comment</comments>
      <pubDate>Tue, 31 Mar 2026 09:43:04 +0900</pubDate>
    </item>
    <item>
      <title>[스프링과 스프링 MVC]  4. LocalDate 바인딩이 왜 자주 깨지는가</title>
      <link>https://woojoo-devlog.tistory.com/120</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Formatter로 &amp;ldquo;문자열 &amp;rarr; 타입&amp;rdquo; 변환 규칙을 등록한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC의 파라미터 바인딩은 편하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 파라미터가 문자열로 들어와도&lt;/li&gt;
&lt;li&gt;int/boolean 같은 기본 타입은 자동 변환해주고&lt;/li&gt;
&lt;li&gt;DTO 필드에도 알아서 채워준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실전에서 가장 자주 터지는 타입이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날짜(LocalDate, LocalDateTime)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 왜 날짜에서 에러가 자주 나나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청 파라미터는 기본적으로 전부 문자열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 폼에서 날짜를 입력하면 이런 문자열이 온다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dueDate=2020-10-10&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 스프링이 이 문자열을&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LocalDate 객체로 바꾸는 규칙을 &amp;ldquo;항상 자동으로 알 수는 없다&amp;rdquo;는 점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 바인딩 과정에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;String을 LocalDate로 변환할 수 없다&amp;rdquo;&lt;/li&gt;
&lt;li&gt;같은 타입 변환 에러가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774876786872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [문제 상황] 스프링 MVC가 &quot;문자열&quot;을 LocalDate로 자동 변환 못 해서 에러가 나는 케이스
// - 요청: dueDate=2020-10-10
// - DTO: LocalDate dueDate
// ================================

@Data
public class TodoDTO {
    private String title;
    private LocalDate dueDate; // ✅ 여기서 바인딩 문제가 자주 난다
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 해결 방법 1: @DateTimeFormat&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 필드에 날짜 형식을 명시하면 된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)
private LocalDate dueDate;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 스프링이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yyyy-MM-dd 패턴으로 문자열을 파싱해서 LocalDate로 변환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 특정 필드/파라미터에 대해 &amp;ldquo;형식&amp;rdquo;을 선언하는 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 해결 방법 2: Formatter 등록 (전역 규칙을 만드는 방식)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@DateTimeFormat`은 간단하지만&lt;br /&gt;프로젝트 전체에서 동일한 규칙을 강제하거나, 커스텀 규칙이 더 필요하면 Formatter를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Formatter는 한 문장으로 말하면 이거다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 &amp;harr; 특정 타입 변환 규칙을 코드로 등록하는 장치&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Formatter는 두 메서드를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;parse(String text, Locale locale) : 문자열 &amp;rarr; 타입&lt;/li&gt;
&lt;li&gt;print(T object, Locale locale) : 타입 &amp;rarr; 문자열&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDateFormatter는 이런 식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2020-10-10 &amp;rarr; LocalDate.of(2020,10,10)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) Formatter는 어디에 등록하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서는 보통 `WebMvcConfigurer`에서 등록한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new LocalDateFormatter());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 등록하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 전체에서 LocalDate 바인딩에 이 규칙이 적용될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;전역 타입 변환 규칙&amp;rdquo;을 등록하는 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 언제 무엇을 쓰는가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르게 해결: @DateTimeFormat&lt;/li&gt;
&lt;li&gt;프로젝트 전체 규칙/커스텀 변환 필요: Formatter 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 결국 같은 문제를 해결한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;문자열로 들어온 요청 데이터를, 원하는 타입으로 변환하는 규칙&amp;rdquo;을 제공한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 요청 파라미터는 기본적으로 문자열이다&lt;/li&gt;
&lt;li&gt;LocalDate 같은 타입은 문자열 &amp;rarr; 타입 변환 규칙이 필요해서 바인딩 에러가 자주 난다&lt;/li&gt;
&lt;li&gt;@DateTimeFormat은 필드 단위로 형식을 지정하는 쉬운 해결책이다&lt;/li&gt;
&lt;li&gt;Formatter는 문자열 &amp;harr; 타입 변환 규칙을 전역으로 등록하는 방식이다&lt;/li&gt;
&lt;li&gt;Formatter는 WebMvcConfigurer의 addFormatters로 등록한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/120</guid>
      <comments>https://woojoo-devlog.tistory.com/120#entry120comment</comments>
      <pubDate>Tue, 31 Mar 2026 00:09:13 +0900</pubDate>
    </item>
    <item>
      <title>[스프링과 스프링 MVC]  3. 파라미터 바인딩과 Model</title>
      <link>https://woojoo-devlog.tistory.com/119</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 MVC는 request를 직접 만지지 않아도 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 MVC에서는 요청 데이터를 읽는 방식이 명확했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getParameter(&quot;title&quot;)&lt;/li&gt;
&lt;li&gt;request.setAttribute(&quot;todos&quot;, list)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request를 직접 만지는 코드가 컨트롤러에 반복된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 이 부분을 크게 추상화했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 파라미터를 자동으로 모아서 객체(DTO)에 넣어준다&lt;/li&gt;
&lt;li&gt;View로 넘길 데이터도 Model로 넣으면 알아서 request에 옮겨준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러 코드가 짧아지고 의도가 더 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 파라미터 바인딩: DTO를 파라미터로 받으면 자동으로 채워준다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 컨트롤러 메서드 파라미터를 보고&lt;br /&gt;요청 파라미터를 자동으로 수집해서 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 요청이 이렇게 들어온다고 하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;title=study&lt;/li&gt;
&lt;li&gt;finished=true&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 DTO를 파라미터로 받으면&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;public String register(TodoDTO dto)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 내부적으로 대략 이런 작업을 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TodoDTO 객체를 생성한다&lt;/li&gt;
&lt;li&gt;요청 파라미터 이름을 보고 필드에 매칭한다&lt;/li&gt;
&lt;li&gt;setter를 호출해서 값을 채운다&lt;/li&gt;
&lt;li&gt;필요하면 타입 변환도 시도한다(String &amp;rarr; int/boolean 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;DTO 하나로 요청 데이터 수집이 끝나는 구조&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774858588053&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [파라미터 바인딩] 요청 파라미터를 DTO로 자동 수집
// - 요청 파라미터 이름과 DTO 필드명이 매칭되면 자동으로 setXXX를 호출해 채운다
// ================================

@Data
public class TodoDTO {
    private String title;
    private boolean finished;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 바인딩이 되려면 조건이 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터 바인딩이 자연스럽게 되려면 조건이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파라미터 이름과 DTO 필드명이 맞아야 한다&lt;/li&gt;
&lt;li&gt;DTO에 기본 생성자가 있어야 한다(스프링이 객체를 만들어야 함)&lt;/li&gt;
&lt;li&gt;setter가 있어야 한다(setXXX로 채우기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DTO는 보통 JavaBeans 형태로 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;private 필드&lt;/li&gt;
&lt;li&gt;getter/setter&lt;/li&gt;
&lt;li&gt;기본 생성자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lombok을 쓰면 @Data나 @Getter/@Setter로 쉽게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774858662050&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [컨트롤러] DTO 파라미터로 받기 + Model로 View에 전달
// ================================

@Controller
@RequestMapping(&quot;/todo&quot;)
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService;

    @PostMapping(&quot;/register&quot;)
    public String register(TodoDTO dto) { // ✅ 자동 바인딩
        todoService.register(dto);
        return &quot;redirect:/todo/list&quot;;
    }

    @GetMapping(&quot;/list&quot;)
    public String list(Model model) {     // ✅ Model로 전달
        model.addAttribute(&quot;todos&quot;, todoService.list());
        return &quot;todo/list&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) @RequestParam: 파라미터 누락/기본값 처리가 필요할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC의 바인딩은 편하지만,&lt;br /&gt;파라미터가 누락되면 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 page는 없으면 1로 처리하고 싶을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 @RequestParam을 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;defaultValue: 없을 때 기본값&lt;/li&gt;
&lt;li&gt;required: 필수 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 @RequestParam은 &amp;ldquo;파라미터 수집의 정책&amp;rdquo;을 명시하는 도구이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774858700399&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [RequestParam] 파라미터가 없을 때 기본값 지정/필수 여부 제어
// ================================

@GetMapping(&quot;/search&quot;)
public String search(
        @RequestParam(defaultValue = &quot;1&quot;) int page,
        @RequestParam(required = false) String keyword
) {
    // page는 없으면 1
    // keyword는 없으면 null
    return &quot;todo/search&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) Model: View로 넘길 데이터를 담는 상자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 MVC에서는 View로 데이터를 넘길 때 request에 담았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.setAttribute(&quot;todos&quot;, list)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서는 Model을 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;model.addAttribute(&quot;todos&quot;, list)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 포인트는 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EL이 Model 객체 자체를 읽는 게 아니다&lt;br /&gt;Model에 담긴 attribute가 request 영역으로 옮겨지고, JSP는 그 값을 읽는다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 스프링 MVC는 내부적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model에 담긴 값을 request attribute로 옮겨준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JSP에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;${todos}&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바로 읽힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 왜 컨트롤러에서 request/response를 덜 만지는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 DispatcherServlet이 요청/응답의 많은 처리를 표준화하고,&lt;br /&gt;컨트롤러는 &amp;ldquo;비즈니스 의도&amp;rdquo;만 표현하게 만드는 게 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러는 보통 이런 코드가 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO로 입력 받기&lt;/li&gt;
&lt;li&gt;Service 호출&lt;/li&gt;
&lt;li&gt;Model에 넣기&lt;/li&gt;
&lt;li&gt;view 이름 또는 redirect 반환하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 유지보수성과 생산성을 크게 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 MVC는 요청 파라미터를 DTO로 자동 바인딩해준다&lt;/li&gt;
&lt;li&gt;바인딩은 이름 매칭 + 기본 생성자 + setter가 필요하다&lt;/li&gt;
&lt;li&gt;@RequestParam으로 기본값/필수 여부를 명시할 수 있다&lt;/li&gt;
&lt;li&gt;Model에 담으면 스프링이 request attribute로 옮겨서 JSP EL이 읽을 수 있게 한다&lt;/li&gt;
&lt;li&gt;그래서 컨트롤러가 request/response를 직접 만지는 코드가 줄어든다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/119</guid>
      <comments>https://woojoo-devlog.tistory.com/119#entry119comment</comments>
      <pubDate>Mon, 30 Mar 2026 17:36:27 +0900</pubDate>
    </item>
    <item>
      <title>[스프링과 스프링 MVC]  2. 스프링 MVC는 &amp;ldquo;프론트 컨트롤러&amp;rdquo;로 움직인다</title>
      <link>https://woojoo-devlog.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 MVC에서는 보통 이런 흐름이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL마다 서블릿이 매핑된다&lt;/li&gt;
&lt;li&gt;서블릿이 doGet/doPost에서 요청을 처리한다&lt;/li&gt;
&lt;li&gt;필요하면 JSP로 forward/redirect 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 이 흐름을 더 강하게 &amp;ldquo;통제&amp;rdquo;한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서는 모든 요청이 반드시 DispatcherServlet을 거친다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 스프링 MVC의 프론트 컨트롤러(Front-Controller)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;모든 요청의 입구&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774855032576&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [Controller 예시] @GetMapping / @PostMapping
// - 서블릿의 doGet/doPost 대신 &quot;메서드 단위&quot;로 매핑한다
// ================================

@Controller
@RequestMapping(&quot;/todo&quot;)
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService;

    // GET /todo/list
    @GetMapping(&quot;/list&quot;)
    public String list(Model model) {
        model.addAttribute(&quot;todos&quot;, todoService.list());
        return &quot;todo/list&quot;; // 뷰 이름(예: /WEB-INF/views/todo/list.jsp)
    }

    // GET /todo/register
    @GetMapping(&quot;/register&quot;)
    public String registerForm() {
        return &quot;todo/register&quot;;
    }

    // POST /todo/register
    @PostMapping(&quot;/register&quot;)
    public String register(TodoDTO dto) { // 파라미터 바인딩(자동 수집)
        todoService.register(dto);
        return &quot;redirect:/todo/list&quot;; // PRG
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) DispatcherServlet은 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 한 문장으로 말하면 이거다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 받아서, 어떤 컨트롤러가 처리할지 결정하고, 실행하고, 응답까지 연결하는 중앙 관제탑&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 스프링 MVC는 공통 처리(인터셉터, 예외 처리, 바인딩 등)를&lt;br /&gt;중앙에서 일관되게 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 서블릿의 &amp;ldquo;서블릿 단위&amp;rdquo; 매핑이, 스프링에서는 &amp;ldquo;메서드 단위&amp;rdquo; 매핑으로 바뀐다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 방식에서는 보통 URL 하나당 서블릿이 하나 매핑되는 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서는 컨트롤러 클래스 하나가 여러 URL을 처리하고,&lt;br /&gt;그 안에서 메서드 단위로 매핑이 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@RequestMapping(&quot;/todo&quot;) : 클래스 레벨 공통 경로&lt;/li&gt;
&lt;li&gt;@GetMapping(&quot;/list&quot;) : GET /todo/list&lt;/li&gt;
&lt;li&gt;@PostMapping(&quot;/register&quot;) : POST /todo/register&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 doGet/doPost 같은 &amp;ldquo;서블릿 라이프사이클 메서드&amp;rdquo; 대신&lt;br /&gt;&amp;ldquo;요청 처리 메서드&amp;rdquo;를 여러 개 두는 구조로 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 스프링 MVC 요청 처리 흐름(핵심)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC의 큰 흐름은 이렇게 이해하면 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저 요청이 들어오면 DispatcherServlet이 받는다&lt;/li&gt;
&lt;li&gt;HandlerMapping이 &amp;ldquo;어떤 컨트롤러 메서드&amp;rdquo;가 처리할지 찾는다&lt;/li&gt;
&lt;li&gt;해당 메서드를 실행한다&lt;/li&gt;
&lt;li&gt;반환값을 해석해서 View를 렌더링하거나, 데이터로 바로 응답한다&lt;/li&gt;
&lt;li&gt;response를 전송한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DispatcherServlet이 입구/출구를 통제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) @Controller는 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Controller는 스프링이 인식하는 &amp;ldquo;웹 컨트롤러 빈&amp;rdquo; 표시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 component-scan을 통해 @Controller가 붙은 클래스를 빈으로 등록하고,&lt;br /&gt;DispatcherServlet이 요청을 매핑할 때 이 컨트롤러들을 대상으로 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 @Controller는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빈 등록 대상이면서&lt;/li&gt;
&lt;li&gt;웹 요청 처리 대상으로도 등록되는&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표시라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) PRG 흐름도 스프링에서는 더 자연스럽게 표현된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿에서는 response.sendRedirect(...)를 직접 호출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC에서는 반환 문자열로 redirect를 표현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;return &quot;redirect:/todo/list&quot;;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;처리 후 redirect&amp;rdquo;가 코드에서 더 읽기 쉬운 형태로 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 MVC는 모든 요청이 &lt;b&gt;DispatcherServlet&lt;/b&gt;을 거치는 프론트 컨트롤러 구조이다&lt;/li&gt;
&lt;li&gt;URL 매핑이 서블릿 단위가 아니라 컨트롤러 메서드 단위로 세분화된다&lt;/li&gt;
&lt;li&gt;@Controller는 스프링 MVC 요청 처리 대상이 되는 빈 표시이다&lt;/li&gt;
&lt;li&gt;redirect는 &quot;redirect:/...&quot; 형태로 자연스럽게 표현된다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/118</guid>
      <comments>https://woojoo-devlog.tistory.com/118#entry118comment</comments>
      <pubDate>Mon, 30 Mar 2026 16:12:15 +0900</pubDate>
    </item>
    <item>
      <title>[스프링과 스프링 MVC]  1. DI와 빈(Bean)</title>
      <link>https://woojoo-devlog.tistory.com/117</link>
      <description>&lt;h1&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이전까지는 이런 구조로 왔다.&lt;/span&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller(서블릿)가 요청을 받고&lt;/li&gt;
&lt;li&gt;Service/DAO를 호출하고&lt;/li&gt;
&lt;li&gt;JSP로 forward/redirect로 흐름을 제어한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 규모가 커질수록 코드가 이렇게 변한다는 점이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러가 서비스 객체를 직접 new로 만들기 시작한다&lt;/li&gt;
&lt;li&gt;서비스가 DAO를 직접 new로 만들기 시작한다&lt;/li&gt;
&lt;li&gt;구현체가 바뀌면 여기저기 코드가 같이 바뀐다&lt;/li&gt;
&lt;li&gt;테스트가 어려워진다(가짜 객체로 바꾸기 힘들다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 &amp;ldquo;객체를 누가 만들고, 누가 누구를 참조하게 만들지&amp;rdquo;를 분리해야 한다.&lt;br /&gt;이때 등장하는 핵심 개념이 DI(의존성 주입)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774852185880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [DI가 없을 때] Controller가 Service를 직접 new 해서 결합이 강해진다
// - 구현체 교체/테스트가 어려워진다
// ================================


public class TodoController {

    private final TodoService service = new TodoService(); // ❌ 직접 생성(강한 결합)

    public void handle() {
        service.register(&quot;study&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DI는 &amp;ldquo;의존 객체를 외부에서 주입받는 방식&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI(Dependency Injection)는 말 그대로 의존성을 주입하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 의존성은 이런 의미이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A가 일을 하려면 B가 필요하다&lt;/li&gt;
&lt;li&gt;즉 A는 B에 의존한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방식(강한 결합)은 A가 B를 직접 만든다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;A 내부에서 new B()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI 방식(느슨한 결합)은 A가 &amp;ldquo;B가 필요하다&amp;rdquo;만 말하고,&lt;br /&gt;B를 누가 만들지는 외부가 결정한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;A 생성자에 B를 넣어준다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이거다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 생성과 객체 사용을 분리한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빈(Bean)은 &amp;ldquo;스프링이 관리하는 객체&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 DI를 하기 위해 &amp;ldquo;객체를 대신 생성하고 관리&amp;rdquo;한다.&lt;br /&gt;스프링이 관리하는 객체를 빈(Bean)이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 빈은 단순히 &amp;ldquo;객체&amp;rdquo;가 아니라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 컨테이너가 생성하고&lt;/li&gt;
&lt;li&gt;생명주기를 관리하고&lt;/li&gt;
&lt;li&gt;필요한 곳에 주입하는&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리 대상 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ApplicationContext는 &amp;ldquo;빈을 담아두는 컨테이너&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에는 빈들을 담아두는 큰 상자가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 ApplicationContext이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext는 이런 역할을 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 빈이 등록되어 있는지 알고 있다&lt;/li&gt;
&lt;li&gt;빈을 생성한다&lt;/li&gt;
&lt;li&gt;의존 관계를 연결해서 주입한다&lt;/li&gt;
&lt;li&gt;같은 빈을 재사용(싱글톤 기본)해서 관리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;내가 new 하지 않아도 되는 이유&amp;rdquo;가 여기에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링을 쓰면 개발자는 &amp;ldquo;객체 생성&amp;rdquo;이 아니라 &amp;ldquo;등록과 주입&amp;rdquo;을 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링을 쓰기 시작하면 개발자의 작업 방식이 바뀐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예전: 필요한 객체를 직접 new 해서 연결한다&lt;/li&gt;
&lt;li&gt;스프링: 빈으로 등록하고, 필요한 곳에서는 주입받는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 코드의 초점이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;어디서 new 했지?&amp;rdquo;가 아니라&lt;/li&gt;
&lt;li&gt;&amp;ldquo;이 객체는 빈으로 등록됐나?&amp;rdquo;로 바뀐다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자 주입이 기본이 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI는 여러 방식이 있지만, 실무에서 가장 추천되는 건 생성자 주입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 주입이 좋은 이유는 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성이 없으면 객체 생성이 애초에 불가능해져서 문제를 빨리 발견한다&lt;/li&gt;
&lt;li&gt;필드를 final로 둘 수 있어서 안정적이다&lt;/li&gt;
&lt;li&gt;테스트에서 가짜 객체를 쉽게 주입할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스프링 3 이후로는 생성자 주입이 사실상 기본 패턴이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774852225930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [스프링 빈 등록 + 생성자 주입 예시]
// - @Service, @Controller 같은 어노테이션으로 빈 등록
// - @RequiredArgsConstructor로 생성자 주입을 간단히
// ================================

@Service
public class TodoService {
    public void register(String title) { /* ... */ }
}

@Controller
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService; // 스프링이 생성자에 주입

    @PostMapping(&quot;/todo/register&quot;)
    public String register(String title) {
        todoService.register(title);
        return &quot;redirect:/todo/list&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;component-scan: &amp;ldquo;어노테이션 붙은 클래스를 찾아서 빈으로 등록한다&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 빈을 등록하는 방법은 크게 두 가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML에 &amp;lt;bean&amp;gt;으로 등록&lt;/li&gt;
&lt;li&gt;컴포넌트 스캔(component-scan)으로 자동 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔은 이런 동작이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 패키지를 훑어보면서&lt;br /&gt;@Component 계열 어노테이션이 붙은 클래스를 찾아&lt;br /&gt;빈으로 등록한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 대표적인 어노테이션이 다음이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Controller&lt;/li&gt;
&lt;li&gt;@Service&lt;/li&gt;
&lt;li&gt;@Repository&lt;/li&gt;
&lt;li&gt;@Component&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;어노테이션을 붙이면 스프링이 빈으로 등록한다&amp;rdquo;는 말의 실체가 component-scan이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DI는 객체 생성과 사용을 분리해서 결합도를 낮추는 방식이다&lt;/li&gt;
&lt;li&gt;스프링은 빈(Bean)이라는 관리 객체를 만들고 DI를 수행한다&lt;/li&gt;
&lt;li&gt;ApplicationContext가 빈을 생성/관리/주입하는 컨테이너이다&lt;/li&gt;
&lt;li&gt;생성자 주입이 기본 패턴이고, @Controller/@Service 등은 component-scan으로 빈 등록된다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/117</guid>
      <comments>https://woojoo-devlog.tistory.com/117#entry117comment</comments>
      <pubDate>Mon, 30 Mar 2026 15:50:35 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  5. MyBatis 설정 흐름</title>
      <link>https://woojoo-devlog.tistory.com/116</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;DataSource &amp;rarr; SqlSessionFactory &amp;rarr; Mapper Scan&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis를 &amp;ldquo;코드 사용&amp;rdquo; 관점에서 보면 Mapper 인터페이스만 만들면 되는 것처럼 보인다.&lt;br /&gt;하지만 그게 동작하려면 컨테이너(스프링) 쪽에서 연결을 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 연결은 딱 3단계이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DataSource 준비 (HikariCP)&lt;/li&gt;
&lt;li&gt;SqlSessionFactory 준비&lt;/li&gt;
&lt;li&gt;Mapper Scan 등록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774845690083&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [JDBC DAO에서 반복되는 코드] 자원 얻기/닫기 + ResultSet 매핑
// - MyBatis가 주로 줄여주는 부분이 여기다
// ================================

public class TodoJdbcDao {

    private final DataSource ds;

    public TodoJdbcDao(DataSource ds) {
        this.ds = ds;
    }

    public List&amp;lt;TodoVO&amp;gt; findAll() throws SQLException {

        String sql = &quot;select tno, title, finished from tbl_todo order by tno desc&quot;;

        try (Connection conn = ds.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {

            List&amp;lt;TodoVO&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

            while (rs.next()) {
                TodoVO vo = new TodoVO(
                        rs.getLong(&quot;tno&quot;),
                        rs.getString(&quot;title&quot;),
                        rs.getBoolean(&quot;finished&quot;)
                );
                list.add(vo);
            }
            return list;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) DataSource: DB 연결을 제공하는 창구&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis도 결국 DB와 연결해야 SQL을 실행한다.&lt;br /&gt;그래서 MyBatis는 DataSource를 필요로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 이미 앞에서 만든 HikariCP(DataSource)를 그대로 쓴다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;풀에서 Connection을 빌려오는 창구&amp;rdquo;가 DataSource이고,&lt;br /&gt;MyBatis는 그 창구를 통해 Connection을 얻는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) SqlSessionFactory: MyBatis 실행의 핵심 공장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에서 실제 SQL 실행 단위는 SqlSession이다.&lt;br /&gt;그리고 그 SqlSession을 만들어내는 공장이 SqlSessionFactory이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구조는 이렇게 이해하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SqlSessionFactory &amp;rarr; SqlSession 생성&lt;/li&gt;
&lt;li&gt;SqlSession &amp;rarr; SQL 실행, 트랜잭션, 매핑 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 개발자가 SqlSession을 직접 다루기보다는&lt;br /&gt;스프링 연동을 통해 Mapper를 주입받아 쓰는 경우가 많지만,&lt;br /&gt;내부적으로는 SqlSession이 움직인다고 보면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) Mapper Scan: 인터페이스를 찾아 &amp;ldquo;프록시 구현체&amp;rdquo;를 만든다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis의 가장 큰 특징 중 하나가 이거다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mapper 인터페이스만 만들고&lt;/li&gt;
&lt;li&gt;구현 클래스는 만들지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 MyBatis가 프록시 구현체를 만들어서 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 가능하게 하는 게 Mapper Scan이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 scan의 의미는 이런 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 패키지를 훑어서 Mapper 인터페이스를 찾고,&lt;br /&gt;그 인터페이스의 구현체(프록시)를 만들어 빈으로 등록한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 TodoMapper 같은 인터페이스를 서비스에서 바로 주입받아 쓸 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1774846233759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [MyBatis Mapper 인터페이스 예시] (어노테이션 기반)
// - 구현 클래스를 내가 만들지 않는다
// - MyBatis가 프록시 구현체를 만들어준다
// ================================

@Mapper
public interface TodoMapper {

    @Select(&quot;select tno, title, finished from tbl_todo order by tno desc&quot;)
    List&amp;lt;TodoVO&amp;gt; findAll();

    @Insert(&quot;insert into tbl_todo(title, finished) values(#{title}, #{finished})&quot;)
    int insert(TodoVO vo);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;XML 매퍼 파일을 쓰면 mapperLocations가 추가된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에서 SQL을 XML로 분리해서 관리한다면&lt;br /&gt;SqlSessionFactory에 매퍼 파일 위치를 알려줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이런 형태로 등록한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;classpath*:mappers/**/*.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 있으면 resources 아래의 mappers 폴더 하위 XML 파일들을 찾아서 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariCP(DataSource)가 Connection을 제공한다&lt;/li&gt;
&lt;li&gt;SqlSessionFactory가 SqlSession을 만든다&lt;/li&gt;
&lt;li&gt;SqlSession이 SQL 실행과 매핑을 수행한다&lt;/li&gt;
&lt;li&gt;Mapper Scan이 Mapper 인터페이스를 찾아 프록시 빈을 만든다&lt;/li&gt;
&lt;li&gt;서비스는 DAO 대신 Mapper를 주입받아 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyBatis 설정은 DataSource &amp;rarr; SqlSessionFactory &amp;rarr; Mapper Scan 순서로 연결된다&lt;/li&gt;
&lt;li&gt;SqlSessionFactory는 SqlSession을 만드는 공장이다&lt;/li&gt;
&lt;li&gt;Mapper Scan이 Mapper 인터페이스 구현체(프록시)를 자동으로 만든다&lt;/li&gt;
&lt;li&gt;XML 매퍼를 쓰면 mapperLocations 설정이 필요하다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/116</guid>
      <comments>https://woojoo-devlog.tistory.com/116#entry116comment</comments>
      <pubDate>Mon, 30 Mar 2026 13:55:52 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  4. MyBatis는 무엇을 해결하는가</title>
      <link>https://woojoo-devlog.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC에서 반복되는 &amp;ldquo;매핑/자원관리&amp;rdquo;를 줄인다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC로 DAO를 작성할 수는 있다.&lt;br /&gt;그리고 실제로 기본기는 JDBC로 충분히 잡는 게 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JDBC DAO를 계속 작성해보면&lt;br /&gt;특정 패턴의 코드가 계속 반복되는 걸 느끼게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection 얻기&lt;/li&gt;
&lt;li&gt;PreparedStatement 만들기&lt;/li&gt;
&lt;li&gt;파라미터 setXXX 바인딩&lt;/li&gt;
&lt;li&gt;executeQuery/executeUpdate 호출&lt;/li&gt;
&lt;li&gt;ResultSet에서 꺼내서 VO로 매핑&lt;/li&gt;
&lt;li&gt;자원 close 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 특히 반복이 큰 부분이 두 가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ResultSet &amp;rarr; 객체 매핑&lt;/li&gt;
&lt;li&gt;자원 정리/예외 처리 패턴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 이 반복을 줄이기 위해 등장한 SQL 매핑 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MyBatis는 &amp;ldquo;SQL 매핑 프레임워크&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis를 한 문장으로 말하면 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 실행 결과를 자바 객체로 매핑해주는 도구&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;SQL을 없애는 프레임워크&amp;rdquo;가 아니다.&lt;br /&gt;오히려 SQL을 그대로 쓰면서, 자바 코드의 반복을 줄여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MyBatis는 이런 상황에서 잘 맞는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL을 직접 통제하고 싶다&lt;/li&gt;
&lt;li&gt;복잡한 쿼리를 그대로 쓰고 싶다&lt;/li&gt;
&lt;li&gt;JDBC 반복 코드를 줄이고 싶다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MyBatis가 줄여주는 것 1: ResultSet 매핑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC에서는 직접 이런 작업을 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rs.getLong(&quot;tno&quot;)&lt;/li&gt;
&lt;li&gt;rs.getString(&quot;title&quot;)&lt;/li&gt;
&lt;li&gt;new TodoVO(...)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 결과 컬럼과 객체 필드를 매핑해서&lt;br /&gt;이 과정을 상당 부분 자동화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;결과를 객체로 만드는 반복&amp;rdquo;이 줄어든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MyBatis가 줄여주는 것 2: 자원 정리/예외 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC에서는 Connection/Statement/ResultSet을 닫아야 한다.&lt;br /&gt;try-with-resources로 깔끔하게 만들 수는 있지만&lt;br /&gt;그래도 반복되는 틀은 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis(+스프링 연동)를 쓰면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 획득/반납&lt;/li&gt;
&lt;li&gt;Statement/ResultSet close&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 자원 관리가 더 자동화된다.&lt;/p&gt;
&lt;pre id=&quot;code_1774841452164&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [JDBC DAO에서 반복되는 코드] 자원 얻기/닫기 + ResultSet 매핑
// - MyBatis가 주로 줄여주는 부분이 여기다
// ================================

public class TodoJdbcDao {

    private final DataSource ds;

    public TodoJdbcDao(DataSource ds) {
        this.ds = ds;
    }

    public List&amp;lt;TodoVO&amp;gt; findAll() throws SQLException {

        String sql = &quot;select tno, title, finished from tbl_todo order by tno desc&quot;;

        try (Connection conn = ds.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {

            List&amp;lt;TodoVO&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

            while (rs.next()) {
                TodoVO vo = new TodoVO(
                        rs.getLong(&quot;tno&quot;),
                        rs.getString(&quot;title&quot;),
                        rs.getBoolean(&quot;finished&quot;)
                );
                list.add(vo);
            }
            return list;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mapper 인터페이스라는 방식이 핵심이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis를 쓰면 DAO 구현 클래스를 직접 만들지 않고&lt;br /&gt;Mapper 인터페이스만 정의하는 방식이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 인터페이스만 정의한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findAll()&lt;/li&gt;
&lt;li&gt;insert(...)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 MyBatis가 이 인터페이스의 구현체(프록시 객체)를 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 개발자는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;메서드 시그니처만 정의&amp;rdquo;하고&lt;/li&gt;
&lt;li&gt;&amp;ldquo;SQL 매핑만 연결&amp;rdquo;해두면&lt;/li&gt;
&lt;li&gt;실제 실행은 MyBatis가 대신한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 생산성을 크게 올려준다.&lt;/p&gt;
&lt;pre id=&quot;code_1774841569444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [MyBatis Mapper 인터페이스 예시] (어노테이션 기반)
// - 구현 클래스를 내가 만들지 않는다
// - MyBatis가 프록시 구현체를 만들어준다
// ================================

@Mapper
public interface TodoMapper {

    @Select(&quot;select tno, title, finished from tbl_todo order by tno desc&quot;)
    List&amp;lt;TodoVO&amp;gt; findAll();

    @Insert(&quot;insert into tbl_todo(title, finished) values(#{title}, #{finished})&quot;)
    int insert(TodoVO vo);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL은 어디에 두는가: 어노테이션 vs XML&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis는 SQL을 두는 방식이 두 가지가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 어노테이션(@Select, @Insert 등)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 SQL은 빠르게 작성 가능&lt;/li&gt;
&lt;li&gt;하지만 SQL이 길어지면 가독성이 떨어진다&lt;/li&gt;
&lt;li&gt;변경 시 전체 빌드/컴파일 영향이 있을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) XML 매퍼 파일&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL을 파일로 분리해서 관리하기 좋다&lt;/li&gt;
&lt;li&gt;SQL이 길거나 복잡할수록 유리하다&lt;/li&gt;
&lt;li&gt;실무에서는 XML 방식을 선호하는 경우가 많다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 SQL이 커질수록 XML 분리가 더 편해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyBatis는 SQL을 없애는 게 아니라, SQL을 &amp;ldquo;매핑&amp;rdquo;해주는 프레임워크이다&lt;/li&gt;
&lt;li&gt;JDBC에서 반복되는 ResultSet 매핑과 자원 관리 코드를 줄여준다&lt;/li&gt;
&lt;li&gt;Mapper 인터페이스만 정의하고 구현체는 MyBatis가 프록시로 만든다&lt;/li&gt;
&lt;li&gt;SQL은 어노테이션으로 둘 수도 있고, XML로 분리할 수도 있다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/115</guid>
      <comments>https://woojoo-devlog.tistory.com/115#entry115comment</comments>
      <pubDate>Mon, 30 Mar 2026 13:12:06 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  3. Connection Pool / DataSource / HikariCP</title>
      <link>https://woojoo-devlog.tistory.com/114</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결을 미리 만들어두고 빌려 쓴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC는 DB와 연결해야 SQL을 실행할 수 있다.&lt;br /&gt;그래서 Connection이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection 생성은 생각보다 비용이 큰 작업이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어올 때마다 매번 새 연결을 만들면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;느려지고&lt;/li&gt;
&lt;li&gt;DB 연결 수가 급격히 늘고&lt;/li&gt;
&lt;li&gt;결국 서버가 버티기 힘들어진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는 거의 무조건 &lt;b&gt;Connection Pool&lt;/b&gt;을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connection을 새로 만드는 게 왜 비싼가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection을 만든다는 건 단순히 객체 하나 만드는 게 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB와 네트워크(TCP) 연결을 맺는다&lt;/li&gt;
&lt;li&gt;인증/권한 확인 같은 협상이 들어간다&lt;/li&gt;
&lt;li&gt;네트워크 왕복이 발생한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 요청마다 이 과정을 반복하면 성능이 크게 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connection Pool의 아이디어는 단순하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 미리 만들어두는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 시작할 때 Connection을 여러 개 만들어둔다&lt;/li&gt;
&lt;li&gt;요청이 오면 그 중 하나를 &amp;ldquo;빌려준다&amp;rdquo;&lt;/li&gt;
&lt;li&gt;사용이 끝나면 &amp;ldquo;반납받는다&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 Connection Pool이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 풀은 &amp;ldquo;Connection 재사용 시스템&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774841023105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [DataSource 사용 예시] (풀에서 Connection을 빌려온다)
// - getConnection()을 호출하면 &quot;새로 연결&quot;이 아니라 &quot;풀에서 꺼내온 Connection&quot;일 수 있다
// - close()는 실제 종료가 아니라 &quot;풀에 반납&quot;이 된다
// ================================

public class TodoDAO {

    private final DataSource dataSource;

    public TodoDAO(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public List&amp;lt;TodoVO&amp;gt; findAll() throws SQLException {

        String sql = &quot;select tno, title, finished from tbl_todo order by tno desc&quot;;

        try (Connection conn = dataSource.getConnection();   // 풀에서 빌림
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {

            List&amp;lt;TodoVO&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

            while (rs.next()) {
                list.add(new TodoVO(
                        rs.getLong(&quot;tno&quot;),
                        rs.getString(&quot;title&quot;),
                        rs.getBoolean(&quot;finished&quot;)
                ));
            }

            return list;
        }
        // conn.close()는 try-with-resources에 의해 호출되지만,
        // &quot;진짜 종료&quot;가 아니라 보통 풀에 &quot;반납&quot;하는 의미가 된다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DataSource는 &amp;ldquo;풀을 쓰기 위한 표준 인터페이스&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 Connection Pool을 통일된 방식으로 쓰기 위해&lt;br /&gt;javax.sql.DataSource라는 인터페이스를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource 관점에서 개발자는 딱 한 줄만 중요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dataSource.getConnection()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 호출이 &amp;ldquo;새 연결 생성&amp;rdquo;일 수도 있고,&lt;br /&gt;대부분은 &amp;ldquo;풀에서 빌려오기&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DataSource는 개발자에게&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;Connection을 얻는 표준 창구&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀을 쓰면 close()의 의미가 달라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 중요한 오해 포인트가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀을 쓰는 상황에서 conn.close()는 보통&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 연결을 진짜 끊는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;풀에 Connection을 반납한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 의미가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 close는 &amp;ldquo;반납&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 풀을 쓰더라도 close는 반드시 해야 한다.&lt;br /&gt;close를 안 하면 풀에 반납이 안 되고 연결이 고갈된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HikariCP는 대표적인 Connection Pool 구현체이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection Pool은 구현체가 필요하다.&lt;br /&gt;그 구현체 중에서 가장 널리 쓰이는 게 HikariCP이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP의 구조는 보통 이렇게 잡는다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HikariConfig로 설정을 만든다&lt;/li&gt;
&lt;li&gt;HikariDataSource를 만든다&lt;/li&gt;
&lt;li&gt;이 DataSource를 DAO에 주입해서 사용한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 HikariDataSource는 DataSource 구현체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774841210760&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [HikariCP 설정 예시] (가장 많이 쓰는 커넥션 풀 구현체 중 하나)
// - HikariConfig -&amp;gt; HikariDataSource
// - HikariDataSource는 DataSource 구현체이다
// ================================

HikariConfig config = new HikariConfig();
config.setJdbcUrl(&quot;jdbc:mariadb://localhost:3306/app&quot;);
config.setUsername(&quot;root&quot;);
config.setPassword(&quot;1234&quot;);

// 풀 크기 같은 옵션
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);

DataSource ds = new HikariDataSource(config);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DAO는 DataSource를 가진다&lt;/li&gt;
&lt;li&gt;DAO는 필요할 때 getConnection()으로 Connection을 얻는다(빌림)&lt;/li&gt;
&lt;li&gt;try-with-resources로 작업이 끝나면 close된다(반납)&lt;/li&gt;
&lt;li&gt;풀은 Connection을 재사용해서 성능을 올린다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection 생성은 비싸서 요청마다 새로 만들면 느리다&lt;/li&gt;
&lt;li&gt;Connection Pool은 연결을 미리 만들어두고 빌려 쓰는 방식이다&lt;/li&gt;
&lt;li&gt;DataSource는 풀을 쓰기 위한 표준 인터페이스이다&lt;/li&gt;
&lt;li&gt;풀 환경에서 close()는 보통 &amp;ldquo;종료&amp;rdquo;가 아니라 &amp;ldquo;반납&amp;rdquo;이다&lt;/li&gt;
&lt;li&gt;HikariCP는 대표적인 커넥션 풀 구현체이다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/114</guid>
      <comments>https://woojoo-devlog.tistory.com/114#entry114comment</comments>
      <pubDate>Mon, 30 Mar 2026 12:28:20 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  2. JDBC란?</title>
      <link>https://woojoo-devlog.tistory.com/113</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;자바가 DB와 통신하는 표준 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연동을 한다는 말은 결국 이 뜻이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 프로그램이 네트워크로 DB 서버에 연결한다&lt;/li&gt;
&lt;li&gt;SQL을 전송한다&lt;/li&gt;
&lt;li&gt;DB가 실행한 결과를 다시 받아서 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 자바 표준 API로 정리한 게 JDBC이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JDBC는 &amp;ldquo;DB 연결과 SQL 실행을 위한 자바 표준 규격&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC로 DB 작업을 할 때 항상 나오는 3가지 객체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC는 결국 아래 세 가지 객체를 중심으로 돌아간다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection&lt;/li&gt;
&lt;li&gt;PreparedStatement&lt;/li&gt;
&lt;li&gt;ResultSet&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 개만 이해하면 JDBC의 큰 흐름은 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774836291531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [JDBC 기본 흐름] (조회: SELECT)
// 1) Connection 얻기
// 2) PreparedStatement 준비
// 3) 파라미터 바인딩(setXXX)
// 4) executeQuery() -&amp;gt; ResultSet
// 5) ResultSet.next()로 행 이동하며 꺼내기(getXXX)
// 6) 자원 close (try-with-resources 추천)
// ================================

public List&amp;lt;TodoVO&amp;gt; findAll(Connection conn) throws SQLException {

    String sql = &quot;select tno, title, finished from tbl_todo order by tno desc&quot;;

    try (PreparedStatement ps = conn.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {

        List&amp;lt;TodoVO&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

        while (rs.next()) {
            long tno = rs.getLong(&quot;tno&quot;);
            String title = rs.getString(&quot;title&quot;);
            boolean finished = rs.getBoolean(&quot;finished&quot;);

            list.add(new TodoVO(tno, title, finished));
        }

        return list;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774836748706&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [JDBC 기본 흐름] (변경: INSERT/UPDATE/DELETE)
// - executeUpdate()는 &quot;영향 받은 행 수&quot;를 int로 반환한다
// ================================

public int insert(Connection conn, String title) throws SQLException {

    String sql = &quot;insert into tbl_todo(title, finished) values(?, ?)&quot;;

    try (PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setString(1, title);
        ps.setBoolean(2, false);

        return ps.executeUpdate(); // 보통 1이 나온다(1행 insert)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) Connection: DB와의 연결 자체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection은 DB와 네트워크 연결을 의미한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결이 있어야 SQL을 보낼 수 있다&lt;/li&gt;
&lt;li&gt;연결이 없으면 아무 것도 못 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JDBC에서 가장 중요한 규칙은 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection은 반드시 close 해야 한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 서버는 동시에 처리할 수 있는 연결 수가 제한적이다.&lt;br /&gt;close를 안 하면 연결이 쌓여서 결국 신규 연결이 막히는 상황이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) PreparedStatement: SQL 실행을 담당&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PreparedStatement는 &lt;b&gt;SQL을 DB로 보내 실행하는 객체&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 이유는 두 가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파라미터 바인딩을 안전하게 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;? 자리에 값을 setXXX()로 넣는다&lt;/li&gt;
&lt;li&gt;SQL 문자열 결합을 줄여서 실수를 줄인다&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SQL Injection 같은 공격을 줄이는 데 유리하다&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력을 SQL로 직접 붙이지 않기 때문에 상대적으로 안전하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JDBC에서는 Statement보다 PreparedStatement를 기본으로 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) ResultSet: SELECT 결과를 읽는 커서(cursor)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT는 &amp;ldquo;결과 데이터&amp;rdquo;를 반환한다.&lt;br /&gt;이 결과를 읽는 도구가 ResultSet이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResultSet은 중요한 특징이 하나 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResultSet은 처음에는 &amp;ldquo;첫 행&amp;rdquo;에 서 있지 않다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 반드시 next()로 다음 행으로 이동해야 한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;while (rs.next()) {
    // 현재 행의 컬럼 값을 꺼낸다
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 값을 꺼낼 때는 컬럼 타입에 맞는 메서드를 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getLong(), getInt(), getString(), getBoolean() &amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC는 SELECT와 DML이 처리 방식이 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 처음 할 때 자주 헷갈리는 부분이 여기다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SELECT (조회)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과 데이터가 나온다&lt;/li&gt;
&lt;li&gt;그래서 executeQuery()를 쓰고 ResultSet을 받는다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DML (INSERT/UPDATE/DELETE)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과 데이터가 아니라 &amp;ldquo;영향 받은 행 수&amp;rdquo;가 중요하다&lt;/li&gt;
&lt;li&gt;그래서 executeUpdate()를 쓰고 int를 받는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 = ResultSet&lt;/li&gt;
&lt;li&gt;변경 = 영향 행 수(int)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 잡아야 JDBC 코드가 자연스럽게 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자원 정리는 try-with-resources가 기본이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC는 자원이 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection&lt;/li&gt;
&lt;li&gt;PreparedStatement&lt;/li&gt;
&lt;li&gt;ResultSet&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 제대로 닫지 않으면 누수(leak)가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가장 깔끔한 방식은 try-with-resources이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 코드처럼 작성하면&lt;br /&gt;블록이 끝날 때 자동으로 close가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC는 자바가 DB와 통신하기 위한 표준 API이다&lt;/li&gt;
&lt;li&gt;Connection은 연결이고 반드시 close 해야 한다&lt;/li&gt;
&lt;li&gt;PreparedStatement로 SQL을 실행하고 setXXX로 파라미터를 바인딩한다&lt;/li&gt;
&lt;li&gt;SELECT는 executeQuery + ResultSet, DML은 executeUpdate + int이다&lt;/li&gt;
&lt;li&gt;try-with-resources로 자원 정리를 자동화하는 게 기본이다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/113</guid>
      <comments>https://woojoo-devlog.tistory.com/113#entry113comment</comments>
      <pubDate>Mon, 30 Mar 2026 11:26:29 +0900</pubDate>
    </item>
    <item>
      <title>[데이터 처리와 DB연동]  1. DTO / VO(Entity) / DAO / Service는 왜 나누는가?</title>
      <link>https://woojoo-devlog.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 만들면 결국 데이터가 필요하다.&lt;br /&gt;게시글, 회원, 주문, 할 일 목록처럼 &amp;ldquo;저장하고 꺼내야 하는 정보&amp;rdquo;가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 건 단순히 &amp;ldquo;DB에 넣고 꺼내는 코드&amp;rdquo;가 아니라,&lt;br /&gt;그 코드를 어떤 구조로 나눠서 유지보수 가능하게 만들 것인가이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통 아래 네 가지 역할을 분리해서 생각한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO&lt;/li&gt;
&lt;li&gt;VO(Entity)&lt;/li&gt;
&lt;li&gt;DAO&lt;/li&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) DTO: 계층 사이를 오가는 &amp;ldquo;전달용 데이터 묶음&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO(Data Transfer Object)는 이름 그대로 &amp;ldquo;전달용 객체&amp;rdquo;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러 &amp;harr; 서비스&lt;/li&gt;
&lt;li&gt;서비스 &amp;harr; DAO&lt;/li&gt;
&lt;li&gt;서버 &amp;harr; 화면(JSP/API)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 계층 사이에서 값을 주고받을 때 쓰는 택배 상자이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO의 특징은 보통 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 값을 한 번에 묶어서 전달한다&lt;/li&gt;
&lt;li&gt;필요하면 입력값 검증(스프링에서는 더 강하게)에도 쓰인다&lt;/li&gt;
&lt;li&gt;로직을 넣기보다 &amp;ldquo;데이터 담기&amp;rdquo;에 집중한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DTO는 &amp;ldquo;역할&amp;rdquo;이 아니라 &amp;ldquo;운반&amp;rdquo;이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) VO(Entity): DB 한 행(row)을 표현하는 &amp;ldquo;데이터 자체&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VO(Value Object) 또는 Entity는 DB의 한 행을 자바 객체로 표현한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 todo 테이블이 있다면&lt;br /&gt;그 테이블의 한 행을 표현하는 Todo 같은 클래스가 VO/Entity 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VO/Entity는 이런 성격이 강하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 구조에 가깝다&lt;/li&gt;
&lt;li&gt;&amp;ldquo;데이터 그 자체&amp;rdquo;를 표현한다&lt;/li&gt;
&lt;li&gt;보통 DAO가 이 객체 단위로 데이터를 다룬다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO가 &amp;ldquo;배송 상자&amp;rdquo;라면&lt;br /&gt;VO/Entity는 &amp;ldquo;실제 물건&amp;rdquo;에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) DAO: DB 접근을 전담하는 객체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO(Data Access Object)는 DB 접근 로직을 모아놓은 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DAO는 &amp;ldquo;데이터베이스와 직접 대화하는 계층&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO가 하는 일의 예시는 이런 것들이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;insert/update/delete/select 실행&lt;/li&gt;
&lt;li&gt;JDBC로 Connection/PreparedStatement/ResultSet 처리&lt;/li&gt;
&lt;li&gt;결과를 VO/Entity로 매핑해서 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스/컨트롤러는 SQL이 어떻게 실행되는지 몰라도 된다&lt;br /&gt;DB 접근은 DAO가 책임진다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) Service: 비즈니스 로직(기능)을 묶는 계층&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 기능 단위 로직을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;Todo 등록&amp;rdquo; 기능이 있다면&lt;br /&gt;그 기능이 단순 insert 하나로 끝나지 않을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력값 검증&lt;/li&gt;
&lt;li&gt;중복 체크&lt;/li&gt;
&lt;li&gt;트랜잭션 처리&lt;/li&gt;
&lt;li&gt;여러 DAO 호출 조합&lt;/li&gt;
&lt;li&gt;정책(규칙) 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 &amp;ldquo;업무 규칙&amp;rdquo;을 모으는 곳이 Service이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DAO가 &amp;ldquo;데이터 접근&amp;rdquo;이라면&lt;br /&gt;Service는 &amp;ldquo;기능의 의미&amp;rdquo;를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 MVC에서 일반적인 흐름은 이런 식이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Controller가 요청 파라미터를 받아 DTO를 만든다&lt;/li&gt;
&lt;li&gt;Controller가 Service를 호출한다&lt;/li&gt;
&lt;li&gt;Service가 필요한 정책/로직을 수행한다&lt;/li&gt;
&lt;li&gt;Service가 DAO를 호출해 DB 작업을 수행한다&lt;/li&gt;
&lt;li&gt;DAO가 DB 결과를 VO/Entity로 만들어 반환한다&lt;/li&gt;
&lt;li&gt;필요하면 Service가 VO/Entity를 DTO로 변환해 Controller에 준다&lt;/li&gt;
&lt;li&gt;Controller가 View(JSP)에 전달한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 역할을 나눠서 &amp;ldquo;책임 분리&amp;rdquo;를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DTO와 VO를 왜 둘 다 만들까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 현실적으로 제일 많이 나오는 질문이 이거다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;DTO랑 VO가 비슷한데 왜 둘 다 만들지?&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 목적이 다르기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO: 계층 사이 전달 / 입력값 중심 / 화면 중심&lt;/li&gt;
&lt;li&gt;VO(Entity): DB 모델 표현 / 저장/조회 중심&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 귀찮아 보이지만&lt;br /&gt;프로젝트가 커질수록 분리해 둔 구조가 더 유리해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 스프링/JPA로 넘어가면&lt;br /&gt;VO(Entity)는 필수 개념이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO는 전달용 데이터 상자이다&lt;/li&gt;
&lt;li&gt;VO(Entity)는 DB 한 행을 표현하는 데이터 자체이다&lt;/li&gt;
&lt;li&gt;DAO는 DB 접근을 전담한다&lt;/li&gt;
&lt;li&gt;Service는 기능(비즈니스 로직)을 묶는다&lt;/li&gt;
&lt;li&gt;역할 분리를 하면 유지보수와 확장이 쉬워진다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/112</guid>
      <comments>https://woojoo-devlog.tistory.com/112#entry112comment</comments>
      <pubDate>Mon, 30 Mar 2026 10:57:18 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  5. 리스너(Listener)란?</title>
      <link>https://woojoo-devlog.tistory.com/111</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;특정 이벤트 시점에 자동 실행되는 훅(hook)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 요청이 들어올 때마다 실행되는 공통 처리 장치였다.&lt;br /&gt;즉 &amp;ldquo;요청 흐름 중간&amp;rdquo;에 끼어드는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 어떤 작업은 &amp;ldquo;요청이 올 때마다&amp;rdquo;가 아니라&lt;br /&gt;특정 시점에 한 번만 실행되는 게 더 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 것들이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션이 시작될 때 초기화 작업을 하고 싶다&lt;/li&gt;
&lt;li&gt;애플리케이션이 종료될 때 자원 정리를 하고 싶다&lt;/li&gt;
&lt;li&gt;세션이 생성/종료될 때 로그를 남기고 싶다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 &amp;ldquo;이벤트 기반 실행&amp;rdquo;을 위해 서블릿 API는 리스너(Listener)를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스너는 &amp;ldquo;옵저버 패턴&amp;rdquo; 형태이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너는 감시 대상(이벤트)이 있고,&lt;br /&gt;이벤트가 발생하면 리스너가 자동으로 호출되는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 개발자는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;언제 실행될지&amp;rdquo;를 직접 호출로 제어하는 게 아니라&lt;/li&gt;
&lt;li&gt;&amp;ldquo;이 이벤트가 발생하면 이 코드를 실행해라&amp;rdquo;를 등록한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 리스너의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774834153754&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [Listener 기본 형태] 애플리케이션 시작/종료 시점에 자동 실행
// - @WebListener + ServletContextListener
// ================================

@WebListener
public class AppInitListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 웹 애플리케이션 시작 시 1번 실행
        ServletContext app = sce.getServletContext();

        // 애플리케이션 공용 객체 등록 (Application Scope)
        app.setAttribute(&quot;appName&quot;, &quot;W2&quot;);

        System.out.println(&quot;[Listener] contextInitialized: appName set&quot;);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // 웹 애플리케이션 종료 시 1번 실행
        System.out.println(&quot;[Listener] contextDestroyed&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ServletContextListener: 애플리케이션 시작/종료 이벤트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 리스너가 ServletContextListener이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;contextInitialized() : 웹 애플리케이션 시작 시 1회 실행&lt;/li&gt;
&lt;li&gt;contextDestroyed() : 웹 애플리케이션 종료 시 1회 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;서버가 뜰 때 / 내려갈 때&amp;rdquo; 실행되는 훅이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 리스너가 왜 중요하냐면,&lt;br /&gt;초기화 작업을 &amp;ldquo;한 곳에 모아서&amp;rdquo; 처리할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ServletContext는 애플리케이션 공용 저장소(Application Scope)이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`ServletContextListener`는 `ServletContextEvent`를 받는다.&lt;br /&gt;여기서 getServletContext()로 ServletContext를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServletContext는 한 문장으로 말하면 이거다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 웹 애플리케이션 전체에서 공유하는 저장소&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 여기다 값을 넣으면 모든 서블릿/JSP가 공유해서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app.setAttribute(&quot;appName&quot;, &quot;W2&quot;);&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 JSP에서는 EL로 이렇게 읽을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;${appName}&lt;/li&gt;
&lt;li&gt;${applicationScope.appName}&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 application scope이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리스너를 쓰는 대표적인 이유: &amp;ldquo;초기화&amp;rdquo;를 한 번에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션은 보통 시작할 때 준비해야 하는 것들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 커넥션 풀 초기화&lt;/li&gt;
&lt;li&gt;서비스 객체 생성 및 등록&lt;/li&gt;
&lt;li&gt;공용 설정값 로딩&lt;/li&gt;
&lt;li&gt;공용 리소스 캐싱&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 각 컨트롤러에서 매번 만들면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 생성 문제가 생기고&lt;/li&gt;
&lt;li&gt;관리가 어렵고&lt;/li&gt;
&lt;li&gt;시작 시점 제어도 힘들어진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 리스너에서 한 번만 생성해서 ServletContext에 등록해두고&lt;br /&gt;컨트롤러가 가져다 쓰는 구조가 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(나중에 스프링이 이런 역할을 더 체계적으로 해준다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@WebListener로 등록한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너는 컨테이너가 알아서 실행해야 하므로 등록이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 보통 어노테이션으로 등록한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@WebListener&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어노테이션이 붙어 있으면 톰캣이 시작할 때 리스너를 인식하고&lt;br /&gt;해당 이벤트 시점에 자동 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리스너는 특정 이벤트 시점에 자동 실행되는 훅이다&lt;/li&gt;
&lt;li&gt;필터가 &amp;ldquo;요청마다 실행&amp;rdquo;이라면, 리스너는 &amp;ldquo;시작/종료 같은 이벤트에 실행&amp;rdquo;이다&lt;/li&gt;
&lt;li&gt;ServletContextListener는 애플리케이션 시작/종료 시점에 실행된다&lt;/li&gt;
&lt;li&gt;ServletContext는 애플리케이션 공용 저장소(Application Scope)이다&lt;/li&gt;
&lt;li&gt;초기화 작업(DB 풀, 공용 객체 준비 등)을 리스너에서 한 번에 처리할 수 있다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/111</guid>
      <comments>https://woojoo-devlog.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 30 Mar 2026 10:37:37 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  4. 필터(Filter)란?</title>
      <link>https://woojoo-devlog.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;ldquo;컨트롤러 앞에서 공통 로직을 한 번에 적용한다&amp;rdquo;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;로그인 체크를 어디에 넣어야 하지?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보호해야 하는 URL이 많아지면&lt;br /&gt;컨트롤러마다 똑같은 코드를 반복하게 된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;HttpSession session = req.getSession(false);
if (session == null || session.getAttribute(&quot;loginUser&quot;) == null) {
    resp.sendRedirect(&quot;/login-form&quot;);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 모든 서블릿에 복붙하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 중복되고&lt;/li&gt;
&lt;li&gt;빠뜨리기 쉽고&lt;/li&gt;
&lt;li&gt;유지보수도 힘들어진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;요청이 컨트롤러에 들어가기 전에&amp;rdquo; 공통 로직을 적용하는 장치가 필요하다.&lt;br /&gt;그게 필터(Filter)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필터는 &amp;ldquo;서블릿 앞단에서 실행되는 가로채기(Interceptor)&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 서블릿 API에서 제공하는 특별한 객체이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청이 서블릿/JSP에 도달하기 전에 실행된다(사전 처리)&lt;/li&gt;
&lt;li&gt;필요하면 서블릿 실행 후에도 실행될 수 있다(사후 처리)&lt;/li&gt;
&lt;li&gt;특정 URL 패턴에만 적용할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 필터는 &amp;ldquo;컨트롤러 앞에서 공통 처리를 한 번에 적용하는 장치&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필터의 핵심 메서드: doFilter()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 doFilter() 메서드 하나가 핵심이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트는 두 가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Filter는 HTTP 전용이 아니다&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래서 파라미터 타입이 ServletRequest/ServletResponse이다&lt;/li&gt;
&lt;li&gt;HTTP 기능이 필요하면 HttpServletRequest/HttpServletResponse로 다운캐스팅한다&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반드시 chain.doFilter()를 호출해야 다음 단계로 진행된다&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 호출을 하지 않으면 요청이 필터에서 멈춘다&lt;/li&gt;
&lt;li&gt;즉 &amp;ldquo;통과시키지 않겠다&amp;rdquo;는 의미가 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 필터는 다음이 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건이 맞으면 통과(chain.doFilter)&lt;/li&gt;
&lt;li&gt;조건이 틀리면 여기서 끊고 redirect/에러 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@WebFilter로 적용 범위를 지정한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 모든 요청에 다 걸 수도 있고,&lt;br /&gt;특정 경로에만 걸 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@WebFilter(&quot;/*&quot;) : 전체 요청&lt;/li&gt;
&lt;li&gt;@WebFilter(&quot;/todo/*&quot;) : todo 아래만&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 보통 &amp;ldquo;로그인 체크&amp;rdquo;처럼 필요한 범위만 걸어주는 방식이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 체크 필터 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 체크 필터는 보통 이런 흐름으로 작성한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 요청 URI를 확인한다&lt;/li&gt;
&lt;li&gt;로그인 페이지/로그인 처리 URL은 예외로 통과시킨다&lt;/li&gt;
&lt;li&gt;그 외 경로는 세션에서 loginUser를 확인한다&lt;/li&gt;
&lt;li&gt;없으면 로그인 페이지로 redirect 한다&lt;/li&gt;
&lt;li&gt;있으면 chain.doFilter로 통과시킨다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 컨트롤러마다 로그인 체크 코드를 쓰지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필터는 UTF-8 인코딩 처리에도 자주 쓰인다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST로 한글이 깨지는 문제는&lt;br /&gt;컨트롤러마다 request.setCharacterEncoding(&quot;UTF-8&quot;)을 넣으면 해결되지만&lt;br /&gt;이것도 중복이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인코딩 처리도 필터로 많이 뺀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 필터는 로그인뿐 아니라 &amp;ldquo;모든 공통 관심사&amp;rdquo;를 처리하는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터는 서블릿 앞에서 실행되는 공통 처리 장치이다&lt;/li&gt;
&lt;li&gt;doFilter에서 chain.doFilter를 호출해야 다음 단계로 넘어간다&lt;/li&gt;
&lt;li&gt;HTTP 기능이 필요하면 HttpServletRequest/Response로 다운캐스팅한다&lt;/li&gt;
&lt;li&gt;로그인 체크, 인코딩 처리 같은 반복 코드를 필터로 분리할 수 있다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/110</guid>
      <comments>https://woojoo-devlog.tistory.com/110#entry110comment</comments>
      <pubDate>Mon, 30 Mar 2026 10:17:51 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  3. SESSIONID이란?</title>
      <link>https://woojoo-devlog.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;세션 저장소의 키&amp;rdquo;로 쓰이는 쿠키 값&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 글에서 세션은 이렇게 동작한다고 정리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 데이터는 서버(톰캣)의 세션 저장소에 보관한다&lt;/li&gt;
&lt;li&gt;브라우저는 쿠키로 &amp;ldquo;세션ID&amp;rdquo;만 들고 다닌다&lt;/li&gt;
&lt;li&gt;서버는 그 세션ID로 세션 저장소에서 사용자 세션을 찾는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 그 &amp;ldquo;세션ID&amp;rdquo;가 바로 JSESSIONID의 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JSESSIONID는 로그인 정보가 아니다.&lt;br /&gt;서버가 로그인 정보를 찾기 위한 &lt;b&gt;키(열쇠 번호)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSESSIONID의 정체: 쿠키 이름이다 (값이 세션ID)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 용어를 정확히 분리해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSESSIONID : &lt;b&gt;쿠키의 이름(name)&lt;/b&gt; 이다&lt;/li&gt;
&lt;li&gt;JSESSIONID=abc123... : 여기서 abc123...이 &lt;b&gt;세션ID(value)&lt;/b&gt; 이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 브라우저는 쿠키로 이런 형태를 들고 다닌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cookie: JSESSIONID=abc123...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버(톰캣)는 이 abc123... 값을 읽어서&lt;br /&gt;세션 저장소에서 해당 세션을 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 저장소 관점에서 보면 JSESSIONID는 &amp;ldquo;키&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 내부를 개념적으로 보면 보통 이런 구조이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 저장소(Map 같은 구조)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key: 세션ID (쿠키 JSESSIONID의 value)&lt;/li&gt;
&lt;li&gt;value: 세션 한 칸(사용자별 attribute 저장 공간)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JSESSIONID 값이 바뀌면&lt;br /&gt;서버 입장에서는 &amp;ldquo;다른 세션&amp;rdquo;으로 인식한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;session.getId()가 곧 JSESSIONID 값이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿에서 세션ID를 직접 보면 더 직관적이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// ================================
// 세션ID 확인용 예시
// - session.getId() 값이 곧 JSESSIONID 쿠키의 value와 대응된다
// ================================

@WebServlet(&quot;/session/id&quot;)
public class SessionIdServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        HttpSession session = req.getSession(); // 없으면 생성 + JSESSIONID 발급 준비
        String sessionId = session.getId();

        resp.setContentType(&quot;text/plain; charset=UTF-8&quot;);
        resp.getWriter().println(&quot;session.getId() = &quot; + sessionId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청을 &amp;ldquo;쿠키 없는 상태&amp;rdquo;에서 처음 호출하면, 보통 이런 일이 벌어진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버가 새 세션을 만들고 session.getId()를 생성한다&lt;/li&gt;
&lt;li&gt;응답 헤더로 Set-Cookie: JSESSIONID=방금생성한ID가 내려간다&lt;/li&gt;
&lt;li&gt;다음 요청부터 브라우저가 Cookie: JSESSIONID=그ID를 자동으로 붙인다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 session.getId() 값과 브라우저가 들고 다니는 JSESSIONID 값은 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSESSIONID는 &amp;ldquo;로그인 정보&amp;rdquo;가 아니라 &amp;ldquo;로그인 정보를 찾는 열쇠&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 흔히 하는 오해가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;JSESSIONID가 있으니까 로그인된 거 아닌가?&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSESSIONID가 있다는 건 단지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;서버 세션 저장소에 접근할 수 있는 열쇠가 있다&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 여부는 결국 세션 안에 저장된 값으로 결정된다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 로그인 여부 체크는 보통 이렇게 한다
HttpSession session = req.getSession(false);
String loginUser = (session == null) ? null : (String) session.getAttribute(&quot;loginUser&quot;);

if (loginUser == null) {
    // 로그인 안 됨
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 핵심은 이거다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSESSIONID = 세션을 찾는 키&lt;/li&gt;
&lt;li&gt;loginUser = 로그인 상태를 판단하는 데이터(세션 attribute)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSESSIONID는 언제 발급되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 아래 시점에서 발급된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;req.getSession()을 호출했는데, 현재 요청에 유효한 세션이 없을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;세션이 새로 만들어지는 순간&amp;rdquo;에 발급된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 요청에 유효한 JSESSIONID가 이미 있고 세션이 살아 있으면&lt;br /&gt;대부분은 새로 발급되지 않는다(그냥 기존 세션을 찾아서 쓴다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSESSIONID는 언제까지 유지되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 두 개를 구분해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 쿠키(JSESSIONID)의 유지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 톰캣이 발급하는 JSESSIONID는 &amp;ldquo;세션 쿠키&amp;rdquo; 형태가 많다&lt;/li&gt;
&lt;li&gt;즉 브라우저를 종료하면 쿠키가 사라질 수 있다(설정에 따라 다를 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 서버 세션의 유지(세션 타임아웃)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 세션을 영원히 유지하지 않는다&lt;/li&gt;
&lt;li&gt;일정 시간 동안 요청이 없으면 만료시키고 저장소에서 제거한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 브라우저에 쿠키가 남아 있어도&lt;br /&gt;서버에서 세션이 만료되면 그 JSESSIONID는 더 이상 유효하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상태에서 다시 요청이 오면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 &amp;ldquo;세션이 없네?&amp;rdquo;라고 판단하고&lt;/li&gt;
&lt;li&gt;새 세션을 만들고&lt;/li&gt;
&lt;li&gt;새 JSESSIONID를 다시 발급할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션을 강제로 종료하면 어떻게 되나: invalidate()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃은 보통 세션을 무효화하는 방식으로 처리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// ================================
// 로그아웃 예시: 세션 무효화
// ================================

@WebServlet(&quot;/logout&quot;)
public class LogoutController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        HttpSession session = req.getSession(false);
        if (session != null) {
            session.invalidate(); // 서버 세션 저장소에서 해당 세션을 무효화
        }

        resp.sendRedirect(req.getContextPath() + &quot;/login-form&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invalidate()의 의미는 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 저장소에서 &amp;ldquo;그 세션 한 칸&amp;rdquo;을 폐기한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 브라우저가 기존 JSESSIONID 쿠키를 들고 다시 요청해도&lt;br /&gt;서버는 세션을 못 찾아서 새 세션을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실무에서는 쿠키도 지우도록 응답을 추가하기도 하지만, 핵심은 서버 세션 무효화이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSESSIONID는 쿠키 이름이고, 그 값(value)이 세션ID이다&lt;/li&gt;
&lt;li&gt;세션ID는 서버 세션 저장소에서 세션을 찾는 &amp;ldquo;키&amp;rdquo; 역할을 한다&lt;/li&gt;
&lt;li&gt;session.getId() 값이 세션ID이고, JSESSIONID 쿠키 값과 연결된다&lt;/li&gt;
&lt;li&gt;JSESSIONID가 있다고 로그인된 게 아니다(로그인 여부는 session attribute로 판단)&lt;/li&gt;
&lt;li&gt;세션은 서버에서 timeout으로 만료되고, 로그아웃은 보통 invalidate로 처리한다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/109</guid>
      <comments>https://woojoo-devlog.tistory.com/109#entry109comment</comments>
      <pubDate>Mon, 30 Mar 2026 10:12:40 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통처리]  2-1. req.getSession()과 JSESSIONID, HttpSession</title>
      <link>https://woojoo-devlog.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;ldquo;세션은 어디에 생기고, 쿠키는 언제 내려가고, HttpSession은 정확히 무엇인가&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 공부하다 아래와 같은 궁금증이 들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;session.setAttribute(&quot;loginUser&quot;, id)를 하면 Set-Cookie: JSESSIONID=...가 내려가는 건가?&lt;/li&gt;
&lt;li&gt;req.getSession()은 &amp;ldquo;세션 저장소&amp;rdquo;를 만드는 건가, &amp;ldquo;JSESSIONID 값&amp;rdquo;을 만드는 건가?&lt;/li&gt;
&lt;li&gt;새 세션 공간이 HttpSession 자체인가, 아니면 저장소 안의 한 칸이 따로 있는 건가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키(JSESSIONID)와 세션(HttpSession)은 같은 게 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 기반 로그인에서 항상 같이 등장하는 두 개념은 역할이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JSESSIONID(쿠키의 값)&lt;/b&gt;: 브라우저가 들고 다니는 &amp;ldquo;열쇠(식별자)&amp;rdquo;이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 저장소(서버)&lt;/b&gt;: 서버(톰캣) 메모리에 있는 &amp;ldquo;사물함 보관소&amp;rdquo;이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 한 칸(개별 세션)&lt;/b&gt;: 사물함 보관소 안의 &amp;ldquo;사용자별 칸&amp;rdquo;이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HttpSession&lt;/b&gt;: 개발자가 자바 코드에서 그 &amp;ldquo;한 칸&amp;rdquo;을 다루도록 주어지는 핸들(표준 인터페이스)이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 쿠키에 로그인 정보를 직접 넣는 게 아니라&lt;br /&gt;쿠키에는 &amp;ldquo;세션ID(열쇠 번호)&amp;rdquo;만 담고,&lt;br /&gt;실제 로그인 정보는 서버 세션 저장소에 담는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;session.setAttribute(&quot;loginUser&quot;, id)는 쿠키를 만드는 코드가 아니다&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;session.setAttribute(&quot;loginUser&quot;, id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 &lt;b&gt;쿠키를 내려주는 코드가 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 딱 한 가지 동작만 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;서버의 세션 저장소에서 내 세션 한 칸에 loginUser=id를 저장한다&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, setAttribute는 세션 내부에 데이터를 넣는 동작이다.&lt;br /&gt;쿠키(JSESSIONID)를 만들거나 내려주는 역할은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 쿠키는 언제 내려가나?&lt;br /&gt;그 트리거가 바로 req.getSession()이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;req.getSession()은 정확히 뭘 하냐: (있으면 찾고, 없으면 만든다)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;req.getSession()은 한 문장으로 요약하면 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;요청이 들고 온 JSESSIONID로 기존 세션을 찾아서 돌려주고, 없으면 새 세션을 만들고 새 JSESSIONID까지 발급한다&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;찾기 + 만들기&amp;rdquo;가 같이 들어있다.&lt;br /&gt;단, &lt;b&gt;세션이 없는 경우에만 만들기 로직이 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;ldquo;세션이 없다&amp;rdquo;의 의미는 뭔가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣은 요청을 받으면 먼저 확인한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청 헤더에 Cookie: JSESSIONID=...가 있는가?&lt;/li&gt;
&lt;li&gt;그 값으로 서버 세션 저장소에서 세션을 실제로 찾을 수 있는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료/삭제된 세션이면 못 찾는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 하나라도 실패하면 &amp;ldquo;세션이 없다&amp;rdquo;로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;케이스 A: JSESSIONID가 없거나 유효하지 않을 때&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;HttpSession session = req.getSession();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄로 톰캣은 &lt;b&gt;두 가지를 같이&lt;/b&gt; 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 세션 저장소에 새 세션 한 칸을 만든다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 한 칸을 찾기 위한 세션ID를 만들고(=JSESSIONID 값), 응답에 Set-Cookie로 내려줄 준비를 한다&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이때는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 저장소 &amp;ldquo;한 칸&amp;rdquo;이 새로 생기고&lt;/li&gt;
&lt;li&gt;그 칸을 가리키는 &amp;ldquo;열쇠 번호&amp;rdquo;가 새로 생긴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 응답이 나갈 때 브라우저로 이런 헤더가 내려간다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Set-Cookie: JSESSIONID=새값; Path=/; ...&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;케이스 B: JSESSIONID가 이미 있고 유효할 때&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;HttpSession session = req.getSession();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &amp;ldquo;만드는 것&amp;rdquo;이 아니라 &amp;ldquo;찾는 것&amp;rdquo;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저가 Cookie: JSESSIONID=기존값을 붙여서 보낸다&lt;/li&gt;
&lt;li&gt;톰캣이 그 값으로 세션 저장소에서 세션 한 칸을 찾는다&lt;/li&gt;
&lt;li&gt;찾은 세션을 HttpSession 형태로 돌려준다&lt;/li&gt;
&lt;li&gt;새로 만들 게 없으니 Set-Cookie를 굳이 다시 내릴 필요가 없다(일반적인 흐름)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 &amp;ldquo;새 세션 공간&amp;rdquo;은 HttpSession 자체인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 저장소 안에는 &amp;ldquo;세션 한 칸(세션 엔트리)&amp;rdquo;이 실제로 존재한다&lt;/li&gt;
&lt;li&gt;개발자가 자바 코드에서 만지는 것은 그 한 칸을 조작하기 위한 표준 인터페이스인 HttpSession이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구조는 이렇게 이해하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;세션 저장소(Map 같은 구조)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key = 세션ID (쿠키 JSESSIONID의 값)&lt;/li&gt;
&lt;li&gt;value = 세션 한 칸(Session 엔트리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 개발자에게는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 value(세션 한 칸)를 조작할 수 있는 핸들로 HttpSession이 제공된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;같냐?&amp;rdquo;라고 물으면 엄밀히는 구현체가 따로 있지만, 학습 관점에서는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 한 칸(서버 엔트리) 1개 &amp;harr; HttpSession 1개가 대응된다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 흐름을 타임라인으로 보면 완전히 정리된다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 첫 로그인 요청(브라우저에 JSESSIONID 없음)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /login (Cookie 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 코드:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HttpSession session = req.getSession();     // ① 세션 없으니 새 세션 생성 + 새 세션ID 발급 준비
session.setAttribute(&quot;loginUser&quot;, id);      // ② 서버 세션 한 칸에 loginUser 저장
resp.sendRedirect(&quot;/home&quot;);                 // ③ 응답 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답이 나갈 때:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Set-Cookie: JSESSIONID=...가 내려간다 (①에서 새 세션을 만들었으니까)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSESSIONID 쿠키를 저장한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 다음 요청부터(브라우저가 JSESSIONID를 자동 전송)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /home 요청&lt;/li&gt;
&lt;li&gt;요청 헤더에 Cookie: JSESSIONID=... 자동 첨부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSESSIONID로 세션 저장소에서 세션 한 칸을 찾는다&lt;/li&gt;
&lt;li&gt;session.getAttribute(&quot;loginUser&quot;)로 로그인 상태를 확인할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getSession() vs getSession(false)가 중요한 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getSession()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 없으면 &lt;b&gt;새로 만든다&lt;/b&gt; (세션 한 칸 + 세션ID + Set-Cookie 준비)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;getSession(false)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 없으면 &lt;b&gt;null을 반환한다&lt;/b&gt; (새로 만들지 않는다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 체크 같은 곳에서 getSession()을 무심코 쓰면&lt;br /&gt;로그인 안 한 사용자에게도 세션을 쓸데없이 만들어서 서버 메모리를 낭비할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;체크&amp;rdquo;는 보통 getSession(false)가 더 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;session.setAttribute(...)는 &lt;b&gt;세션 데이터 저장&lt;/b&gt;이지, 쿠키 발급이 아니다&lt;/li&gt;
&lt;li&gt;req.getSession()은
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;있으면 기존 세션을 &lt;b&gt;찾고&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;없으면 서버에 세션 한 칸을 &lt;b&gt;만들고&lt;/b&gt; + 새 세션ID(JSESSIONID 값)를 &lt;b&gt;발급&lt;/b&gt;한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세션 저장소 안의 &amp;ldquo;개별 세션 한 칸&amp;rdquo;이 실제 세션이고&lt;br /&gt;HttpSession은 그 한 칸을 자바 코드에서 다루기 위한 핸들(인터페이스)이다&lt;/li&gt;
&lt;li&gt;세션ID는 쿠키(JSESSIONID)로 브라우저가 들고 다니는 &amp;ldquo;열쇠&amp;rdquo; 역할이다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/108</guid>
      <comments>https://woojoo-devlog.tistory.com/108#entry108comment</comments>
      <pubDate>Mon, 30 Mar 2026 09:47:10 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  2. 세션이란?</title>
      <link>https://woojoo-devlog.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키는 &amp;ldquo;열쇠&amp;rdquo;, 데이터는 &amp;ldquo;서버에 보관&amp;rdquo;한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 상태 유지를 가능하게 하지만 한계가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에 저장되므로 노출/변조 위험이 있다&lt;/li&gt;
&lt;li&gt;큰 데이터를 넣기 어렵다(문자열, 크기 제한)&lt;/li&gt;
&lt;li&gt;중요한 사용자 상태를 쿠키에 담는 건 위험하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키에는 &amp;ldquo;식별자&amp;rdquo;만 넣고&lt;br /&gt;실제 사용자 상태/데이터는 서버에 저장한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 세션(session)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션의 핵심 구조: 세션ID로 서버 저장소를 찾는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 결국 이런 구조이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 사용자별 &amp;ldquo;저장 공간&amp;rdquo;을 가진다(세션 저장소)&lt;/li&gt;
&lt;li&gt;각 공간은 세션ID로 구분된다&lt;/li&gt;
&lt;li&gt;브라우저는 세션ID를 쿠키로 들고 다닌다&lt;/li&gt;
&lt;li&gt;요청이 오면 서버는 세션ID로 세션 공간을 찾아준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 세션은 &amp;ldquo;브라우저 저장(쿠키)&amp;rdquo;과 &amp;ldquo;서버 저장(세션 저장소)&amp;rdquo;을 조합한 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;톰캣에서 세션이 자동으로 만들어지는 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣 같은 서블릿 컨테이너는 세션을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 아래를 호출하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getSession()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣은 다음을 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청에 세션ID 쿠키가 있는지 확인한다&lt;/li&gt;
&lt;li&gt;없으면 새 세션을 만들고 세션ID를 발급한다&lt;/li&gt;
&lt;li&gt;응답에 Set-Cookie: JSESSIONID=...로 내려준다&lt;/li&gt;
&lt;li&gt;다음 요청부터 브라우저는 Cookie: JSESSIONID=...를 자동으로 붙인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;세션은 쿠키(JSESSIONID) 없이 동작할 수 없다&amp;rdquo;는 구조이다.&lt;br /&gt;쿠키가 세션을 찾는 열쇠 역할을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774828145993&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [세션 사용 예시] 로그인 성공 시 세션에 사용자 정보를 저장
// ================================

@WebServlet(&quot;/login&quot;)
public class LoginController extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String id = req.getParameter(&quot;id&quot;);
        String pw = req.getParameter(&quot;pw&quot;);

        // 예시: 인증 성공했다고 가정
        boolean ok = &quot;user1&quot;.equals(id) &amp;amp;&amp;amp; &quot;1234&quot;.equals(pw);

        if (ok) {
            // 세션이 없으면 생성
            HttpSession session = req.getSession();
            session.setAttribute(&quot;loginUser&quot;, id); // 세션에 로그인 정보 저장

            resp.sendRedirect(req.getContextPath() + &quot;/home&quot;);
        } else {
            resp.sendRedirect(req.getContextPath() + &quot;/login-form&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 저장소는 서버에 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션의 데이터는 브라우저에 저장되지 않는다.&lt;br /&gt;서버(톰캣) 메모리의 세션 저장소에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 세션은 다음 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중요한 데이터를 브라우저에 노출하지 않는다&lt;/li&gt;
&lt;li&gt;객체를 그대로 저장할 수 있다(문자열만이 아님)&lt;/li&gt;
&lt;li&gt;로그인 상태 유지에 적합하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 단점도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 메모리를 사용한다&lt;/li&gt;
&lt;li&gt;사용자가 많으면 세션이 쌓인다&lt;/li&gt;
&lt;li&gt;그래서 만료/정리가 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션은 언제까지 유지되는가: timeout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 영원히 유지되지 않는다.&lt;br /&gt;컨테이너는 일정 시간 동안 요청이 없으면 세션을 만료시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;오래 안 쓰는 세션은 정리해서 메모리를 비운다&amp;rdquo;가 기본 전략이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣은 기본적으로 세션 타임아웃을 가지고 있고&lt;br /&gt;주기적으로 오래된 세션을 청소한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션으로 로그인 상태를 유지하는 전형적인 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 이용한 로그인 체크는 보통 이렇게 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 로그인 성공 시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getSession()으로 세션을 얻고(없으면 생성)&lt;/li&gt;
&lt;li&gt;session.setAttribute(&quot;loginUser&quot;, user)로 로그인 정보를 저장한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 보호된 페이지 접근 시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getSession(false)로 세션을 얻는다(없으면 null)&lt;/li&gt;
&lt;li&gt;session.getAttribute(&quot;loginUser&quot;)가 있으면 로그인으로 판단한다&lt;/li&gt;
&lt;li&gt;없으면 로그인 페이지로 redirect 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 세션 기반 로그인 유지의 기본 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getSession() vs getSession(false)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 실전에서 중요한 차이가 하나 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getSession()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 없으면 새로 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;getSession(false)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 없으면 null을 반환한다(새로 만들지 않는다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 체크처럼 &amp;ldquo;없으면 없는 대로 처리&amp;rdquo;해야 하는 상황에서는&lt;br /&gt;보통 getSession(false)를 쓰는 게 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키는 브라우저에 저장되므로 중요한 데이터를 담기 어렵다&lt;/li&gt;
&lt;li&gt;세션은 쿠키(세션ID)로 서버 저장소를 찾아오는 구조이다&lt;/li&gt;
&lt;li&gt;톰캣은 request.getSession() 호출 시 JSESSIONID를 발급하고 세션을 만든다&lt;/li&gt;
&lt;li&gt;로그인 상태 유지는 보통 session.setAttribute로 구현한다&lt;/li&gt;
&lt;li&gt;getSession(false)는 세션이 없으면 null(불필요한 세션 생성 방지)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/107</guid>
      <comments>https://woojoo-devlog.tistory.com/107#entry107comment</comments>
      <pubDate>Mon, 30 Mar 2026 09:20:21 +0900</pubDate>
    </item>
    <item>
      <title>[상태 유지와 공통 처리]  1. 쿠키란?</title>
      <link>https://woojoo-devlog.tistory.com/106</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 무상태를 &amp;ldquo;브라우저 저장소&amp;rdquo;로 보완한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 무상태(stateless)라고 했다.&lt;br /&gt;즉 서버는 기본적으로 &amp;ldquo;이전 요청이 누가 보낸 건지&amp;rdquo;를 기억하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 로그인, 장바구니, 사용자 맞춤 화면 같은 기능은&lt;br /&gt;이전 상태를 이어서 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 웹에는 상태 유지를 위한 메커니즘이 필요하고,&lt;br /&gt;그 출발점이 쿠키(cookie)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키는 &amp;ldquo;서버와 브라우저 사이를 오가는 작은 문자열 데이터&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 기본적으로 이름과 값으로 이루어진 데이터 조각이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름(name)&lt;/li&gt;
&lt;li&gt;값(value)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿠키는 브라우저에 저장되고,&lt;br /&gt;이후 요청에서 자동으로 서버에 다시 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 쿠키는 &lt;b&gt;브라우저가 들고 다니는 상태 정보&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키가 오고 가는 방식은 헤더로 결정된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 HTTP 메시지의 헤더로 주고받는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 &amp;rarr; 브라우저: Set-Cookie 헤더로 쿠키 발급&lt;/li&gt;
&lt;li&gt;브라우저 &amp;rarr; 서버: Cookie 헤더로 쿠키 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버는 &amp;ldquo;응답(response)&amp;rdquo;에서 쿠키를 내려주고,&lt;br /&gt;브라우저는 &amp;ldquo;다음 요청(request)&amp;rdquo;에서 그 쿠키를 붙여서 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키 기본 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 보통 이런 순서로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;첫 요청: 브라우저에 쿠키가 없으면 Cookie 헤더 없이 요청한다&lt;/li&gt;
&lt;li&gt;서버 응답: 서버가 Set-Cookie로 쿠키를 내려준다&lt;/li&gt;
&lt;li&gt;브라우저 저장: 브라우저가 쿠키를 저장한다&lt;/li&gt;
&lt;li&gt;다음 요청: 브라우저가 Cookie 헤더로 쿠키를 자동 첨부한다&lt;/li&gt;
&lt;li&gt;서버는 쿠키 값을 읽어서 사용자 상태를 추적한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 쿠키는 HTTP의 무상태성을 &amp;ldquo;브라우저 저장&amp;rdquo;으로 보완하는 장치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키는 서버가 발급하지만, 저장과 전송은 브라우저가 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 오해하면 안 되는 포인트가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 쿠키를 &amp;ldquo;발급&amp;rdquo;한다(Set-Cookie)&lt;/li&gt;
&lt;li&gt;브라우저는 쿠키를 &amp;ldquo;저장&amp;rdquo;한다&lt;/li&gt;
&lt;li&gt;브라우저는 조건이 맞으면 쿠키를 &amp;ldquo;자동으로 전송&amp;rdquo;한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버가 매 요청마다 쿠키를 직접 들고 오는 게 아니라,&lt;br /&gt;브라우저가 알아서 붙여서 보내준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠키 옵션이 중요한 이유: 언제, 어디로 전송되는지가 결정된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 &amp;ldquo;그냥 항상 다 붙는 문자열&amp;rdquo;이 아니다.&lt;br /&gt;대표적으로 아래 옵션이 쿠키 동작을 바꾼다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Path: 어떤 경로 요청에 쿠키를 붙일지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/로 두면 사이트 전체에 붙는다&lt;/li&gt;
&lt;li&gt;/todo로 두면 /todo/... 요청에만 붙는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Max-Age: 쿠키가 얼마나 유지될지(초 단위)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정하면 브라우저가 디스크에 저장(지속 쿠키)&lt;/li&gt;
&lt;li&gt;설정하지 않거나 -1이면 브라우저 종료 시 삭제(세션 쿠키)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;쿠키가 왜 안 붙지?&amp;rdquo; 문제의 상당수는&lt;br /&gt;Path/Max-Age 설정에서 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774826972304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [서버에서 쿠키 발급] 응답에 Set-Cookie가 포함되게 만든다
// ================================

@WebServlet(&quot;/cookie/set&quot;)
public class CookieSetServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        Cookie cookie = new Cookie(&quot;theme&quot;, &quot;dark&quot;); // 이름=값

        cookie.setPath(&quot;/&quot;);          // 사이트 전체에서 전송되게
        cookie.setMaxAge(60 * 60);    // 1시간(초 단위). -1이면 브라우저 종료 시 삭제(세션 쿠키)

        resp.addCookie(cookie);       // Set-Cookie 헤더로 내려감
        resp.setContentType(&quot;text/plain; charset=UTF-8&quot;);
        resp.getWriter().println(&quot;cookie set: theme=dark&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774826991621&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [서버에서 쿠키 읽기] 브라우저가 Cookie 헤더로 보내온 쿠키를 읽는다
// ================================

@WebServlet(&quot;/cookie/read&quot;)
public class CookieReadServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String theme = null;

        Cookie[] cookies = req.getCookies(); // 없으면 null일 수도 있다
        if (cookies != null) {
            for (Cookie c : cookies) {
                if (&quot;theme&quot;.equals(c.getName())) {
                    theme = c.getValue();
                    break;
                }
            }
        }

        resp.setContentType(&quot;text/plain; charset=UTF-8&quot;);
        resp.getWriter().println(&quot;theme = &quot; + theme);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP는 무상태라서 상태 유지가 필요하다&lt;/li&gt;
&lt;li&gt;쿠키는 브라우저에 저장되는 작은 문자열 데이터이다&lt;/li&gt;
&lt;li&gt;서버는 Set-Cookie로 발급하고, 브라우저는 Cookie 헤더로 자동 전송한다&lt;/li&gt;
&lt;li&gt;Path/Max-Age 같은 옵션이 쿠키 전송 조건을 결정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 쿠키는 브라우저에 저장되기 때문에 한계가 있고,&lt;br /&gt;그 한계를 보완하기 위해 서버 쪽 저장소를 쓰는 방식이 세션이다.&lt;/p&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/106</guid>
      <comments>https://woojoo-devlog.tistory.com/106#entry106comment</comments>
      <pubDate>Mon, 30 Mar 2026 08:34:54 +0900</pubDate>
    </item>
    <item>
      <title>[MVC와 웹 흐름 제어]  7. WEB-INF에 JSP를 두는 이유: &amp;ldquo;JSP 직접 호출&amp;rdquo;을 구조적으로 막는다</title>
      <link>https://woojoo-devlog.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC 구조를 제대로 만들려면 한 가지를 강제로 지켜야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저는 JSP를 직접 호출하지 않는다&lt;/li&gt;
&lt;li&gt;브라우저는 Controller(서블릿) URL만 호출한다&lt;/li&gt;
&lt;li&gt;Controller가 처리한 뒤 JSP(View)로 forward 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는 JSP가 파일 경로 기반으로 접근 가능하면&lt;br /&gt;사용자가 주소창에 JSP URL을 직접 입력해서 들어올 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 &amp;ldquo;규칙&amp;rdquo;으로만 막으려 하면 항상 구멍이 생긴다.&lt;br /&gt;그래서 웹 애플리케이션에는 아예 &amp;ldquo;직접 접근이 불가능한 보호 영역&amp;rdquo;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 WEB-INF이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WEB-INF는 브라우저에서 직접 접근할 수 없는 특별한 경로이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WEB-INF 아래에 있는 자원은&lt;br /&gt;브라우저의 외부 요청으로 직접 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 아래 같은 URL을 브라우저에서 직접 입력해도 실행되지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/WEB-INF/views/todo/list.jsp&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성 때문에 WEB-INF는 &amp;ldquo;외부 접근 차단 구역&amp;rdquo;으로 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JSP를 WEB-INF 아래에 두면 뭐가 달라지나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP가 일반 경로에 있으면 이런 일이 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 JSP URL만 알면 직접 GET 요청으로 들어온다&lt;/li&gt;
&lt;li&gt;Controller를 거치지 않는다&lt;/li&gt;
&lt;li&gt;GET/POST 제어, 데이터 준비, 권한 체크 같은 흐름이 무너진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 JSP를 WEB-INF 아래에 두면 구조가 바뀐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저는 JSP에 직접 접근할 수 없다&lt;/li&gt;
&lt;li&gt;Controller만 RequestDispatcher.forward로 접근할 수 있다&lt;/li&gt;
&lt;li&gt;즉 &lt;b&gt;반드시 Controller를 거치게&lt;/b&gt;&amp;nbsp;강제된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 MVC 구조를 코드 규칙이 아니라 &lt;b&gt;배치 구조&lt;/b&gt;로 보장해주는 장치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WEB-INF는 &amp;ldquo;View는 내부 자원&amp;rdquo;이라는 의도를 강제한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC에서 View(JSP)는 사용자에게 노출되는 엔드포인트가 아니다.&lt;br /&gt;View는 Controller가 내부적으로 선택해서 렌더링하는 내부 자원이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WEB-INF 아래로 JSP를 숨기면&lt;br /&gt;이 의도가 구조적으로 강제된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL 설계는 Controller 기준으로 한다&lt;/li&gt;
&lt;li&gt;JSP 파일명/폴더 구조는 내부 구현이다&lt;/li&gt;
&lt;li&gt;화면 파일이 바뀌어도 외부 URL은 유지할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 유지보수에 유리해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전에서 흔한 배치 형태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MVC 구조에서는 JSP를 보통 이런 위치에 둔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/WEB-INF/views/...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Controller는 항상 이 경로로 forward 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getRequestDispatcher(&quot;/WEB-INF/views/...&quot;).forward(...)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름이 &amp;ldquo;브라우저 &amp;rarr; Controller &amp;rarr; View&amp;rdquo; 구조를 안정적으로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WEB-INF는 브라우저에서 직접 접근할 수 없는 보호 영역이다&lt;/li&gt;
&lt;li&gt;JSP를 WEB-INF 아래에 두면 JSP 직접 호출을 구조적으로 차단할 수 있다&lt;/li&gt;
&lt;li&gt;결과적으로 브라우저는 반드시 Controller를 통해서만 화면을 보게 된다&lt;/li&gt;
&lt;li&gt;URL은 Controller 기준으로 설계되고, JSP는 내부 구현으로 숨겨진다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/105</guid>
      <comments>https://woojoo-devlog.tistory.com/105#entry105comment</comments>
      <pubDate>Sun, 29 Mar 2026 22:24:38 +0900</pubDate>
    </item>
    <item>
      <title>[MVC와 웹 흐름 제어]  6.PRG 패턴: POST 후에는 왜 redirect를 하는가</title>
      <link>https://woojoo-devlog.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록/수정/삭제 같은 기능을 만들면 보통 POST 요청을 사용한다.&lt;br /&gt;문제는 &amp;ldquo;POST 처리 후에 무엇을 응답으로 보여줄 것인가&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보 단계에서는 이런 흐름을 만들기 쉽다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST로 처리한다&lt;/li&gt;
&lt;li&gt;처리 결과 화면을 바로 보여준다(HTML)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 잘 동작한다.&lt;br /&gt;하지만 브라우저의 &amp;ldquo;새로고침&amp;rdquo; 동작 때문에 구조적인 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRG가 없으면 발생하는 문제: POST 재전송&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST 처리 후에 결과 화면을 바로 응답하면,&lt;br /&gt;브라우저 입장에서는 &amp;ldquo;현재 화면을 만든 마지막 요청&amp;rdquo;이 POST가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 사용자가 새로고침(F5)을 하면 브라우저는 보통 이렇게 행동한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 요청이 POST였다&lt;/li&gt;
&lt;li&gt;그 POST를 다시 보내서 현재 화면을 다시 만들려고 한다&lt;/li&gt;
&lt;li&gt;그래서 &amp;ldquo;POST 재전송&amp;rdquo; 경고가 뜬다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 단순히 경고로 끝나면 괜찮은데, 실전에서는 문제가 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글이 중복 등록될 수 있다&lt;/li&gt;
&lt;li&gt;주문이 중복으로 생성될 수 있다&lt;/li&gt;
&lt;li&gt;결제가 중복으로 시도될 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;새로고침&amp;rdquo;이 치명적인 부작용을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRG는 이 문제를 구조로 해결한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRG는 Post-Redirect-Get의 약자이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 단순하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자는 POST로 처리를 요청한다&lt;/li&gt;
&lt;li&gt;서버는 처리를 완료한 뒤 &amp;ldquo;다른 URL로 이동하라&amp;rdquo;는 redirect 응답을 보낸다&lt;/li&gt;
&lt;li&gt;브라우저는 redirect를 따라 GET 요청을 새로 보낸다&lt;/li&gt;
&lt;li&gt;최종 화면은 GET 응답으로 만들어진다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 화면을 결정하는 마지막 요청이 POST가 아니라 GET이 되게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRG의 핵심 효과: 새로고침이 안전해진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRG를 적용하면 마지막 화면은 GET의 결과이다.&lt;br /&gt;그래서 사용자가 새로고침을 해도 GET이 다시 실행될 뿐이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET은 조회 성격이므로 부작용이 적다&lt;/li&gt;
&lt;li&gt;중복 등록/중복 결제 같은 위험을 크게 줄인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 PRG는 &amp;ldquo;새로고침에 안전한 흐름&amp;rdquo;을 만드는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드로 보면 차이가 더 명확하다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나쁜 흐름(POST 후 결과 화면을 바로 보여줌)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 처리 후 forward로 JSP 결과 화면을 응답&lt;/li&gt;
&lt;li&gt;마지막 요청이 POST가 되어서 새로고침 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 흐름(PRG)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 처리 후 sendRedirect로 목록/상세 같은 GET URL로 이동&lt;/li&gt;
&lt;li&gt;마지막 요청이 GET이 되어서 새로고침이 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 실무에서 &amp;ldquo;POST 처리 후 redirect&amp;rdquo;가 기본이 되는 이유이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 응답으로 바로 화면을 만들면 새로고침 시 POST 재전송이 발생할 수 있다&lt;/li&gt;
&lt;li&gt;PRG는 POST 이후 redirect를 통해 마지막 화면을 GET으로 만든다&lt;/li&gt;
&lt;li&gt;PRG를 쓰면 새로고침이 안전해지고 중복 처리 위험이 줄어든다&lt;/li&gt;
&lt;li&gt;그래서 등록/수정/삭제 같은 처리는 보통 PRG로 구성한다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/104</guid>
      <comments>https://woojoo-devlog.tistory.com/104#entry104comment</comments>
      <pubDate>Sun, 29 Mar 2026 21:56:25 +0900</pubDate>
    </item>
    <item>
      <title>[MVC와 웹 흐름 제어]  5. forward vs redirect: 언제 무엇을 써야 하는가</title>
      <link>https://woojoo-devlog.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC에서 흐름 제어를 하다 보면 결국 매번 이 선택을 하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward로 JSP를 보여줄 것인가&lt;/li&gt;
&lt;li&gt;redirect로 다른 URL로 이동시킬 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 차이를 정확히 잡아두면&lt;br /&gt;&amp;ldquo;왜 PRG가 필요한지&amp;rdquo;, &amp;ldquo;왜 WEB-INF에 JSP를 두는지&amp;rdquo;도 자연스럽게 이해된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄로 정리하면 이렇게 된다&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward: 서버 내부에서 같은 요청을 들고 View(JSP)로 넘긴다&lt;/li&gt;
&lt;li&gt;redirect: 브라우저에게 새 요청을 보내게 해서 다른 URL로 이동시킨다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 핵심 차이는 &amp;ldquo;request가 유지되느냐, 끊기느냐&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 이동 주체가 다르다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;forward&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 내부 이동이다.&lt;br /&gt;컨트롤러가 RequestDispatcher로 JSP에게 처리를 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 이 사실을 모른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redirect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 이동이다.&lt;br /&gt;서버가 Location 헤더를 보내고, 브라우저가 그 주소로 다시 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) request/response의 연속성이 다르다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;forward는 request/response가 유지된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러에서 request.setAttribute()로 담아둔 데이터가&lt;br /&gt;JSP에서 그대로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;조회 화면 렌더링&amp;rdquo;에서 가장 자연스럽다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redirect는 request/response가 끊긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 새 요청을 보내므로 request/response는 새로 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러에서 request에 담아둔 값은 redirect 이후에 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect 후에 값을 넘기려면 다른 전략을 써야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 스트링으로 붙이기&lt;/li&gt;
&lt;li&gt;세션에 저장하기&lt;/li&gt;
&lt;li&gt;(프레임워크가 있으면) flash attribute 같은 임시 저장소 쓰기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 주소창(URL)이 다르다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;forward는 URL이 안 바뀐다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 내부에서만 이동했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에서는&lt;br /&gt;&amp;ldquo;처리된 JSP 화면을 보고 있지만 주소는 여전히 컨트롤러 URL&amp;rdquo;인 상태가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redirect는 URL이 바뀐다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 Location으로 새 요청을 보내기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에서는&lt;br /&gt;&amp;ldquo;주소창이 실제 화면에 해당하는 URL로 바뀐 상태&amp;rdquo;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 언제 무엇을 쓰는가 (실무 기준)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;forward가 자연스러운 경우: 조회 화면&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목록 조회 화면&lt;/li&gt;
&lt;li&gt;상세 조회 화면&lt;/li&gt;
&lt;li&gt;입력 폼 화면(GET)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;화면을 만들어 보여주는 목적&amp;rdquo;일 때 forward가 기본이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;redirect가 자연스러운 경우: 처리 후 이동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;등록 처리(POST) 후 목록으로 이동&lt;/li&gt;
&lt;li&gt;수정 처리(POST) 후 상세로 이동&lt;/li&gt;
&lt;li&gt;삭제 처리(POST) 후 목록으로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &amp;ldquo;상태를 바꾸는 처리&amp;rdquo;를 하고 난 뒤에는 redirect가 기본이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴이 바로 PRG(Post-Redirect-Get)로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward: 서버 내부 이동, request 유지, URL 안 바뀜, setAttribute로 데이터 전달 가능&lt;/li&gt;
&lt;li&gt;redirect: 브라우저 새 요청, request 끊김, URL 바뀜, setAttribute로 데이터 전달 불가&lt;/li&gt;
&lt;li&gt;조회(화면 렌더링)는 forward가 자연스럽고&lt;/li&gt;
&lt;li&gt;등록/수정/삭제 같은 처리 후에는 redirect가 자연스럽다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/103</guid>
      <comments>https://woojoo-devlog.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 29 Mar 2026 21:47:49 +0900</pubDate>
    </item>
    <item>
      <title>[MVC와 웹 흐름 제어]  4. redirect(sendRedirect)</title>
      <link>https://woojoo-devlog.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;ldquo;브라우저에게 새 요청을 시키는 방식&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 서버 내부 이동이었다.&lt;br /&gt;즉 같은 request/response를 들고 JSP로 넘기는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect는 완전히 다르다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect는 서버가 브라우저에게 &amp;ldquo;다른 곳으로 다시 요청하라&amp;rdquo;고 지시하는 방식이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 redirect는 &amp;ldquo;서버 내부에서 처리 담당자를 바꾸는 것&amp;rdquo;이 아니라&lt;br /&gt;&amp;ldquo;브라우저에게 새 요청을 유도하는 것&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;sendRedirect는 무엇을 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 보통 이렇게 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;response.sendRedirect(&quot;/todo/list&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄의 의미는 다음이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 응답을 만들 때 Location 헤더를 포함해서 보낸다&lt;/li&gt;
&lt;li&gt;브라우저는 이 응답을 받으면 화면을 그대로 유지하지 않는다&lt;/li&gt;
&lt;li&gt;주소창을 Location 값으로 바꾸고, 그 주소로 새 요청을 보낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 redirect는 1번 요청으로 끝나는 게 아니라&lt;br /&gt;&amp;ldquo;추가 요청&amp;rdquo;이 자동으로 발생하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;redirect의 가장 중요한 특징: request가 끊긴다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward와 redirect의 가장 큰 차이는 request의 연속성이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward: 같은 request/response 유지&lt;/li&gt;
&lt;li&gt;redirect: 브라우저가 새 요청을 보내므로 request/response가 새로 만들어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 redirect 이후에는 컨트롤러에서 request.setAttribute()로 담아둔 값이 유지되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 그 값은 &amp;ldquo;이전 요청&amp;rdquo;의 request scope에 있던 것이고,&lt;br /&gt;redirect는 &amp;ldquo;새 요청&amp;rdquo;을 발생시키기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 다음은 성립하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST 컨트롤러에서 req.setAttribute(&quot;msg&quot;,&quot;ok&quot;)&lt;/li&gt;
&lt;li&gt;redirect 이후 JSP에서 ${msg} 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 request가 끊겨서 값이 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(redirect 후에도 값을 넘기려면 쿼리 스트링, 세션, flash scope 같은 전략이 필요하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;redirect는 언제 쓰는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect는 보통 &amp;ldquo;처리 작업&amp;rdquo; 이후에 많이 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 것들이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;등록(POST) 처리 후 목록(GET)으로 이동&lt;/li&gt;
&lt;li&gt;수정(POST) 처리 후 상세(GET)로 이동&lt;/li&gt;
&lt;li&gt;삭제(POST) 처리 후 목록(GET)으로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴이 중요한 이유는 다음 글에서 다룰 PRG(Post-Redirect-Get)로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;redirect는 브라우저 입장에서 어떤 일이 일어나는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect 응답을 받으면 브라우저는 이렇게 행동한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 응답에 Location이 있는지 확인한다&lt;/li&gt;
&lt;li&gt;주소창을 Location 값으로 바꾼다&lt;/li&gt;
&lt;li&gt;Location으로 새 요청을 보낸다(보통 GET)&lt;/li&gt;
&lt;li&gt;새 응답을 받아서 화면을 만든다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 redirect를 쓰면 사용자 입장에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주소창이 바뀐다&lt;/li&gt;
&lt;li&gt;새로고침을 해도 &amp;ldquo;마지막이 GET&amp;rdquo;인 상태로 남기 쉬워진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 장점 때문에 처리 후 redirect가 자주 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward는 서버 내부 이동이다(request 유지, URL 안 바뀜)&lt;/li&gt;
&lt;li&gt;redirect는 브라우저가 새 요청을 보내는 방식이다(request 끊김, URL 바뀜)&lt;/li&gt;
&lt;li&gt;sendRedirect는 Location 헤더로 브라우저를 다른 URL로 이동시킨다&lt;/li&gt;
&lt;li&gt;처리(POST) 후에는 redirect가 자주 쓰이고, 이는 PRG 패턴으로 이어진다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/102</guid>
      <comments>https://woojoo-devlog.tistory.com/102#entry102comment</comments>
      <pubDate>Sun, 29 Mar 2026 20:20:29 +0900</pubDate>
    </item>
    <item>
      <title>[MVC와 웹 흐름 제어]  3. RequestDispatcher와 forward</title>
      <link>https://woojoo-devlog.tistory.com/101</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;ldquo;같은 요청을 들고 JSP로 넘긴다&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC에서 Controller(서블릿)는 보통 두 가지 중 하나를 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View(JSP)로 화면을 만들어서 응답한다&lt;/li&gt;
&lt;li&gt;다른 URL로 이동시키도록 응답한다(redirect)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 먼저 MVC에서 가장 기본이 되는 방식인 &lt;b&gt;forward&lt;/b&gt;부터 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RequestDispatcher는 &amp;ldquo;요청을 다른 자원에게 넘기는 통로&amp;rdquo;이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RequestDispatcher는 말 그대로 &amp;ldquo;배포/전달&amp;rdquo; 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러가 처리하던 요청을&lt;br /&gt;JSP 같은 다른 자원에게 넘겨서 최종 응답을 만들게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 입장에서는 RequestDispatcher를 이렇게 얻는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request.getRequestDispatcher(&quot;JSP 경로&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 forward로 넘긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dispatcher.forward(request, response)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward의 가장 중요한 특징: request/response가 그대로 유지된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward를 이해할 때 핵심은 이 한 줄이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 같은 request/response를 들고 내부에서 이동한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 브라우저는 forward가 일어난 걸 모른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저가 새 요청을 보내는 게 아니다&lt;/li&gt;
&lt;li&gt;서버 내부에서 &amp;ldquo;처리 담당자&amp;rdquo;만 바뀌는 것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음이 성립한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러가 request.setAttribute()로 담아둔 값이 JSP에서 그대로 보인다&lt;/li&gt;
&lt;li&gt;request scope가 유지된다&lt;/li&gt;
&lt;li&gt;주소창(URL)은 바뀌지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1774781479768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [forward 예시] Controller -&amp;gt; JSP로 &quot;같은 요청&quot;을 넘긴다
// - request.setAttribute로 모델을 담고
// - RequestDispatcher.forward로 JSP에게 요청 처리를 넘긴다
// ================================

@WebServlet(&quot;/user/profile&quot;)
public class ProfileController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 1) 컨트롤러가 데이터(모델)를 준비한다
        String userName = &quot;woojoo&quot;;
        int point = 1200;

        // 2) requestScope에 담는다 (forward로 넘어가면 JSP에서도 그대로 보인다)
        req.setAttribute(&quot;userName&quot;, userName);
        req.setAttribute(&quot;point&quot;, point);

        // 3) forward: 같은 request/response를 들고 JSP로 이동
        req.getRequestDispatcher(&quot;/WEB-INF/views/user/profile.jsp&quot;)
           .forward(req, resp);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774781526646&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- /WEB-INF/views/user/profile.jsp --&amp;gt;
&amp;lt;h1&amp;gt;Profile&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;name = ${userName}&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;point = ${point}&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 코드에서 userName, point를 request에 담았기 때문에&lt;br /&gt;JSP는 ${userName}, ${point}로 바로 출력할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward는 왜 MVC에서 기본이 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 &amp;ldquo;화면 렌더링&amp;rdquo;에 최적화된 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러가 하는 일은 단순해진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청 파라미터 처리/검증&lt;/li&gt;
&lt;li&gt;서비스/DAO 호출&lt;/li&gt;
&lt;li&gt;화면에 필요한 데이터 준비&lt;/li&gt;
&lt;li&gt;request에 담기(setAttribute)&lt;/li&gt;
&lt;li&gt;JSP로 forward&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JSP는 화면 출력만 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 역할 분리가 깔끔해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward vs include (거의 안 쓰는 이유까지)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RequestDispatcher에는 include()도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forward() : 지금까지 만든 응답을 무시하고 JSP 결과로 응답을 만든다&lt;/li&gt;
&lt;li&gt;include() : 지금까지 만든 응답 + JSP 결과를 합쳐서 응답한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 대부분 forward만 쓴다.&lt;br /&gt;include는 부분 화면 조합 같은 특수한 상황에서만 쓰는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;forward의 주의점: 응답을 이미 커밋하면 못 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward는 &amp;ldquo;JSP가 최종 응답을 만들게 넘기는 것&amp;rdquo;이다.&lt;br /&gt;그래서 컨트롤러가 response에 이미 많은 내용을 써버리면 문제가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 컨트롤러에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;response.getWriter()로 바디를 써버리거나&lt;/li&gt;
&lt;li&gt;이미 응답이 커밋된 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 forward가 실패할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC에서는 그래서 컨트롤러가 response 바디를 직접 쓰는 일을 최소화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RequestDispatcher는 요청을 다른 자원(JSP 등)에게 넘기는 도구이다&lt;/li&gt;
&lt;li&gt;forward는 서버 내부 이동이며 같은 request/response가 유지된다&lt;/li&gt;
&lt;li&gt;request scope가 유지되므로 setAttribute로 전달한 데이터가 JSP에서 그대로 보인다&lt;/li&gt;
&lt;li&gt;주소창 URL은 바뀌지 않는다&lt;/li&gt;
&lt;li&gt;MVC에서 화면 렌더링은 forward가 기본이다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Web/Web Basics</category>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/101</guid>
      <comments>https://woojoo-devlog.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 29 Mar 2026 19:53:52 +0900</pubDate>
    </item>
    <item>
      <title>[MCV와 웹 흐름 제어]  2. MVC는 무엇이고 왜 나누는가?</title>
      <link>https://woojoo-devlog.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 글에서 &amp;ldquo;JSP 단독 사용의 한계&amp;rdquo;를 봤다.&lt;br /&gt;핵심은 한 가지였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면(JSP) 안에 로직이 섞이기 시작하면 유지보수가 급격히 망가진다&lt;/li&gt;
&lt;li&gt;JSP 파일 URL이 노출되면 URL이 구현에 종속된다&lt;/li&gt;
&lt;li&gt;GET/POST 접근 제한 같은 흐름 제어가 구조적으로 어렵다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 역할을 분리해야 한다.&lt;br /&gt;이 역할 분리를 이름 붙인 게 MVC이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC는 &amp;ldquo;역할 분리&amp;rdquo; 구조이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC는 Model-View-Controller의 약자이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller: 요청을 받고 처리하고 흐름을 제어한다&lt;/li&gt;
&lt;li&gt;View: 화면을 출력한다&lt;/li&gt;
&lt;li&gt;Model: 화면에 필요한 데이터(재료)이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 &amp;ldquo;정답 구조&amp;rdquo;가 아니라 &amp;ldquo;의도가 역할 분리&amp;rdquo;라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller: 요청 처리 + 흐름 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 브라우저가 보낸 request를 받아서 다음을 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 기능을 수행할지 결정한다(라우팅)&lt;/li&gt;
&lt;li&gt;파라미터를 읽고 검증한다&lt;/li&gt;
&lt;li&gt;서비스/DAO 같은 로직을 호출한다&lt;/li&gt;
&lt;li&gt;결과 데이터를 준비한다&lt;/li&gt;
&lt;li&gt;어떤 View로 보낼지 결정한다(forward/redirect)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Controller는 &amp;ldquo;중앙 조정자&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 기반 MVC에서는 보통 Controller가 서블릿이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View: 화면 출력에만 집중&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 화면을 출력하는 역할이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML 구조를 만든다&lt;/li&gt;
&lt;li&gt;EL/JSTL로 값을 출력하고 반복/조건을 처리한다&lt;/li&gt;
&lt;li&gt;가능하면 로직은 넣지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 기반 MVC에서는 View가 보통 JSP이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 JSP는 &amp;ldquo;결과 화면&amp;rdquo;만 담당하고,&lt;br /&gt;실제 처리는 Controller가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Model: View에 필요한 데이터(재료)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model은 &amp;ldquo;DB 모델&amp;rdquo; 같은 좁은 의미로 오해하기 쉬운데,&lt;br /&gt;여기서 말하는 Model은 단순하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View가 화면을 그리기 위해 필요한 데이터 묶음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러가 만들어서 View에 전달하는 값들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 MVC에서는 보통 이 전달이 request.setAttribute()로 이루어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;req.setAttribute(&quot;todos&quot;, todoList);&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 JSP에서는 `${todos}`로 바로 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 request 영역이 Model 전달 통로 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC 흐름을 한 번에 보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 + JSP MVC의 전형적인 흐름은 이렇다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저가 Controller URL을 요청한다&lt;/li&gt;
&lt;li&gt;Controller(서블릿)가 파라미터를 읽고 로직을 실행한다&lt;/li&gt;
&lt;li&gt;Controller가 Model(데이터)을 준비한다&lt;/li&gt;
&lt;li&gt;Controller가 request에 Model을 담는다(setAttribute)&lt;/li&gt;
&lt;li&gt;Controller가 View(JSP)로 forward 한다&lt;/li&gt;
&lt;li&gt;View(JSP)가 Model을 EL/JSTL로 출력한다&lt;/li&gt;
&lt;li&gt;최종 HTML이 response로 브라우저에 전송된다&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1774774219665&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ================================
// [MVC 기본 예시] Controller(서블릿)가 Model(데이터)을 준비해서 View(JSP)로 넘긴다
// - Controller: 요청 처리 + 흐름 제어
// - Model: 화면에 필요한 데이터
// - View: 화면 출력(JSP)
// ================================
@WebServlet(&quot;/todo/list&quot;)
public class TodoListController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 1) Controller가 Model(데이터)을 준비한다
        List&amp;lt;Todo&amp;gt; todos = List.of(
                new Todo(1L, &quot;study servlet&quot;, false),
                new Todo(2L, &quot;write blog&quot;, true)
        );

        // 2) request에 담는다 (Model 전달)
        req.setAttribute(&quot;todos&quot;, todos);

        // 3) View로 forward 한다 (JSP가 화면을 만든다)
        req.getRequestDispatcher(&quot;/WEB-INF/views/todo/list.jsp&quot;)
           .forward(req, resp);
    }

    // 단순 예시용 DTO/VO 느낌의 클래스
    static class Todo {
        Long id;
        String title;
        boolean finished;

        Todo(Long id, String title, boolean finished) {
            this.id = id;
            this.title = title;
            this.finished = finished;
        }

        public String getTitle() { return title; }
        public boolean isFinished() { return finished; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVC로 나누면 뭐가 좋아지는가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 유지보수가 쉬워진다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 변경은 JSP에서 한다&lt;/li&gt;
&lt;li&gt;로직 변경은 Controller/Service에서 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로의 수정이 서로를 덜 깨뜨린다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) URL이 &amp;ldquo;파일 경로&amp;rdquo;에 종속되지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 JSP 파일이 아니라 Controller URL만 호출한다.&lt;br /&gt;JSP는 WEB-INF 아래로 숨겨서 직접 접근을 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 내부 구현(JSP 파일 구조)이 바뀌어도&lt;br /&gt;외부 URL은 유지할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 흐름 제어가 가능해진다&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 요청은 GET만 허용한다&lt;/li&gt;
&lt;li&gt;처리 후에는 redirect로 이동시킨다(PRG)&lt;/li&gt;
&lt;li&gt;권한 없으면 로그인으로 보낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 제어가 Controller에서 일관되게 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVC는 역할 분리를 위한 구조이다&lt;/li&gt;
&lt;li&gt;Controller는 요청 처리와 흐름 제어를 담당한다&lt;/li&gt;
&lt;li&gt;View는 화면 출력만 담당한다(JSP)&lt;/li&gt;
&lt;li&gt;Model은 View가 필요로 하는 데이터이다&lt;/li&gt;
&lt;li&gt;서블릿 MVC에서는 request.setAttribute가 Model 전달 통로가 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Controller가 View로 넘길 때 사용하는 핵심 도구인&lt;br /&gt;RequestDispatcher와 forward()를 코드로 정리한다.&lt;/p&gt;</description>
      <author>sqaxe1</author>
      <guid isPermaLink="true">https://woojoo-devlog.tistory.com/100</guid>
      <comments>https://woojoo-devlog.tistory.com/100#entry100comment</comments>
      <pubDate>Sun, 29 Mar 2026 17:51:37 +0900</pubDate>
    </item>
  </channel>
</rss>