<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>CODESYUN</title>
    <link>https://codesyun.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 18:04:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>s_ih_yun</managingEditor>
    <image>
      <title>CODESYUN</title>
      <url>https://tistory1.daumcdn.net/tistory/4216428/attach/b6e495261c094d70a89237810e499be7</url>
      <link>https://codesyun.tistory.com</link>
    </image>
    <item>
      <title>[Spring] Google Cloud Vision API로 이미지에서 텍스트 추출하는 기능 만들기</title>
      <link>https://codesyun.tistory.com/entry/Spring-Google-Cloud-Vision-API%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%B6%94%EC%B6%9C%ED%95%98%EB%8A%94-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</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;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxCrQ/dJMb9gjB8k8/D8Y9hxWl2F5Ox1pMfB4Ekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxCrQ/dJMb9gjB8k8/D8Y9hxWl2F5Ox1pMfB4Ekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxCrQ/dJMb9gjB8k8/D8Y9hxWl2F5Ox1pMfB4Ekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxCrQ%2FdJMb9gjB8k8%2FD8Y9hxWl2F5Ox1pMfB4Ekk%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;200&quot; height=&quot;136&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&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;&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. Vision API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.0. Google Cloud Vision API 란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google에서 제공하는 머신러닝 기반 이미지 분석 AI
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 인식, 객체 검출 등에서 매우 효과적으로 사용할 수 있는 API 제공&lt;/li&gt;
&lt;/ul&gt;
&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;a href=&quot;https://cloud.google.com/vision?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cloud.google.com/vision?hl=ko&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbWQg3/dJMb9Ogpebq/ozj1i8cZOsxbwQmpHekusK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbWQg3/dJMb9Ogpebq/ozj1i8cZOsxbwQmpHekusK/img.png&quot; data-alt=&quot;잘 하네!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbWQg3/dJMb9Ogpebq/ozj1i8cZOsxbwQmpHekusK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbWQg3%2FdJMb9Ogpebq%2Fozj1i8cZOsxbwQmpHekusK%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;727&quot; height=&quot;712&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘 하네!&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. Vision AI 환경 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chung-develop.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chung-develop.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760977688011&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Google Vision AI(이미지분석 API) 적용-이미지 내 텍스트 추출하기(OCR)-1편&quot; data-og-description=&quot;Google Vision AI(이미지분석) 적용하기-이미지 내 텍스트 추출하기(OCR)-1편 1. Google Vision AI API란? 구글에서 제공해주는 머신러닝 기반의 이미지 분석 API이다. 이 Vision API를 이용하면 이미지를 이용해 &quot; data-og-host=&quot;chung-develop.tistory.com&quot; data-og-source-url=&quot;https://chung-develop.tistory.com/47&quot; data-og-url=&quot;https://chung-develop.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UdbRa/hyZL0McIIm/Yz9iXXUIyvestsfI0Ma8Bk/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/Lgzic/hyZLecPbG4/Nfer9N8PKO60pNM2nNc29k/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/nJ89M/hyZMbNKPiK/6HweXyQjbzzGKiGCw6BHKk/img.png?width=1642&amp;amp;height=902&amp;amp;face=0_0_1642_902&quot;&gt;&lt;a href=&quot;https://chung-develop.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chung-develop.tistory.com/47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UdbRa/hyZL0McIIm/Yz9iXXUIyvestsfI0Ma8Bk/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/Lgzic/hyZLecPbG4/Nfer9N8PKO60pNM2nNc29k/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/nJ89M/hyZMbNKPiK/6HweXyQjbzzGKiGCw6BHKk/img.png?width=1642&amp;amp;height=902&amp;amp;face=0_0_1642_902');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google Vision AI(이미지분석 API) 적용-이미지 내 텍스트 추출하기(OCR)-1편&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google Vision AI(이미지분석) 적용하기-이미지 내 텍스트 추출하기(OCR)-1편 1. Google Vision AI API란? 구글에서 제공해주는 머신러닝 기반의 이미지 분석 API이다. 이 Vision API를 이용하면 이미지를 이용해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chung-develop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 글들을 참고하여 Vision API에서 다음 과정을 거쳤다!&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;Vision API 사용 설정&lt;/li&gt;
&lt;li&gt;서비스 계정 등록&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;JSON 파일 저장!&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. Vision API 키 환경 변수 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 발급받은 Vision API의 인증 키(JSON 파일)을 환경 변수로 등록&lt;/li&gt;
&lt;li&gt;환경 변수 &lt;b&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/b&gt;를 Google Cloud SDK가 자동으로 읽어 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.1. 개발 환경변수 등록 : IntelliJ&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Run/Debug Configurations &amp;gt; Environment variables
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Name : GOOGLE_APPLICATION_CREDENTIALS&lt;/li&gt;
&lt;li&gt;Value : 인증 키 JSON 파일 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNUxkC/dJMb9WZJs6q/ynfwLVGnncRz6U5jW7K7RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNUxkC/dJMb9WZJs6q/ynfwLVGnncRz6U5jW7K7RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNUxkC/dJMb9WZJs6q/ynfwLVGnncRz6U5jW7K7RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNUxkC%2FdJMb9WZJs6q%2FynfwLVGnncRz6U5jW7K7RK%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;471&quot; height=&quot;203&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zRjQW/dJMb86aee4T/s29VJxomWXi1FcFi0kGiNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zRjQW/dJMb86aee4T/s29VJxomWXi1FcFi0kGiNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zRjQW/dJMb86aee4T/s29VJxomWXi1FcFi0kGiNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzRjQW%2FdJMb86aee4T%2Fs29VJxomWXi1FcFi0kGiNk%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;1032&quot; height=&quot;668&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;668&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;h4 data-ke-size=&quot;size20&quot;&gt;1.2.2. 운영 환경변수 등록&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영 서버 /opt/posleep/keys/vision-sa.json 위치에 인증 키 JSON 파일을 올려두었다&lt;/li&gt;
&lt;li&gt;&lt;u&gt;app.env&lt;/u&gt;에 다음과 같이 인증 키 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;docker-compose.yml에 지정해둔 api의 env_file&lt;/li&gt;
&lt;li&gt;컨테이너 내부에서 읽기 가능!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760978323932&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS=/opt/posleep/keys/vision-sa.json&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;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;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;2. Spring에서 Vision API 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.0. 의존성 추가 및 서비스 호출&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 추가 (build.gradle)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760978672991&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	...
    implementation 'com.google.cloud:google-cloud-vision:3.35.0'
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 ~ 백엔드 API
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에서 이미지 업로드(&amp;lt;input type=&quot;file&quot; .../&amp;gt;) 후 백엔드 API 호출 (ingredients/ocr)&lt;/li&gt;
&lt;li&gt;Controller에서는 MultipartFile로 이미지 받아 검증 후, Service로 전달&lt;/li&gt;
&lt;li&gt;Service에서 Vision API 호출!&lt;/li&gt;
&lt;/ul&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;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;2.1. Vision API 사용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같은 메소드(callVision)을 작성하여 Vision API를 호출하여 이미지 &amp;rarr; 텍스트 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1.1. 메서드 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1760979111847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private String callVision(MultipartFile image) throws Exception {
    ByteString imgBytes = ByteString.copyFrom(image.getBytes());
    Image img = Image.newBuilder().setContent(imgBytes).build();

    Feature feat = Feature.newBuilder()
            .setType(Feature.Type.TEXT_DETECTION)
            .build();

    AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
            .addFeatures(feat)
            .setImage(img)
            .build();

    try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
        BatchAnnotateImagesResponse response = client.batchAnnotateImages(List.of(request));
        AnnotateImageResponse res = response.getResponses(0);

        if (res.hasError()) {
            throw new IllegalStateException(res.getError().getMessage());
        }

        return res.getFullTextAnnotation().getText();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1.2. 코드 라인별 설명&lt;/h4&gt;
&lt;pre id=&quot;code_1760979350070&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ByteString imgBytes = ByteString.copyFrom(image.getBytes());&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vision API는 내부적으로 gRPC를 이용하기 때문에 ByteString 형태로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979411932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Image img = Image.newBuilder().setContent(imgBytes).build();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vision API에서 사용하는 Image protobuf 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979450339&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Feature feat = Feature.newBuilder()
        .setType(Feature.Type.TEXT_DETECTION)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setType() : OCR 기능 지정&lt;/li&gt;
&lt;li&gt;Feature.Type.TEXT_DETECTION : 문자 인식 모드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979550410&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
        .addFeatures(feat)
        .setImage(img)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vision API에 보낼 실제 요청 객체 : 분석 기능, 인식할 이미지 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979609216&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vision API에 연결할 gRPC 클라이언트 생성&lt;/li&gt;
&lt;li&gt;내부적으로 인증(환경변수 GOOGLE_APPLICATION_CREDENTIALS)과 통신 자동 처리&lt;/li&gt;
&lt;li&gt;try (...) 문으로 사용 끝나면 자동으로 세션 종료 및 리소스 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979684456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BatchAnnotateImagesResponse response = client.batchAnnotateImages(List.of(request));&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 API 호출 부분 (한 번에 여러 이미지 요청 가능)&lt;/li&gt;
&lt;li&gt;서버로 gRPC 호출 &amp;rarr; Vision API가 이미지 처리 후 응답 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760979773412&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AnnotateImageResponse res = response.getResponses(0);

if (res.hasError()) {
    throw new IllegalStateException(res.getError().getMessage());
}

return res.getFullTextAnnotation().getText();&lt;/code&gt;&lt;/pre&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 파싱 및 DB 업로드를 포함한 코드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;callVision(image) : Vision API 호출하여 이미지 &amp;rarr; 텍스트 변환&lt;/li&gt;
&lt;li&gt;parseNameCounts(fullText) : 추출된 텍스트 파싱 및 정제&lt;/li&gt;
&lt;li&gt;@Transactional + setQuantity() : 엔티티가 영속 상태가 되어 메서드 정상 종료시 Commit (Update)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지에 있는 식재료 &amp;rarr; 이미지 인식된 수량 등록, 이미지에 없는 식재료 &amp;rarr; 수량 0으로 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760979907999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void processAndApply(MultipartFile image) throws Exception {
    String fullText = callVision(image);

    Map&amp;lt;String,Integer&amp;gt; ocrResults = parseNameCounts(fullText);
    List&amp;lt;Ingredient&amp;gt; ingredients = repo.findAll();

    for (Ingredient ing : ingredients) {
        String name = ing.getName();
        Integer parsedCount = ocrResults.get(name);
        if (parsedCount != null) {
            ing.setQuantity(parsedCount);
            ing.setIsRegistered(true);
        } else {
            ing.setQuantity(0);
        }
    }
}

private Map&amp;lt;String, Integer&amp;gt; parseNameCounts(String fullText) {
    Deque&amp;lt;Integer&amp;gt; counts = new ArrayDeque&amp;lt;&amp;gt;();
    Deque&amp;lt;String&amp;gt; names = new ArrayDeque&amp;lt;&amp;gt;();
    Map&amp;lt;String, Integer&amp;gt; result = new LinkedHashMap&amp;lt;&amp;gt;();

    List&amp;lt;String&amp;gt; rawLines = Arrays.stream(fullText.split(&quot;\\R&quot;))
            .map(String::trim)
            .toList();
    int startIdx = findStartIndex(rawLines, START_ANCHOR);
    int endIdx = findEndIndex(rawLines, startIdx, END_ANCHORS);

    List&amp;lt;String&amp;gt; lines = rawLines.subList(startIdx, endIdx);
    for (String raw : lines) {
        String line = normalizeLine(raw);
        if (line.isEmpty()) continue;

        Matcher m = COUNT_LINE.matcher(line);
        if (m.matches()) {
            int c = Integer.parseInt(m.group(1));
            counts.addLast(c);
        }
        if (isName(line)) {
            names.addLast(line);
        }

        while (!counts.isEmpty() &amp;amp;&amp;amp; !names.isEmpty()) {
            result.put(names.removeFirst(), counts.removeFirst());
        }
    }
    return result;
}&lt;/code&gt;&lt;/pre&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;Vision API 사용 코드 : /backend/src/main/java/com/syun/posleep/service/&lt;b&gt;IngredientOcrService.java&lt;/b&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;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;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;3. ⭐️ 성공 ⭐️&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;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_5975.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KjqcQ/dJMb9W6vhcD/JCWKiyqseC9OHuGqqJItjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KjqcQ/dJMb9W6vhcD/JCWKiyqseC9OHuGqqJItjk/img.png&quot; data-alt=&quot;포켓몬슬립 가방 스크린샷&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KjqcQ/dJMb9W6vhcD/JCWKiyqseC9OHuGqqJItjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKjqcQ%2FdJMb9W6vhcD%2FJCWKiyqseC9OHuGqqJItjk%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;400&quot; height=&quot;867&quot; data-filename=&quot;IMG_5975.png&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;포켓몬슬립 가방 스크린샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chJYFT/dJMb9YwtWxf/JLuyttNGBFRsOrKQeOtwJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chJYFT/dJMb9YwtWxf/JLuyttNGBFRsOrKQeOtwJK/img.png&quot; data-alt=&quot;업로드 &amp;amp;gt; 파일 선택 &amp;amp;gt; 업로드/분석 기능 실행 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chJYFT/dJMb9YwtWxf/JLuyttNGBFRsOrKQeOtwJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchJYFT%2FdJMb9YwtWxf%2FJLuyttNGBFRsOrKQeOtwJK%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;500&quot; height=&quot;705&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;업로드 &amp;gt; 파일 선택 &amp;gt; 업로드/분석 기능 실행 후&lt;/figcaption&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;&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;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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/syun32/2025-posleep&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760980963526&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - syun32/2025-posleep&quot; data-og-description=&quot;Contribute to syun32/2025-posleep development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GypIX/hyZL3oA7wJ/aKPx46fkpQNNr9l31XDV50/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/chdo8o/hyZLnnfXss/QOvI4VDTHkLiu19ClgwTJ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/syun32/2025-posleep&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GypIX/hyZL3oA7wJ/aKPx46fkpQNNr9l31XDV50/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/chdo8o/hyZLnnfXss/QOvI4VDTHkLiu19ClgwTJ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - syun32/2025-posleep&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to syun32/2025-posleep development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/380</guid>
      <comments>https://codesyun.tistory.com/entry/Spring-Google-Cloud-Vision-API%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%97%90%EC%84%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%B6%94%EC%B6%9C%ED%95%98%EB%8A%94-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry380comment</comments>
      <pubDate>Tue, 21 Oct 2025 02:26:32 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] EC2 인스턴스 SSH Connection timed out : 탄력적 IP 연결 및 요금 주의</title>
      <link>https://codesyun.tistory.com/entry/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-SSH-Connection-timed-out-%ED%83%84%EB%A0%A5%EC%A0%81-IP-%EC%97%B0%EA%B2%B0-%EB%B0%8F-%EC%9A%94%EA%B8%88-%EC%A3%BC%EC%9D%98</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;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;aws_logo_smile.png&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B2lAs/dJMb9bWUxse/ItjOLvab1sR4ufonmKHIfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B2lAs/dJMb9bWUxse/ItjOLvab1sR4ufonmKHIfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B2lAs/dJMb9bWUxse/ItjOLvab1sR4ufonmKHIfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB2lAs%2FdJMb9bWUxse%2FItjOLvab1sR4ufonmKHIfK%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;200&quot; height=&quot;143&quot; data-filename=&quot;aws_logo_smile.png&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;803&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;&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;0. 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에 EC2에 SSH(port 22) 접속에는 문제가 없었음&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;/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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퍼블릭 IP 변경에 따른 DNS/접속 지연 문제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스를 중지/시작하거나 리부트 하는 경우 IP가 변경될 수 있음&lt;/li&gt;
&lt;li&gt;VPC 설정이나 퍼블릭 IP 갱신 등 영향으로 IP 변경 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;신규 인바운드 포트가 열리면서 보안그룹, 네트워크 ACL 설정이 추가됐을 가능성
&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;&lt;b&gt;탄력적 IP 적용&lt;/b&gt;으로 고정된 주소를 사용하여 IP 변경 리스크를 제거하고 SSH 접속 안정성을 확보하자!&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;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;2. 탄력적 IP 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 탄력적 IP 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Baqo0/dJMb9i2NTj4/64gsbXEkOhyRfU4LGfXqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Baqo0/dJMb9i2NTj4/64gsbXEkOhyRfU4LGfXqk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Baqo0/dJMb9i2NTj4/64gsbXEkOhyRfU4LGfXqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBaqo0%2FdJMb9i2NTj4%2F64gsbXEkOhyRfU4LGfXqk0%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;514&quot; height=&quot;438&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;438&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;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 탄력적 IP 주소 연결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탄력적 IP 주소 선택 &amp;rarr; 탄력적 IP 주소 연결 &amp;rarr; 인스턴스 선택 &amp;rarr; 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wqz8H/dJMb9NPkPZo/BMzZ13UNAKalbTh9r5iTV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wqz8H/dJMb9NPkPZo/BMzZ13UNAKalbTh9r5iTV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wqz8H/dJMb9NPkPZo/BMzZ13UNAKalbTh9r5iTV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwqz8H%2FdJMb9NPkPZo%2FBMzZ13UNAKalbTh9r5iTV0%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;612&quot; height=&quot;314&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;314&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;h3 data-ke-size=&quot;size23&quot;&gt;2.3. ⭐️ 성공 ⭐️&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탄력적 IP는 고정된 퍼블릭 IPv4 주소
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서도 항상 같은 IP로 접속하게 되므로 보안그룹, DNS, 캐시 등 변동 요인 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;퍼블릭 IP가 변경되거나 불안정한 문제 제거되어 SSH 접속 안정성 확보!&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;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;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;3. ⚠️ 과금 주의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. AWS Public IPv4 요금 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760974980512&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;공지 &amp;ndash; AWS Public IPv4 주소 요금 변경 및 Public IP Insights 기능 출시 | Amazon Web Services&quot; data-og-description=&quot;AWS에서 퍼블릭(Public) IPv4 주소에 대한 새로운 요금이 도입됩니다. 2024년 2월 1일부터 서비스 연결 여부에 관계없이 모든 퍼블릭 IPv4 주소에 대해 시간당 IP당 0.005 USD의 요금이 부과됩니다. 계정에 &quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot; data-og-url=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bToSw3/hyZL06uJT9/5KuuZKfi7gvdud3kSC0BG0/img.jpg?width=1260&amp;amp;height=601&amp;amp;face=0_0_1260_601,https://scrap.kakaocdn.net/dn/bwfTrh/hyZL9h37ar/Bs2g9XnvTnURULK6YskPkK/img.jpg?width=1260&amp;amp;height=601&amp;amp;face=0_0_1260_601,https://scrap.kakaocdn.net/dn/b27dNj/hyZL7q1raB/jHqoP7sIm4alGiKzaFZmV1/img.png?width=900&amp;amp;height=767&amp;amp;face=0_0_900_767&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bToSw3/hyZL06uJT9/5KuuZKfi7gvdud3kSC0BG0/img.jpg?width=1260&amp;amp;height=601&amp;amp;face=0_0_1260_601,https://scrap.kakaocdn.net/dn/bwfTrh/hyZL9h37ar/Bs2g9XnvTnURULK6YskPkK/img.jpg?width=1260&amp;amp;height=601&amp;amp;face=0_0_1260_601,https://scrap.kakaocdn.net/dn/b27dNj/hyZL7q1raB/jHqoP7sIm4alGiKzaFZmV1/img.png?width=900&amp;amp;height=767&amp;amp;face=0_0_900_767');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;공지 &amp;ndash; AWS Public IPv4 주소 요금 변경 및 Public IP Insights 기능 출시 | Amazon Web Services&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AWS에서 퍼블릭(Public) IPv4 주소에 대한 새로운 요금이 도입됩니다. 2024년 2월 1일부터 서비스 연결 여부에 관계없이 모든 퍼블릭 IPv4 주소에 대해 시간당 IP당 0.005 USD의 요금이 부과됩니다. 계정에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프리티어 유저로서 과금의 위험을 피하기 위해 주의하던 탄력적 IP 요금 정책이 모르던 새 바뀌었다!&lt;/li&gt;
&lt;li&gt;(기존) 탄력적 IP에 연결된 인스턴스가 없을 때 과금
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공용 IP 자원을 소모하므로 미사용 상태의 탄력적 IP 방지를 위한 정책&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(2024년 2월 1일 이후) 서비스 연결 여부에 관계없이 시간당 IP당 0.005달러 부과...!
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프리티어는 1년 동안 매월 750시간의 퍼블릭 IPv4 주소 사용이 포함된다&lt;/li&gt;
&lt;li&gt;그래도 1개의 탄력적 IP 주소는 무료로 사용할 수 있다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  인스턴스 제거 시에 &lt;span style=&quot;color: #ee2323;&quot;&gt;탄력적 IP도 꼭 릴리즈&lt;/span&gt;하도록 주의하자&amp;nbsp;&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;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;3.2. 퍼블릭 IP 인사이트 : 사용 중인 퍼블릭 IP 점검&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon VPC IP Address Manager &amp;gt; 퍼블릭 IP 인사이트&lt;/li&gt;
&lt;li&gt;RDS가 퍼블릭 접근 허용되어 있는지, 연결되지 않은 탄력적 IP는 없는지 점검해보자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGziIE/dJMb84Dt3bH/T7EKLj27Pl61B6qpE2R96K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGziIE/dJMb84Dt3bH/T7EKLj27Pl61B6qpE2R96K/img.png&quot; data-alt=&quot;I'm Ok  &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGziIE/dJMb84Dt3bH/T7EKLj27Pl61B6qpE2R96K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGziIE%2FdJMb84Dt3bH%2FT7EKLj27Pl61B6qpE2R96K%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;1039&quot; height=&quot;818&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;I'm Ok  &lt;/figcaption&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;&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;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;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;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;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;</description>
      <category>Cloud</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/379</guid>
      <comments>https://codesyun.tistory.com/entry/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-SSH-Connection-timed-out-%ED%83%84%EB%A0%A5%EC%A0%81-IP-%EC%97%B0%EA%B2%B0-%EB%B0%8F-%EC%9A%94%EA%B8%88-%EC%A3%BC%EC%9D%98#entry379comment</comments>
      <pubDate>Tue, 21 Oct 2025 01:06:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Controller와 @RestController, 공통 응답 포맷</title>
      <link>https://codesyun.tistory.com/entry/Spring-Controller%EC%99%80-RestController-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5-%ED%8F%AC%EB%A7%B7</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;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z25Fd/dJMb9P0GLK1/cBDyPFj9N2M5akQRZkk3i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z25Fd/dJMb9P0GLK1/cBDyPFj9N2M5akQRZkk3i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z25Fd/dJMb9P0GLK1/cBDyPFj9N2M5akQRZkk3i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz25Fd%2FdJMb9P0GLK1%2FcBDyPFj9N2M5akQRZkk3i0%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;200&quot; height=&quot;136&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;a href=&quot;https://codesyun.tistory.com/entry/Nextjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Next.js]&amp;nbsp;Thymeleaf&amp;nbsp;기반&amp;nbsp;구조&amp;nbsp;Next.js로&amp;nbsp;리팩토링&amp;nbsp;:&amp;nbsp;Next.js&amp;nbsp;준비&amp;nbsp;및&amp;nbsp;설정&amp;nbsp;/&amp;nbsp;Docker&amp;nbsp;컨테이너화&amp;nbsp;/&amp;nbsp;Nginx&amp;nbsp;프록시&amp;nbsp;설정&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;이전 Thymeleaf로 간단한 페이지를 제공하던 프로젝트에서 Next.js 프론트를 추가하면서&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;백엔드 컨트롤러를 @Controller에서 @RestController로 변경했습니당&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;그러면서 JSON 응답의 일관성을 위해 공통 응답 포맷 적용ㄱ&lt;/span&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;&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;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. @Controller&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전통적은 Spring MVC의 컨트롤러 어노테이션&lt;/li&gt;
&lt;li&gt;주로 View(화면)을 반환하기 위해 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@ResponseBody 어노테이션을 활용하여 JSON 형태 데이터 반환 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760969406212&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/ingredients&quot;)
public class IngredientController {
    ...
    @PostMapping(&quot;/update&quot;)
    public String update(@Valid @ModelAttribute(&quot;form&quot;) IngredientForm form,
                         BindingResult br,
                         RedirectAttributes ra,
                         Model model) {

        if (br.hasErrors()) {
            model.addAttribute(&quot;ingredients&quot;, svc.listAllOrdered());
            return &quot;ingredientList&quot;;
        }
        svc.update(form);
        ra.addFlashAttribute(&quot;msg&quot;, &quot;저장되었습니다.&quot;);

        return &quot;redirect:/ingredients&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;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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. @RestController&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Restful Web Service에서 사용되는 컨트롤러 어노테이션&lt;/li&gt;
&lt;li&gt;@Controller + @ResponseBody가 합쳐진 형태로, JSON 형태의 객체 데이터 반환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 ResponseEntity로 감싸서 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760969638344&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/ingredients&quot;)
public class IngredientController {
    ...
    @PostMapping(
            path = &quot;/update&quot;,
            consumes = MediaType.APPLICATION_JSON_VALUE
    )
    public ResponseEntity&amp;lt;?&amp;gt; update(@Valid @RequestBody IngredientForm form,
                                 BindingResult br) {

        if (br.hasErrors()) {
            return ResponseEntity.badRequest().body(br.getAllErrors());
        }

        svc.update(form);
        return ResponseEntity.ok().build();
    }
    ...&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;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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 공통 응답 포맷 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.0. 사용 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음과 같은 일관된 백엔드 응답을 제공하고자 할 때, Controller의 리턴 타입을 반복적으로 작성해야 한다..!
&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;/ul&gt;
&lt;pre id=&quot;code_1760970384813&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;status&quot;: &quot;success&quot;,
    &quot;data&quot;: {
    }
    &quot;meta&quot;: null
}&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. [BE] API Response DTO&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음과 같이 공통 Response Class 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 ExceptionHandler도 작성하여 Exception 발생 시에도 유형에 따라 처리하자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760970891414&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.syun.posleep.dto.response;

import java.util.Map;

public record ApiResponse&amp;lt;T&amp;gt;(
        String status,
        T data,
        Map&amp;lt;String, Object&amp;gt; meta
) {
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; success(T data) {
        return new ApiResponse&amp;lt;&amp;gt;(&quot;success&quot;, data, null);
    }
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; success(T data, Map&amp;lt;String,Object&amp;gt; meta) {
        return new ApiResponse&amp;lt;&amp;gt;(&quot;success&quot;, data, meta);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/ul&gt;
&lt;pre id=&quot;code_1760971217151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/ingredients&quot;)
public class IngredientController {
    ...
    @GetMapping
    public ApiResponse&amp;lt;List&amp;lt;IngredientSheetRow&amp;gt;&amp;gt; getPage() {
        List&amp;lt;IngredientSheetRow&amp;gt; list = svc.listAllOrdered();
        return ApiResponse.success(list);
    }
    ...&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. [FE] 공통 API 응답 타입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 API 응답 타입 선언&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760971326730&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 공통 API 응답 타입 (frontend/src/types/api.ts)
export type ApiResponse&amp;lt;T&amp;gt; = {
  status: string;            // &quot;success&quot; | &quot;error&quot; 등
  data: T;                   // 실제 데이터
  meta?: Record&amp;lt;string, unknown&amp;gt;; // 페이징, 총 개수 등 부가 정보
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/ul&gt;
&lt;pre id=&quot;code_1760971425430&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 목록 조회 (frontend/src/app/ingredients/page.tsx)
useEffect(() =&amp;gt; {
    (async () =&amp;gt; {
        try {
            setLoading(true);
            const res = await fetch(`${BASE}/ingredients`, { cache: 'no-store' });
            if (!res.ok) throw new Error();
            const json: ApiResponse&amp;lt;Ingredient[]&amp;gt; = await res.json();
...&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;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;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;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;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;p data-ke-size=&quot;size16&quot;&gt;  References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://backendcode.tistory.com/213#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://backendcode.tistory.com/213#google_vignette&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@tkdwns414/Spring-Boot-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5%EC%9A%A9-ApiResponse-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@tkdwns414/Spring-Boot-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5%EC%9A%A9-ApiResponse-%EB%A7%8C%EB%93%A4%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://deveun.tistory.com/entry/SpringBoot-%EA%B3%B5%ED%86%B5-Response-%ED%8F%AC%EB%A7%B7-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://deveun.tistory.com/entry/SpringBoot-%EA%B3%B5%ED%86%B5-Response-%ED%8F%AC%EB%A7%B7-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&lt;/a&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;&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;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;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;</description>
      <category>Spring</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/378</guid>
      <comments>https://codesyun.tistory.com/entry/Spring-Controller%EC%99%80-RestController-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5-%ED%8F%AC%EB%A7%B7#entry378comment</comments>
      <pubDate>Mon, 20 Oct 2025 23:50:04 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Thymeleaf 기반 구조 Next.js로 리팩토링 : Next.js 준비 및 설정 / Docker 컨테이너화 / Nginx 프록시 설정</title>
      <link>https://codesyun.tistory.com/entry/Nextjs</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;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;nextjs.png&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCsLtf/dJMb9bo4Cpb/JaLpwaErIP9vxMkn6Hw9WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCsLtf/dJMb9bo4Cpb/JaLpwaErIP9vxMkn6Hw9WK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCsLtf/dJMb9bo4Cpb/JaLpwaErIP9vxMkn6Hw9WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCsLtf%2FdJMb9bo4Cpb%2FJaLpwaErIP9vxMkn6Hw9WK%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;200&quot; height=&quot;150&quot; data-filename=&quot;nextjs.png&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;기존 프로젝트는 Thymeleaf 기반의 간단한 페이지로 구성되어 있었는데&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;프론트 협업을 대비해서 연습해보고 싶어서 Next.js 기반으로 리팩토링‼&lt;/span&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;&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. Next.js 란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React를 위해 만든 오픈 소스 JavaScript 웹 프레임워크&lt;/li&gt;
&lt;li&gt;React에는 없는 기능 제공
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 사이드 렌더링(SSR; Server-Side Rendering)&lt;/li&gt;
&lt;li&gt;정적 사이트 생성(SSG; Static Site Generation)&lt;/li&gt;
&lt;li&gt;증분 정적 재생성(ISR; Incremental Static Regeneration)&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. SSR vs CSR&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 88px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.6589%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.8682%; height: 17px;&quot;&gt;&lt;b&gt;SSR (Server-Side Rendering)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.4728%; height: 17px;&quot;&gt;&lt;b&gt;CSR (Client-Server Rendering)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25.6589%; height: 18px;&quot;&gt;&lt;b&gt;JavaScript 로딩&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.8682%; height: 18px;&quot;&gt;서버에서 JS 로딩&lt;br /&gt;&amp;rarr; 완성된 HTML 클라이언트로 전달&lt;br /&gt;(초기 로딩 속도 빠름)&lt;/td&gt;
&lt;td style=&quot;width: 36.4728%; height: 18px;&quot;&gt;브라우저에 JS 정보 가진 빈 HTML 전달&lt;br /&gt;&amp;rarr; 브라우저가 JS 로딩하여 UI 빌드&lt;br /&gt;(초기 로드 완료되면 렌더링 빠름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 53px;&quot;&gt;
&lt;td style=&quot;width: 25.6589%; height: 53px;&quot;&gt;&lt;b&gt;SEO&lt;/b&gt;&lt;br /&gt;&lt;b&gt;(Search Engine Optimization)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.8682%; height: 53px;&quot;&gt;HTML 파일에 모든 정보가 포함되어 있어&lt;br /&gt;봇이 데이터 수집 가능&lt;/td&gt;
&lt;td style=&quot;width: 36.4728%; height: 53px;&quot;&gt;초기 HTML 파일이 비어있어&lt;br /&gt;봇이 데이터 수집하기 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;h3 data-ke-size=&quot;size23&quot;&gt;1.2. React vs Vue vs Next.js&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 143px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 17px;&quot;&gt;&lt;b&gt;React&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 17px;&quot;&gt;&lt;b&gt;Vue&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 17px;&quot;&gt;&lt;b&gt;Next.js&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;개발 유형&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;라이브러리&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;프레임워크&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;React 기반 프레임워크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;학습 곡선&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;중간&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;렌더링 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;CSR&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;CSR&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;SSR, SSG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;라우팅&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;별도 라이브러리 필요&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;Vue Router 필요&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;파일 기반 라우팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;상태 관리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;Redux, Context API&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;Vuex&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;Redux, Context API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;사용 사례&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;대규모 SPA&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;소규모~중규모 프로젝트&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;SEO가 필요한 프로젝트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 17.3256%; height: 18px;&quot;&gt;&lt;b&gt;커뮤니티 및 생태계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.7442%; height: 18px;&quot;&gt;매우 활발&lt;/td&gt;
&lt;td style=&quot;width: 28.1395%; height: 18px;&quot;&gt;활발&lt;/td&gt;
&lt;td style=&quot;width: 27.7907%; height: 18px;&quot;&gt;활발 (React 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Next.js 프로젝트 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.0. 최종 목표 디렉토리&lt;/h3&gt;
&lt;pre id=&quot;code_1760954220615&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-posleep/
├─ backend/
├─ frontend/     # &amp;larr; Next.js 프로젝트 추가
├─ infra/
│  ├─ docker-compose.yml
│  └─ nginx/
│     └─ nginx.conf
└─ ...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBoot + Thymeleaf 구조&amp;nbsp; &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;rarr;&amp;rarr;&amp;rarr;&amp;nbsp;&lt;/span&gt; Next.js + 백엔드 API (SpringBoot)&lt;/li&gt;
&lt;li&gt;모노레포 (Monorepo)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI/CD 통합을 위한 구성&lt;/li&gt;
&lt;li&gt;frontend, backend API 프로젝트를 하나의 레포지토리에서 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spring root를 2025-posleep &amp;rarr; 2025-posleep/backend로 변경&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;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 설치 및 프로젝트 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nvm + Node 20 LTS 설치&lt;/li&gt;
&lt;li&gt;프로젝트 생성 : npm create-next-app
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 이름 : frontend&lt;/li&gt;
&lt;li&gt;Turbopack : Next.js 팀이 만든 Rust 기반 초고속 번들러 &amp;rarr; 협업 대비 연습이기 때문에 안정성 중요하여 설치 ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760963648915&quot; class=&quot;dsconfig&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;syun@SyunMac 2025-posleep % npx create-next-app@latest frontend \
  --ts --eslint --tailwind --app --src-dir --import-alias &quot;@/*&quot;
Need to install the following packages:
create-next-app@15.5.3
Ok to proceed? (y) y

✔ Would you like to use Turbopack? (recommended) &amp;hellip; No / Yes&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 로컬 백엔드 API 연동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 중에는 백엔드(로컬 8080)로 붙도록 환경 변수 사용 (.env.local)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경변수를 process.env로 자동으로 로드 (ex. process.env.NEXT_PUBLIC_BACKEND_URL)&lt;/li&gt;
&lt;li&gt;운영 환경 백엔드 URL은 docker-compose.yml 에 설정하여 주입 (3.3 참고)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760963925368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# frontend/.env.local
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 중 CORS 없이 호출하도록 next.config.ts에 rewrites 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760966343864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import type { NextConfig } from &quot;next&quot;;
import { PHASE_DEVELOPMENT_SERVER } from &quot;next/constants&quot;;

const base: NextConfig = {
  reactStrictMode: true,
};

// 개발 모드에서만 /api &amp;rarr; 8080 프록시
const config = (phase: string): NextConfig =&amp;gt; {
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    return {
      ...base,
      async rewrites() {
        return [
          { source: &quot;/api/:path*&quot;, destination: &quot;http://localhost:8080/:path*&quot; },
        ];
      },
    };
  }
  return base;
};

export default config;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. 개발 서버 실행&lt;/h3&gt;
&lt;pre id=&quot;code_1760963695868&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd frontend
npm run dev&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프론트엔드 Docker 컨테이너화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. standalone 빌드 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;next.config.ts 파일에서 standalone 빌드 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 시 .next/standalone 디렉토리 생성 : 서버 실행에 필요한 최소한의 파일만 분리&lt;/li&gt;
&lt;li&gt;EC2나 Docker 같은 서버 환경에서 가볍고 빠르게 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760964139026&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import type { NextConfig } from &quot;next&quot;;
import { PHASE_DEVELOPMENT_SERVER } from &quot;next/constants&quot;;

const base: NextConfig = {
  reactStrictMode: true,
  output: &quot;standalone&quot;,
};

const config = (phase: string): NextConfig =&amp;gt; {
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    return {
      ...base,
      async rewrites() {
        return [
          { source: &quot;/api/:path*&quot;, destination: &quot;http://localhost:8080/:path*&quot; },
        ];
      },
    };
  }
  return base;
};

export default config;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. Dockerfile 작성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;frontend/Dockerfile&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760964190134&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ========= 1) Build stage =========
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# 프로덕션 빌드 (standalone 산출물 생성)
RUN npm run build

# ========= 2) Runtime stage =========
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000

# 보안상 node 유저 사용
RUN addgroup -S nodejs &amp;amp;&amp;amp; adduser -S nextjs -G nodejs

# standalone 서버 코드 &amp;amp; 정적 리소스 복사
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

USER nextjs
CMD [&quot;node&quot;, &quot;server.js&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. docker-compose에 프론트 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;infra/docker-compose.yml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760964815418&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.9&quot;
services:
  api:
    image: syun32/posleep-api:prod
    container_name: posleep-api
    env_file:
      - /opt/posleep/app.env
    volumes:
      - /opt/posleep/keys:/opt/posleep/keys:ro
    expose:
      - &quot;8080&quot;
    restart: unless-stopped
    environment:
      - JAVA_OPTS=-XX:MaxRAMPercentage=75 -XX:+UseG1GC
      - TZ=Asia/Seoul
    networks:
      - posleep-net

  frontend:
    build:
      context: ../frontend
      dockerfile: Dockerfile
    image: syun32/posleep-frontend:prod
    container_name: posleep-frontend
    expose:
      - &quot;3000&quot;
    restart: unless-stopped
    environment:
      - NEXT_PUBLIC_API_BASE_URL=http://api:8080
      - TZ=Asia/Seoul
    networks:
      - posleep-net
    depends_on:
      - api

  nginx:
    image: nginx:1.27-alpine
    container_name: posleep-nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - &quot;80:80&quot;
    restart: unless-stopped
    depends_on:
      - frontend
      - api
    networks:
      - posleep-net

networks:
  posleep-net:
    driver: bridge&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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Nginx 프록시 설정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;infra/nginx/nginx.conf&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;기본 트래픽 &amp;rarr; 프론트&amp;nbsp; (외부에는 80만 오픈, 3000은 컨테이너 내부 네트워크 통신)&lt;/li&gt;
&lt;li&gt;/api &amp;rarr; 백엔드&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(외부에 8080 오픈 유지: 테스트용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760966627045&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;worker_processes auto;

events { worker_connections 1024; }

http {
  upstream frontend {
    server posleep-frontend:3000;
  }

  upstream api {
    server posleep-api:8080;
  }

  server {
    listen 80;
    server_name _;

    # Next.js 프론트
    location / {
      proxy_pass http://frontend;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Spring Boot API
    location /api/ {
      proxy_pass http://api/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }
  }
}&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. GitHub Actions Workflow 변경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.github/workflows/cicd.yml 변경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 env에 프론트 이미지 이름/태그 추가&lt;/li&gt;
&lt;li&gt;프론트 빌드/푸시 Job 추가&lt;/li&gt;
&lt;li&gt;deploy Job은 두 이미지(BE / FE) 모두 푸시된 후에 실행&lt;/li&gt;
&lt;/ul&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;99. 접속 테스트 ⭐️ 성공 ⭐️&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2-IP/&amp;nbsp; &amp;rarr;&amp;nbsp; 프론트엔드(Next.js) 접속 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doZOAN/dJMb89SkoDv/2KF07kUK16C0zMsXuv5bMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doZOAN/dJMb89SkoDv/2KF07kUK16C0zMsXuv5bMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doZOAN/dJMb89SkoDv/2KF07kUK16C0zMsXuv5bMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoZOAN%2FdJMb89SkoDv%2F2KF07kUK16C0zMsXuv5bMk%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;998&quot; height=&quot;720&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;720&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2-IP/api &amp;rarr; 백엔드(Spring) 접속 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GDGcE/dJMb9XRSlHj/jFyMQQWqHedHBcCy8fyUkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GDGcE/dJMb9XRSlHj/jFyMQQWqHedHBcCy8fyUkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GDGcE/dJMb9XRSlHj/jFyMQQWqHedHBcCy8fyUkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGDGcE%2FdJMb9XRSlHj%2FjFyMQQWqHedHBcCy8fyUkk%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;998&quot; height=&quot;720&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;720&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;&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/syun32/2025-posleep&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760967248729&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - syun32/2025-posleep&quot; data-og-description=&quot;Contribute to syun32/2025-posleep development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GypIX/hyZL3oA7wJ/aKPx46fkpQNNr9l31XDV50/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/chdo8o/hyZLnnfXss/QOvI4VDTHkLiu19ClgwTJ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/syun32/2025-posleep&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GypIX/hyZL3oA7wJ/aKPx46fkpQNNr9l31XDV50/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/chdo8o/hyZLnnfXss/QOvI4VDTHkLiu19ClgwTJ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - syun32/2025-posleep&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to syun32/2025-posleep development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;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;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;  References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://subtlething.tistory.com/115#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://subtlething.tistory.com/115#google_vignette&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@attosisss_/CSR-%EA%B3%BC-SSR%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SEO&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@attosisss_/CSR-%EA%B3%BC-SSR%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SEO&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@hyunnzl/React-Vue-Next.js-%EC%84%A4%EB%AA%85%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@hyunnzl/React-Vue-Next.js-%EC%84%A4%EB%AA%85%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@milkboy2564/Next.js-env%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%A0%95%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@milkboy2564/Next.js-env%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%A0%95%EB%A6%AC&lt;/a&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;&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;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;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;</description>
      <category>FrontEnd</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/377</guid>
      <comments>https://codesyun.tistory.com/entry/Nextjs#entry377comment</comments>
      <pubDate>Mon, 20 Oct 2025 18:39:19 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트의 프로퍼티(props) / useState / 이벤트 핸들러(onChange, onClick)</title>
      <link>https://codesyun.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0props-useState-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%AConChange-onClick</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;nbsp;&lt;/p&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-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;157&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t2mAq/btsQEoILnEe/hF8OLdESPqQeBsQzRKOp7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t2mAq/btsQEoILnEe/hF8OLdESPqQeBsQzRKOp7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t2mAq/btsQEoILnEe/hF8OLdESPqQeBsQzRKOp7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft2mAq%2FbtsQEoILnEe%2FhF8OLdESPqQeBsQzRKOp7k%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;300&quot; height=&quot;157&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;157&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;&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;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. 프로퍼티(Props)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 프로퍼티(props)란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;properties의 줄임말&lt;/li&gt;
&lt;li&gt;상위 컴포넌트가 하위 컴포넌트에 값을 전달할 때 사용 (단방향)&lt;/li&gt;
&lt;li&gt;수정할 수 없다 (자식 입장에서 읽기 전용)&lt;/li&gt;
&lt;li&gt;[Example] props
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;text=&amp;rdquo;Click&amp;rdquo;이 props&lt;/li&gt;
&lt;li&gt;자식 컴포넌트(Button)이 이 값을 받아 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;Button text=&quot;Click&quot; /&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;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 컴포넌트에 props 전달하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.1. 자식 컴포넌트에 props 전달&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.js : 부모 컴포넌트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;person(객체)와 money(숫자) 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Content from &quot;./components/Content&quot;;

export default function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;Content
        person={{ name: &quot;홍길동&quot;, color:&quot;blue&quot;}}
        money={65000}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.2. 자식 컴포넌트 내부에서 props 읽기&lt;/h4&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Content({ person, money }) {
    return &amp;lt;h1 style={{color:person.color}}&amp;gt;{person.name}은 {money}원을 가지고 있습니다.&amp;lt;/h1&amp;gt;;
}

export default Content;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 인자(props 객체)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사실 props는 컴포넌트에 대한 유일한 인자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Content(props) {
    let person = props.person;
    let money = props.money;
    return &amp;lt;h1 style={{color:person.color}}&amp;gt;{person.name}은 {money}원을 가지고 있습니다.&amp;lt;/h1&amp;gt;;
}

export default Content;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.3. 기본값 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;money prop이 없거나 money={undefined}로 전달될 때 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;money={null} 또는 size={0}은 해당 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function Content({ person, money = 1000 }) {
	// ...
}
&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;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;h3 data-ke-size=&quot;size23&quot;&gt;1.3. children : 자식을 JSX로 전달하기&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;/ul&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;Content&amp;gt;
	&amp;lt;Button /&amp;gt;
&amp;lt;/Content&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSX 태그 내 콘턴츠를 중첩하면, 부모 컴포넌트는 해당 콘텐츠를 &lt;b&gt;children&lt;/b&gt;이라는 prop으로 받는다&lt;/li&gt;
&lt;li&gt;App.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Content from &quot;./components/Content&quot;;
import Button from &quot;./components/Button&quot;;

export default function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;Content&amp;gt;
        &amp;lt;Button name=&quot;홍길동&quot; color=&quot;blue&quot;/&amp;gt;
      &amp;lt;/Content&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Content.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Content({ children }) {
    return (
        &amp;lt;div className=&quot;content&quot;&amp;gt;
            {children}
        &amp;lt;/div&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;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4. 조건부 렌더링&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;if문 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function App({ isLoggedIn }) {
  let msg;
  if (isLoggedIn) {
    msg = &quot;로그인&quot;;
  } else {
    msg = &quot;로그아웃&quot;;
  }
  return (
    &amp;lt;div&amp;gt;
      {msg}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function App({ isLoggedIn }) {
  let msg = isLoggedIn ? &quot;로그인&quot; : &quot;로그아웃&quot;;
  return (
    &amp;lt;div&amp;gt;
      {msg}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;amp;&amp;amp; 연산자 사용 : else 분기가 필요하지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function App({ isLoggedIn }) {
  return (
    &amp;lt;div&amp;gt;
      {isLoggedIn &amp;amp;&amp;amp; &quot;로그인&quot;}
    &amp;lt;/div&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;&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;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;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;2. useState&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 상태(State)란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트에서의 State는 리액트 Component의 상태를 의미
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state 값이 변경되었을 경우, 해당 컴포넌트를 리렌더링&lt;/li&gt;
&lt;/ul&gt;
&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;같은 컴포넌트를 여러 번 렌더링하면 각각 컴포넌트는 고유한 State를 가짐&lt;/li&gt;
&lt;/ul&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;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;2.2. useState란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트에 state 변수를 추가할 수 있는 React Hook
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state의 **초기값(initialValue)**를 넘겨줄 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;state&lt;/b&gt;와 &lt;b&gt;setter 함수&lt;/b&gt;를 하나의 배열로 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;const [state, setState] = useState(initialState)

// example
const [name, setName] = useState('Gildong');
&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;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;2.3. 예제로 알아보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useState를 호출하여 state 변수(count) 선언 : 초기값 0&lt;/li&gt;
&lt;li&gt;set 함수(setCount)를 통해 state 변수(count) 업데이트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트는 다음 state를 저장하고 새로운 값으로 컴포넌트 다시 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

export default function App() {

  const [count, setCount] = useState(0);

  const handleClick = () =&amp;gt; {
    setCount(count + 1);
  };

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;p&amp;gt;현재 값 : {count}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;+1&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;&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이벤트 핸들러&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클릭, 마우스 호버, 폼 인풋 포커스 등 사용자 상호작용에 따라 유발되는 사용자 정의 함수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;handle로 시작하고 그 뒤에 이벤트명을 붙인 함수명&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;함수를 정의하고 JSX 태그에 prop 형태로 전달&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;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 이벤트 핸들러 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1.1. 함수를 전달하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트(Button) 내부에 이벤트 함수(handleClick) 선언&lt;/li&gt;
&lt;li&gt;함수 내부 로직 구현&lt;/li&gt;
&lt;li&gt;JSX 태그에 함수 전달
&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;/ul&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt; &amp;lt;button onClick={handleClick}&amp;gt;클릭&amp;lt;/button&amp;gt;     // 올바른 예시 
 &amp;lt;button onClick={handleClick()}&amp;gt;클릭&amp;lt;/button&amp;gt;   // 잘못된 예시
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1.2. JSX 안에 익명함수 쓰기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&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;/ul&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;&amp;lt;button onClick={() =&amp;gt; alert(&quot;Clicked&quot;)}&amp;gt;클릭&amp;lt;/button&amp;gt;  // 올바른 예시
&amp;lt;button onClick={alert(&quot;Clicked&quot;)}&amp;gt;클릭&amp;lt;/button&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;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. onChange&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;input의 텍스트가 바뀔 때 발생하는 이벤트&lt;/li&gt;
&lt;li&gt;e.target.value를 통해 이벤트 객체에 담긴 input 값을 읽어올 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;

export default function App() {
  const [name, setName] = useState(&quot;&quot;);

  const handleChange = (e) =&amp;gt; {
    setName(e.target.value);
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input type=&quot;text&quot; value={name} onChange={handleChange}&amp;gt;&amp;lt;/input&amp;gt;
      &amp;lt;h1&amp;gt;{name}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&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;&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;h3 data-ke-size=&quot;size23&quot;&gt;3.3. onClick&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;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; alert(&quot;알림창입니다.&quot;)}&amp;gt;Button&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;&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;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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn&quot;&gt;https://ko.react.dev/learn&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/describing-the-ui&quot;&gt;https://ko.react.dev/learn/describing-the-ui&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@rookieand/React의-state-그리고-useState에-대해-더-알고-싶어졌다&quot;&gt;https://velog.io/@rookieand/React의-state-그리고-useState에-대해-더-알고-싶어졌다&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/useState&quot;&gt;https://ko.react.dev/reference/react/useState&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@ko9612/React-이벤트-처리&quot;&gt;https://velog.io/@ko9612/React-이벤트-처리&lt;/a&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;&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;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;</description>
      <category>FrontEnd</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/376</guid>
      <comments>https://codesyun.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0props-useState-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%95%B8%EB%93%A4%EB%9F%AConChange-onClick#entry376comment</comments>
      <pubDate>Wed, 17 Sep 2025 22:59:02 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트(React)란? : 정의 / 실행환경 구축 / JSX 기본 문법</title>
      <link>https://codesyun.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8React%EB%9E%80-%EC%A0%95%EC%9D%98-%EC%8B%A4%ED%96%89%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-JSX-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</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;nbsp;&lt;/p&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-filename=&quot;react.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zRHyQ/btsQBGYzAfO/XRfr848k1md2AkeH7rWSi1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zRHyQ/btsQBGYzAfO/XRfr848k1md2AkeH7rWSi1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zRHyQ/btsQBGYzAfO/XRfr848k1md2AkeH7rWSi1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzRHyQ%2FbtsQBGYzAfO%2FXRfr848k1md2AkeH7rWSi1%2Fimg.webp&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;300&quot; height=&quot;158&quot; data-filename=&quot;react.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;378&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;&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;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;0. React (리액트)&lt;br /&gt;&lt;br /&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0.1. React란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리액트(React)&lt;/b&gt; : SPA을 위한 자바스크립트 라이브러리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 UI를 작성하기 위한 목적&lt;/li&gt;
&lt;li&gt;재사용을 위해 &amp;lsquo;컴포넌트&amp;rsquo;라는 작은 단위 기반 개발&lt;/li&gt;
&lt;li&gt;여러 개의 컴포넌트를 조합하여 UI 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SPA(Single Page Application)&lt;/b&gt; : 서버로부터 완전한 새로운 페이지를 불러오지 않고 현재 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 애플리케이션이나 웹사이트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML, CSS, JavaScript 파일을 최초 1회만 로드&lt;/li&gt;
&lt;li&gt;이후에는 JS 파일을 통해 DOM 또는 필요한 HTML 파일을 조작&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;0.2. React의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0.2.1. 리액트 컴포넌트&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 특정 부분과 관련된 기능을 컴포넌트 단위로 개발 (JavaScript)
&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;/li&gt;
&lt;li&gt;[Example]&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// SimpleComponent.js

// import React from 'react'; // React 17부터는 생략 가능

const SimpleComponent = () =&amp;gt; {
  return &amp;lt;div&amp;gt;Hello World!&amp;lt;/div&amp;gt;;
};

export default SimpleComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hello World! 라는 텍스트를 출력하는 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.js

import SimpleComponent from './SimpleComponent';

const App = () =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;Welcome to my React App!&amp;lt;/h2&amp;gt;
      &amp;lt;SimpleComponent /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default App;

&lt;/code&gt;&lt;/pre&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;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById('root'));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReactDOM.render를 사용하여 App 컴포넌트를 렌더링하여 화면에 나타남&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0.2.2. JSX 문법&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const SimpleComponent = () =&amp;gt; {
  return &amp;lt;div&amp;gt;Hello World!&amp;lt;/div&amp;gt;;
};

export default SimpleComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML 마크업처럼 보이는 코드는 JavaScript 확장 문법인 &lt;b&gt;JSX(JavaScript XML)&lt;/b&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;SPA의 단점인 자바스크립트 코드로 화면 렌더링을 제어해야 하는 불편함 해결&lt;/li&gt;
&lt;li&gt;HTML 마크업과 유사하여 협업에 편리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0.2.3. 가상 DOM (Virtual DOM) 방식&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;0.2.3.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5fT1o/btsQEF4qZUS/r3po76jN4xqKIhWeFwfrj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5fT1o/btsQEF4qZUS/r3po76jN4xqKIhWeFwfrj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5fT1o/btsQEF4qZUS/r3po76jN4xqKIhWeFwfrj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5fT1o%2FbtsQEF4qZUS%2Fr3po76jN4xqKIhWeFwfrj1%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;700&quot; height=&quot;261&quot; data-filename=&quot;0.2.3.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트 컴포넌트는 최종적으로 UI를 위한 DOM 요소를 생성하고 조작&lt;/li&gt;
&lt;li&gt;DOM에 변화 생기면 브라우저는 랜더링 &amp;rarr; 성능 문제 원인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가상 DOM
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 변화가 일어나면 먼저 VDOM에서 연산하고 달라진 부분만 DOM에 전달&lt;/li&gt;
&lt;li&gt;모든 변화를 한 번에 묶어서 전달 &amp;rarr; 렌더링 규모가 크지만 딱 한 번만 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0.2.4. 단방향 데이터 바인딩&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트에서 뷰가 바뀌는 과정 : 자바 스크립트 객체인 State로 시작 &amp;rarr; State 기반으로 Components 생성 &amp;rarr; Components 기반으로 가상 DOM 그림 &amp;rarr; 실제 DOM하여 화면에 뿌림&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. React 실행 환경 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. npm &amp;amp; node 설치 확인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전에 설치해두었기에 version update만 진행!&lt;/li&gt;
&lt;li&gt;version 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node -v
npm -v 
&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;h3 data-ke-size=&quot;size23&quot;&gt;1.2. React 프로젝트 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Facebook에서 제공하는 Create React App 도구 : 초기 설정 자동으로 해줌!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;npx create-react-app myreact
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 디렉토리로 이동하여 실행 &amp;rarr; &lt;a href=&quot;http://localhost:3000&quot;&gt;localhost:3000&lt;/a&gt; 열림&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;cd myreact
npm start
&lt;/code&gt;&lt;/pre&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-filename=&quot;1.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EzRCu/btsQEeTxo6S/maaQodqqL870qs8skll5L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EzRCu/btsQEeTxo6S/maaQodqqL870qs8skll5L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EzRCu/btsQEeTxo6S/maaQodqqL870qs8skll5L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEzRCu%2FbtsQEeTxo6S%2FmaaQodqqL870qs8skll5L1%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;1084&quot; height=&quot;763&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;763&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;h3 data-ke-size=&quot;size23&quot;&gt;1.3. 디렉토리 구조&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.3.png&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TM64e/btsQEhpbD6T/KqwOPYVAoYk0pMJlJPdyMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TM64e/btsQEhpbD6T/KqwOPYVAoYk0pMJlJPdyMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TM64e/btsQEhpbD6T/KqwOPYVAoYk0pMJlJPdyMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTM64e%2FbtsQEhpbD6T%2FKqwOPYVAoYk0pMJlJPdyMK%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;273&quot; height=&quot;520&quot; data-filename=&quot;1.3.png&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public/ : 정적 파일이 위치하는 폴더 (ex. index.html)&lt;/li&gt;
&lt;li&gt;src/ : 실제 개발을 진행하는 폴더 (React 컴포넌트, JS 파일 위치)&lt;/li&gt;
&lt;li&gt;node_modules/ : 프로젝트에 필요한 모든 npm 패키지가 설치되는 폴더&lt;/li&gt;
&lt;li&gt;package.json : 프로젝트 설정과 의존성 목록 포함된 파일&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;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;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;2. React 컴포넌트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 컴포넌트 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src 폴더 내에 components 폴더 생성&lt;/li&gt;
&lt;li&gt;그 안에 Hello.js 파일 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Hello() {
  return &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. App.js에서 불러오기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hello 컴포넌트를 불러와서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Hello from &quot;./components/Hello&quot;;

function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;Hello /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR65r3/btsQBhLuFgs/Z7fe9SP6TN42IRK15zhkPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR65r3/btsQBhLuFgs/Z7fe9SP6TN42IRK15zhkPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR65r3/btsQBhLuFgs/Z7fe9SP6TN42IRK15zhkPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR65r3%2FbtsQBhLuFgs%2FZ7fe9SP6TN42IRK15zhkPK%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;773&quot; height=&quot;520&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. 여러 컴포넌트 조합하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 컴포넌트들을 가져와서 큰 컴포넌트를 만드는 React 패턴 보여주기&lt;/li&gt;
&lt;li&gt;Header.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Header() {
    return &amp;lt;h1&amp;gt;안녕하세요! 이것은 헤더입니다.&amp;lt;/h1&amp;gt;;
}

export default Header;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Content.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Content() {
    return &amp;lt;p&amp;gt;여기가 본문입니다.&amp;lt;/p&amp;gt;;
}

export default Content;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Footer.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Footer() {
    return &amp;lt;small&amp;gt;&amp;copy; 2025 Syun&amp;lt;/small&amp;gt;;
}

export default Footer;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Header from &quot;./components/Header&quot;;
import Content from &quot;./components/Content&quot;;
import Footer from &quot;./components/Footer&quot;;

function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;Content /&amp;gt;
      &amp;lt;Footer /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.9.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LkeLy/btsQDt4Fk3M/YjBaXVKyZGEd6fOzH86cLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LkeLy/btsQDt4Fk3M/YjBaXVKyZGEd6fOzH86cLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LkeLy/btsQDt4Fk3M/YjBaXVKyZGEd6fOzH86cLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLkeLy%2FbtsQDt4Fk3M%2FYjBaXVKyZGEd6fOzH86cLK%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;773&quot; height=&quot;520&quot; data-filename=&quot;2.9.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&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;&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;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;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;3. JSX 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 변수 선언 및 넣기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수 선언
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;const : 바뀌지 않는 값(상수) 선언 시&lt;/li&gt;
&lt;li&gt;let : 값이 바뀌는 변수 선언 시&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;변수 바인딩 : {변수}&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Hello() {
    const name = &quot;gildong&quot;;
    let age = 20;
    return (
        &amp;lt;h1&amp;gt;Hello, {name}! You are {age} years old.&amp;lt;/h1&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.1.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj744b/btsQCMQUS2L/UvIQntyVhHS0Q9I1Icza0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj744b/btsQCMQUS2L/UvIQntyVhHS0Q9I1Icza0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj744b/btsQCMQUS2L/UvIQntyVhHS0Q9I1Icza0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj744b%2FbtsQCMQUS2L%2FUvIQntyVhHS0Q9I1Icza0K%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;773&quot; height=&quot;520&quot; data-filename=&quot;3.1.png&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;520&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;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 태그의 class 이름&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML 객체 태그의 클래스는 기존의 class가 아닌 &lt;b&gt;className&lt;/b&gt;으로 표기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;클래스이름&quot;&amp;gt;
...
&amp;lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 인라인 CSS 스타일&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 CSS 문법에서 대쉬(-) 빼고 카멜 표기법으로 변경 (ex. background-color &amp;rarr; backgroundColor)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
    const myStyle = {
        backgroundColor : 'red',
        fontSize : 18
    }
  return (
    &amp;lt;div style={myStyle}&amp;gt;
      ...
    &amp;lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4. 조건문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 return 외부에선 if 사용 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 return 내부에선 if 사용 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function MyComponent() {
    if (true) {
        return(&amp;lt;div&amp;gt;참 입니다.&amp;lt;/div&amp;gt;)
    }else{
        return(&amp;lt;div&amp;gt;거짓 입니다.&amp;lt;/div&amp;gt;)
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 return문 내부에서는 삼항연산자 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Hello() {
    const age = 20;
    return (
        &amp;lt;div&amp;gt;
            { age &amp;gt;= 20 ? (
                &amp;lt;p&amp;gt;성인입니다.&amp;lt;/p&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;&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;3.5. 반복문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;return문 안에는 for / forEach문 사용 불가 ❌&lt;/li&gt;
&lt;li&gt;map 메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Week() {
    const weekArr = [&quot;MON&quot;, &quot;TUE&quot;, &quot;WED&quot;, &quot;THU&quot;, &quot;FRI&quot;, &quot;SAT&quot;, &quot;SUN&quot;];
    return (
        &amp;lt;p&amp;gt;
            {weekArr.map((week, index) =&amp;gt; (
                &amp;lt;span key={index}&amp;gt;
                    {week}&amp;lt;br /&amp;gt;
                &amp;lt;/span&amp;gt;
            ))}
        &amp;lt;/p&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Week() {
    const weekArr = [&quot;MON&quot;, &quot;TUE&quot;, &quot;WED&quot;, &quot;THU&quot;, &quot;FRI&quot;, &quot;SAT&quot;, &quot;SUN&quot;];

    const rendering = () =&amp;gt; {
        const result = [];
        for (let i = 0; i &amp;lt; weekArr.length; i++) {
            result.push(&amp;lt;span key={i}&amp;gt;{weekArr[i] + &quot; / &quot;}&amp;lt;/span&amp;gt;);
        }
        return result;
    }

    return (
        &amp;lt;p&amp;gt;{rendering()}&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;&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;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;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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coding-factory.tistory.com/1085&quot;&gt;https://coding-factory.tistory.com/1085&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://space-engineers-developer.tistory.com/55#google_vignette&quot;&gt;https://space-engineers-developer.tistory.com/55#google_vignette&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://contentstoaster.tistory.com/entry/react-must-know-commands#google_vignette&quot;&gt;https://contentstoaster.tistory.com/entry/react-must-know-commands#google_vignette&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codingbroker.tistory.com/123&quot;&gt;https://codingbroker.tistory.com/123&lt;/a&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;&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;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;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;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;</description>
      <category>FrontEnd</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/375</guid>
      <comments>https://codesyun.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8React%EB%9E%80-%EC%A0%95%EC%9D%98-%EC%8B%A4%ED%96%89%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-JSX-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95#entry375comment</comments>
      <pubDate>Wed, 17 Sep 2025 16:22:34 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] EC2-Docker-GitHub Actions CI/CD 구성 (2) : GitHub Actions workflow 및 Build &amp;amp; Deploy를 위한 파일 설정</title>
      <link>https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95</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;nbsp;&lt;/p&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-filename=&quot;cicd.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odWoy/btsQydHHwte/w57t6okONhZ7pp22f5s0fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odWoy/btsQydHHwte/w57t6okONhZ7pp22f5s0fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odWoy/btsQydHHwte/w57t6okONhZ7pp22f5s0fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FodWoy%2FbtsQydHHwte%2Fw57t6okONhZ7pp22f5s0fk%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;200&quot; height=&quot;104&quot; data-filename=&quot;cicd.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;이전 글에 이어서 CI/CD 구성을 이어서 함미다&lt;br /&gt;이전 글 : &lt;a href=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-EC2-RDS-Docker-%EC%84%B8%ED%8C%85&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[CI/CD] EC2-Docker-GitHub Actions CI/CD 구성 (1) : EC2 / RDS / Docker 세팅&lt;/a&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;&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;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;1. EC2 서버 환경 설정&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;/ul&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;sudo mkdir -p /opt/posleep
sudo chown ec2-user:ec2-user /opt/posleep  # 소유자/그룹 변경
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 환경변수 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 : /opt/posleep/app.env&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;cat &amp;gt; app.env &amp;lt;&amp;lt;'EOF'
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:mariadb://&amp;lt;RDS-Endpoint&amp;gt;:3306/posleep?useUnicode=true&amp;amp;characterEncoding=utf8
SPRING_DATASOURCE_USERNAME=&amp;lt;RDS-Master-계정&amp;gt;
SPRING_DATASOURCE_PASSWORD=&amp;lt;RDS-Master-암호&amp;gt;
SERVER_PORT=8080
EOF
&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Repo에 설정 파일 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.0. Repo 구조&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;.
├─ posleep/
│  ├─ Dockerfile                   # 1.컨테이너 빌드 정의
│  ├─ .dockerignore                # 2.도커 빌드에서 제외할 파일들
│  └─ src/...                      # 기존 애플리케이션 소스
│
├─ src/main/resources/
│  └─ application-prod.properties  # 3.운영 환경의 Spring 환경 설정
│  └─ application.properties
│
├─ deploy/
│  └─ docker-compose.yml           # 4.컨테이너 실행을 자동화하는 설정
│
└─ ~~.github/workflows/~~
   └─ ~~cicd.yml~~                     # ~~GitHub Actions에서 등록 예정~~
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. Dockerfile&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 : &amp;lt;repo&amp;gt;/posleep/Dockerfile&lt;/li&gt;
&lt;li&gt;역할 : 애플리케이션 배포용 이미지(DockerImage)를 생성하기 위한 스크립트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소스코드를 빌드(gradlew) &amp;rarr; .jar 패키징&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# ---- Build stage ----
FROM eclipse-temurin:17-jdk-jammy AS build
WORKDIR /app

# gradle wrapper 먼저 복사(캐시 최적화)
COPY gradlew gradle/ ./
RUN chmod +x gradlew

# 전체 소스 복사
COPY . .

RUN ./gradlew clean bootJar --no-daemon

# ---- Runtime stage ----
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar

EXPOSE 8080
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app/app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 스테이지 (Build Stage)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK가 들어있는 이미지에서 Gradle로 소스를 빌드해서 실행 jar 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bootJar 결과물은 보통 build/libs/xxxx.jar에 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;빌드에 필요한 도구(JDK, Gradle 캐시 등)는 이 단계에서만 쓰이고 최종 이미지에는 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;런타임 스테이지 (Runtime Stage)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JRE만 들어있는 가벼운 이미지에, 앞에서 만든 JAR 하나만 복사 &amp;rarr; 작고 빠른 배포용 이미지&lt;/li&gt;
&lt;li&gt;컨테이너는 java -jar /app/app.jar로 애플리케이션 실행&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. .dockerignore&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 : &amp;lt;repo&amp;gt;/posleep/.dockerignore&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;빌드 속도 &amp;uarr;, 이미지 캐시 효율 &amp;uarr;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.git
.gradle
build
out
.idea
*.iml
Dockerfile
docker-compose.yml
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. application-prod.properties&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 : &amp;lt;repo&amp;gt;/src/main/resources/application-prod.properties&lt;/li&gt;
&lt;li&gt;역할 : 운영 환경에서 실행할 때의 Spring 설정 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;server.port=${SERVER_PORT:8080}

# --- DataSource ---
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

# --- JPA/Hibernate ---
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false

# --- LOG ---
logging.level.com.syun.posleep=INFO
logging.level.org.hibernate.SQL=INFO
spring.jpa.properties.hibernate.format_sql=true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(기존) application.properties : 개발용 Spring 설정 파일 (로컬 DB, 로그레벨 DEBUG 등)&lt;/li&gt;
&lt;li&gt;(신규) application-prod.properties : 운영용 Spring 설정 파일 (RDS DB, 로그레벨 INFO 등)&lt;/li&gt;
&lt;li&gt;운영 배포 시에는 SPRING_PROFILES_ACTIVE=prod 환경변수로 이 파일 활성화&lt;/li&gt;
&lt;li&gt;DB 연결정보 등 환경 변수를 app.env에서 정의한 값으로 불러오도록 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 / GitHub에서 민감 정보 노출 X&lt;/li&gt;
&lt;/ul&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;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;2.4. docker-compose.yml&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 : &amp;lt;repo&amp;gt;/deploy/docker-compose.yml&lt;/li&gt;
&lt;li&gt;역할 : EC2에서 컨테이너 실행을 자동화하는 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: &quot;3.8&quot;

services:
  app:
    image: syun32/posleep:prod
    container_name: posleep
    env_file:
      - ./app.env
    ports:
      - &quot;8080:8080&quot;
    restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;docker compose up -d 명령어 한번으로 환경변수, 포트, 이미지 등 실행 옵션 적용&lt;/li&gt;
&lt;li&gt;app.env 파일에서 환경변수 불러와 컨테이너에 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application-prod.properties의 ${&amp;hellip;} 자리에 매핑&lt;/li&gt;
&lt;/ul&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;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;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;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;3. GitHub Actions 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. GitHub Secrets 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Settings &amp;gt; Security &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; Repository secrets &amp;gt; New repository screts&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;DOCKERHUB_USERNAME&lt;/b&gt; : &amp;lt;Docker-USERNAME&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DOCKERHUB_TOKEN&lt;/b&gt; : &amp;lt;Docker-Hub-Personal-Access-Token&amp;gt; (이전에 복사해둔 것)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC2_HOST&lt;/b&gt; : &amp;lt;EC2-퍼블릭-IP&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC2_USER&lt;/b&gt; : (보통) ec2-user&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC2_SSH_KEY&lt;/b&gt; : &amp;lt;EC2 .pem 키 내용 전체&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cat &amp;lt;key-name&amp;gt;.pem 명령어로 키 내용 확인 가능&lt;/li&gt;
&lt;li&gt;-----BEGIN... / -----END... + 줄바꿈 포함해야 함! (내용물만 넣었다가 오류남,,,)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ cat &amp;lt;key-name&amp;gt;.pem

-----BEGIN OPENSSH PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcw...
...
-----END OPENSSH PRIVATE KEY-----&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;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;3.2. GitHub Action workflow 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;repo의 Actions 탭에서 [set up a workflow yourself]&lt;/li&gt;
&lt;li&gt;cicd.yml 파일 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 푸시 &amp;rarr; 도커 이미지 빌드/푸시 &amp;rarr; EC2 배포까지를 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: CI-CD

on:
  push:
    branches: [ &quot;main&quot; ]
  workflow_dispatch:

env:
  IMAGE_NAME: syun32/posleep
  IMAGE_TAG: prod

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # (멀티스테이지 Dockerfile이 빌드까지 처리한다는 전제)
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build &amp;amp; Push (posleep module)
        uses: docker/build-push-action@v6
        with:
          context: ./posleep
          file: ./posleep/Dockerfile
          platforms: linux/amd64
          push: true
          tags: |
            ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
            ${{ env.IMAGE_NAME }}:${{ github.sha }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Checkout (to get compose file)
        uses: actions/checkout@v4

      - name: Upload docker-compose.yml to EC2
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}           # -----BEGIN OPENSSH PRIVATE KEY-----
          source: &quot;deploy/docker-compose.yml&quot;
          target: &quot;/opt/posleep/&quot;

      - name: SSH Deploy on EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            set -e
            cd /opt/posleep

            # docker-compose.yml 위치 표준화
            if [ -f /opt/posleep/deploy/docker-compose.yml ]; then
              mv -f /opt/posleep/deploy/docker-compose.yml /opt/posleep/docker-compose.yml
              rmdir /opt/posleep/deploy 2&amp;gt;/dev/null || true
            fi

            # Private repo면 로그인 필요
            if [ -n &quot;${{ secrets.DOCKERHUB_USERNAME }}&quot; ] &amp;amp;&amp;amp; [ -n &quot;${{ secrets.DOCKERHUB_TOKEN }}&quot; ]; then
              docker login -u &quot;${{ secrets.DOCKERHUB_USERNAME }}&quot; -p &quot;${{ secrets.DOCKERHUB_TOKEN }}&quot;
            fi

            # 최신 이미지 pull 후 재기동
            docker pull ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
            docker compose down || true
            docker compose up -d

            # 오래된 이미지 정리
            docker image prune -f
&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;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;3.3. GitHub Actions 실행 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3.1. Trigger&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;main 브랜치로 push 되면 자동 실행&lt;/li&gt;
&lt;li&gt;Actions &amp;gt; Run workflow으로 수동 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3.2. CI (build-and-push job)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Source Check-Out : 저장소 코드 내려받기&lt;/li&gt;
&lt;li&gt;Docker Buildx 준비&lt;/li&gt;
&lt;li&gt;DockerHub 로그인 : Secrets에 등록된 값으로 로그인&lt;/li&gt;
&lt;li&gt;Dockerfile로 빌드 &amp;amp; Docker Hub에 Push&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3.3. CD (deploy job)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EC2로 docker-compose.yml 업로드&lt;/li&gt;
&lt;li&gt;SSH로 접속 : Secrets에 등록된 정보으로 접속&lt;/li&gt;
&lt;li&gt;배포 스크립트 실행(SSH)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최신 이미지 pull&lt;/li&gt;
&lt;li&gt;docker compose up -d 재기동&lt;/li&gt;
&lt;li&gt;오래된 이미지 청소&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&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;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;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;999. ⭐️ 성공 ⭐️&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buxJIG/btsQxwnsOFa/EBMNs5cJHp15cc6LtBtwE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buxJIG/btsQxwnsOFa/EBMNs5cJHp15cc6LtBtwE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buxJIG/btsQxwnsOFa/EBMNs5cJHp15cc6LtBtwE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuxJIG%2FbtsQxwnsOFa%2FEBMNs5cJHp15cc6LtBtwE0%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;1116&quot; height=&quot;473&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;하핫 Dockerfile, docker-compose 파일을 나중에 올려서 Action 부터 생성한 첫번째 workflow는 실패&lt;br /&gt;But 다 올린 최신 workflow는 성공&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MCjyT/btsQxrmsOyj/Nw3hHrVEj7WNd8z74pzi9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MCjyT/btsQxrmsOyj/Nw3hHrVEj7WNd8z74pzi9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MCjyT/btsQxrmsOyj/Nw3hHrVEj7WNd8z74pzi9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMCjyT%2FbtsQxrmsOyj%2FNw3hHrVEj7WNd8z74pzi9K%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;1255&quot; height=&quot;759&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;759&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&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;&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;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;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;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Project GitHub&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot;&gt;https://github.com/syun32/2025-posleep&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757689275537&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - syun32/2025-posleep&quot; data-og-description=&quot;Contribute to syun32/2025-posleep development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lKR6k/hyZI0GhILW/ygQkJ29kiu8qbMX8zKi6WK/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/b5aNVU/hyZJgamdFc/FQvlSettzU76KXsbMaukE1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/syun32/2025-posleep&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lKR6k/hyZI0GhILW/ygQkJ29kiu8qbMX8zKi6WK/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/b5aNVU/hyZJgamdFc/FQvlSettzU76KXsbMaukE1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - syun32/2025-posleep&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to syun32/2025-posleep development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spongecake.tistory.com/189?category=709735&quot;&gt;https://spongecake.tistory.com/189?category=709735&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@sysy123/Github-Actions-CICD&quot;&gt;https://velog.io/@sysy123/Github-Actions-CICD&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sjh9708.tistory.com/237#google_vignette&quot;&gt;https://sjh9708.tistory.com/237#google_vignette&lt;/a&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;&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;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;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;</description>
      <category>DevOps</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/374</guid>
      <comments>https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95#entry374comment</comments>
      <pubDate>Fri, 12 Sep 2025 23:57:22 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] EC2-Docker-GitHub Actions CI/CD 구성 (1) : EC2 / RDS / Docker 세팅</title>
      <link>https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-EC2-RDS-Docker-%EC%84%B8%ED%8C%85</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;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cicd.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd1kvs/btsQx54bziK/ivXRaMgYTizSn80amEatoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd1kvs/btsQx54bziK/ivXRaMgYTizSn80amEatoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd1kvs/btsQx54bziK/ivXRaMgYTizSn80amEatoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd1kvs%2FbtsQx54bziK%2FivXRaMgYTizSn80amEatoK%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;300&quot; height=&quot;156&quot; data-filename=&quot;cicd.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;포켓몬 슬립 게임하면서 취미로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;요리 레시피 계산기 사이트를 만들고 있었는데&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;CI/CD 구성을 해보고 싶더라구요&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;전체적인 네이밍은 무심코 설정했던 posleep!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;렛츠고  &lt;/span&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;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. CI/CD 구성&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duo8pQ/btsQvNcrpw4/n2BifDhmvkOb3xAHKk1jX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duo8pQ/btsQvNcrpw4/n2BifDhmvkOb3xAHKk1jX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duo8pQ/btsQvNcrpw4/n2BifDhmvkOb3xAHKk1jX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fduo8pQ%2FbtsQvNcrpw4%2Fn2BifDhmvkOb3xAHKk1jX0%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;1806&quot; height=&quot;348&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;348&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;h3 data-ke-size=&quot;size23&quot;&gt;0.1. 구성 요소&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;GitHub&lt;/b&gt; (소스 저장소 + Actions)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actions 트리거 : main 브랜치에 push&lt;/li&gt;
&lt;li&gt;.github/workflows/cicd.yml 파일에 빌드/배포 파이프라인 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker Hub&lt;/b&gt; (이미지 저장소)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Actions가 빌드한 Spring 애플리케이션 이미지를 push&lt;/li&gt;
&lt;li&gt;EC2는 최신 이미지를 pull 받아서 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS EC2&lt;/b&gt; (애플리케이션 서버)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker + Docker Compose로 컨테이너 실행&lt;/li&gt;
&lt;li&gt;/opt/posleep/docker-compose.yml 기준으로 애플리케이션 구동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS RDS&lt;/b&gt; (데이터베이스)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2에서 접근하는 DB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0.2. 동작 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 &amp;amp; Commit&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬에서 코드 작성 후 원격 저장소로 push&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Actions (CI 단계)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 이미지 빌드 (posleep.jar &amp;rarr; Docker image)&lt;/li&gt;
&lt;li&gt;빌드된 이미지를 Docker Hub에 push (&amp;lt;userName&amp;gt;/posleep:prod)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Actions (CD 단계)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scp-action으로 deploy/docker-compose.yml을 EC2 /opt/posleep 경로에 업로드&lt;/li&gt;
&lt;li&gt;ssh-action으로 EC2 ssh 접속하여 Docker Compose 재배포&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EC2&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최신 이미지 실행 (8080 포트 바인딩)&lt;/li&gt;
&lt;li&gt;RDS와 연결하여 서비스 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에서 http://&amp;lt;EC2-IP&amp;gt;:8080 접속&lt;/li&gt;
&lt;li&gt;Spring 애플리케이션 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;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;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. AWS EC2 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. EC2 인스턴스 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AMI : Amazon Linux 2023 kernel-6.1 AMI (64비트 x86_64)&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;SSH(22), HTTP(80), HTTPS(443), 사용자 지정 TCP(8080)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;키 페어 생성(pem) : SSH 접속을 위한 키
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키 파일 내용 출력을 위한 터미널 명령어 : cat &amp;lt;key-name&amp;gt;.pem&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 서버 기본 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.1. JDK 설치&lt;/h4&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;sudo dnf install java-17-amazon-corretto
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.2. Docker 설치&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo yum install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
newgrp docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2.3. Docker Compose 설치&lt;/h4&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;docker-compose.yml 파일에 실행환경 정의&lt;/li&gt;
&lt;li&gt;app.env 같은 환경변수를 compose에서 자동 주입 (RDS 접속 정보 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;sudo mkdir -p /usr/local/lib/docker/cli-plugins
sudo curl -L &quot;&amp;lt;https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-linux-x86_64&amp;gt;&quot; \\
  -o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker compose version
&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;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;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;2. AWS RDS 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. RDS 인스턴스 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔진 : MariaDB&lt;/li&gt;
&lt;li&gt;마스터 계정/비밀번호 설정&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;EC2 보안그룹에 3306 포트 오픈&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. 초기 테이블 세팅&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2를 통해 RDS 접속
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDS를 프라이빗하게 유지해두기 위해, EC2를 통해 접속&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DB 스키마 실행하여 초기 테이블 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flyway 같은 마이그레이션 도구 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Example. Squel Pro에서 접속 (평소에 쓰던 DB관리 툴)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL Host : &amp;lt;RDS-Endpoint&amp;gt;&lt;/li&gt;
&lt;li&gt;Username : &amp;lt;DB-Master-계정&amp;gt;&lt;/li&gt;
&lt;li&gt;Password : &amp;lt;DB-Master-비밀번호&amp;gt;&lt;/li&gt;
&lt;li&gt;Port : 3306&lt;/li&gt;
&lt;li&gt;SSH Host : &amp;lt;EC2-IP-address&amp;gt;&lt;/li&gt;
&lt;li&gt;SSH User : ec2-user&lt;/li&gt;
&lt;li&gt;SSH Key : pem-key 파일 불러오기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baPVIg/btsQwT4vBAS/lksEcxjHiesBMUXrmSmk81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baPVIg/btsQwT4vBAS/lksEcxjHiesBMUXrmSmk81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baPVIg/btsQwT4vBAS/lksEcxjHiesBMUXrmSmk81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaPVIg%2FbtsQwT4vBAS%2FlksEcxjHiesBMUXrmSmk81%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;912&quot; height=&quot;856&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;856&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;&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;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;3. Docker 세팅&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker hub에 리포지토리 생성 (posleep)&lt;/li&gt;
&lt;li&gt;Access Token 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Account settings &amp;gt; Personal access tokens &amp;gt; Generate new token&lt;/li&gt;
&lt;li&gt;Access permissions : Read &amp;amp; Write&lt;/li&gt;
&lt;li&gt;‼️ 한 번만 보여주니깐 복사해두기 &amp;rarr; GitHub Actions에 저장해두고 Docker Access에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQLpWs/btsQyooTeuG/ko1LBAZYLnAEqphAn0zsH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQLpWs/btsQyooTeuG/ko1LBAZYLnAEqphAn0zsH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQLpWs/btsQyooTeuG/ko1LBAZYLnAEqphAn0zsH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQLpWs%2FbtsQyooTeuG%2Fko1LBAZYLnAEqphAn0zsH0%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;679&quot; height=&quot;659&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;659&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;&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;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;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 style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이후 GitHub Actions, Dockerfile, docker-compose 등의 설정은&lt;br /&gt;다음 게시글에 이어서  &lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글 : &lt;a href=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[CI/CD]&amp;nbsp;EC2-Docker-GitHub&amp;nbsp;Actions&amp;nbsp;CI/CD&amp;nbsp;구성&amp;nbsp;(2)&amp;nbsp;:&amp;nbsp;GitHub&amp;nbsp;Actions&amp;nbsp;workflow&amp;nbsp;및&amp;nbsp;Build&amp;nbsp;&amp;amp;&amp;nbsp;Deploy를&amp;nbsp;위한&amp;nbsp;파일&amp;nbsp;설정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757689446516&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[CI/CD] EC2-Docker-GitHub Actions CI/CD 구성 (2) : GitHub Actions workflow 및 Build &amp;amp; Deploy를 위한 파일 설정&quot; data-og-description=&quot;1. EC2 서버 환경 설정배포 디렉터리 생성sudo mkdir -p /opt/posleepsudo chown ec2-user:ec2-user /opt/posleep # 소유자/그룹 변경애플리케이션 환경변수 설정위치 : /opt/posleep/app.envcat &amp;gt; app.env :3306/posleep?useUnicode=true&quot; data-og-host=&quot;codesyun.tistory.com&quot; data-og-source-url=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95#google_vignette&quot; data-og-url=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAs4j8/hyZJfoXQrG/JamrgrePiEhQkOn4zCpmw0/img.png?width=311&amp;amp;height=162&amp;amp;face=0_0_311_162,https://scrap.kakaocdn.net/dn/crUzz3/hyZJrhZcWv/AWxmKhfm8ktALKsVMODzm1/img.png?width=311&amp;amp;height=162&amp;amp;face=0_0_311_162,https://scrap.kakaocdn.net/dn/bV2IKW/hyZIXW6LN6/6NWokkgzzXBYWcttfArxB0/img.png?width=1255&amp;amp;height=759&amp;amp;face=0_0_1255_759&quot;&gt;&lt;a href=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95#google_vignette&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-2-GitHub-Actions-workflow-%EB%B0%8F-Build-Deploy%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%84%A4%EC%A0%95#google_vignette&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAs4j8/hyZJfoXQrG/JamrgrePiEhQkOn4zCpmw0/img.png?width=311&amp;amp;height=162&amp;amp;face=0_0_311_162,https://scrap.kakaocdn.net/dn/crUzz3/hyZJrhZcWv/AWxmKhfm8ktALKsVMODzm1/img.png?width=311&amp;amp;height=162&amp;amp;face=0_0_311_162,https://scrap.kakaocdn.net/dn/bV2IKW/hyZIXW6LN6/6NWokkgzzXBYWcttfArxB0/img.png?width=1255&amp;amp;height=759&amp;amp;face=0_0_1255_759');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CI/CD] EC2-Docker-GitHub Actions CI/CD 구성 (2) : GitHub Actions workflow 및 Build &amp;amp; Deploy를 위한 파일 설정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. EC2 서버 환경 설정배포 디렉터리 생성sudo mkdir -p /opt/posleepsudo chown ec2-user:ec2-user /opt/posleep # 소유자/그룹 변경애플리케이션 환경변수 설정위치 : /opt/posleep/app.envcat &amp;gt; app.env :3306/posleep?useUnicode=true&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codesyun.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; 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;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;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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Project GitHub&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/syun32/2025-posleep&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757689232399&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - syun32/2025-posleep&quot; data-og-description=&quot;Contribute to syun32/2025-posleep development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-url=&quot;https://github.com/syun32/2025-posleep&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lKR6k/hyZI0GhILW/ygQkJ29kiu8qbMX8zKi6WK/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/b5aNVU/hyZJgamdFc/FQvlSettzU76KXsbMaukE1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226&quot;&gt;&lt;a href=&quot;https://github.com/syun32/2025-posleep&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/syun32/2025-posleep&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lKR6k/hyZI0GhILW/ygQkJ29kiu8qbMX8zKi6WK/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226,https://scrap.kakaocdn.net/dn/b5aNVU/hyZJgamdFc/FQvlSettzU76KXsbMaukE1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1012_153_1079_226');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - syun32/2025-posleep&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to syun32/2025-posleep development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spongecake.tistory.com/189?category=709735&quot;&gt;https://spongecake.tistory.com/189?category=709735&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@sysy123/Github-Actions-CICD&quot;&gt;https://velog.io/@sysy123/Github-Actions-CICD&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sjh9708.tistory.com/237#google_vignette&quot;&gt;https://sjh9708.tistory.com/237#google_vignette&lt;/a&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;&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;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;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;</description>
      <category>DevOps</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/373</guid>
      <comments>https://codesyun.tistory.com/entry/CICD-EC2-Docker-GitHub-Actions-CICD-%EA%B5%AC%EC%84%B1-EC2-RDS-Docker-%EC%84%B8%ED%8C%85#entry373comment</comments>
      <pubDate>Fri, 12 Sep 2025 20:34:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 영속성 컨텍스트(Persistence Context) : 정의 / 사용 / FetchType</title>
      <link>https://codesyun.tistory.com/entry/Spring-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context-%EC%A0%95%EC%9D%98-%EC%82%AC%EC%9A%A9-FetchType</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;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQee2O/btsQsQUJTUY/QcgF1chwP7JxRX9Qum8CEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQee2O/btsQsQUJTUY/QcgF1chwP7JxRX9Qum8CEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQee2O/btsQsQUJTUY/QcgF1chwP7JxRX9Qum8CEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQee2O%2FbtsQsQUJTUY%2FQcgF1chwP7JxRX9Qum8CEk%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;200&quot; height=&quot;136&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&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;&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;1. 영속성 컨텍스트 (Persistence Context)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 영속성 컨텍스트란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA가 데이터베이스와 직접 연결하지 않고 엔티티 객체를 관리하는 메모리 공간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티를 관리하는 1차 캐시(저장소)&lt;/li&gt;
&lt;li&gt;매번 DB-JPA 직접 통신하지 않고, 엔티티를 보관하면서 변경 사항 추적&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 엔티티의 생명주기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;images_neptunes032_post_ecd3b113-862f-4158-a208-e1eeec92d61d_image.png&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oplZc/btsQv20gFMe/yfn8HIcum6Vi5LKetyqIB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oplZc/btsQv20gFMe/yfn8HIcum6Vi5LKetyqIB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oplZc/btsQv20gFMe/yfn8HIcum6Vi5LKetyqIB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoplZc%2FbtsQv20gFMe%2Fyfn8HIcum6Vi5LKetyqIB1%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;504&quot; height=&quot;379&quot; data-filename=&quot;images_neptunes032_post_ecd3b113-862f-4158-a208-e1eeec92d61d_image.png&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;379&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;h4 data-ke-size=&quot;size20&quot;&gt;1.2.1. 비영속 (new/transient)&lt;/h4&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;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Member member = new Member();
member.setId(&quot;member1&quot;);
member.setUsername(&quot;홍길동&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;h4 data-ke-size=&quot;size20&quot;&gt;1.2.2. 영속 (managed)&lt;/h4&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;EntityManager em = EntityManagerFactory.createEntityManager();
em.getTransaction.begin();

Member member new Member();
member.setId(&quot;member1&quot;);
member.setUsername(&quot;홍길동&quot;);

// 객체를 영속성 컨텍스트에 저장 (영속)
em.persist(member);
&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;h4 data-ke-size=&quot;size20&quot;&gt;1.2.3. 준영속 (detached)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영속성 컨텍스트에 저장되었다가 분리된 상태
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등 영속성 컨텍스트의 기능 동작 X&lt;/li&gt;
&lt;li&gt;식별자 값을 가지고 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 엔티티를 영속성 컨텍스트에서 분리 (준영속)
em.detach(member);
&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;h4 data-ke-size=&quot;size20&quot;&gt;1.2.4. 삭제 (removed)&lt;/h4&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;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제 (삭제)
em.remove(member);
&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;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;1.3. 영속성 컨텍스트의 특징&lt;/h3&gt;
&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;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;&lt;b&gt;영속성 컨텍스트와 데이터베이스 저장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 트랜잭션을 커밋하는 순간, 새로 저장된 엔티티를 DB에 반영 (flush)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1차 캐시&lt;/b&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;1차 캐시에 있으면 조회하여 반환 없으면 DB에서 조회하고 1차 캐시에 저장하고 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일성(identity) 보장&lt;/b&gt; : 동일한 트랜잭션에서는 같은 객체 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션을 지원하는 쓰기 지연 (Transactional write-behind)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티 매니저는 트랜잭션 커밋 전까지 내부 쿼리 저장소에 INSERT SQL 모아둠&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋 시 모아둔 쿼리를 DB에 보낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 감지(Dirty Checking)&lt;/b&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;/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;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;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;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;2. 영속성 컨텍스트의 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.0. EntityManager&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분 EntityManager를 통해 영속성 컨텍스트 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA에서 영속성 컨텍스트를 활용하려면 EntityManager를 사용해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spring Boot 환경에서는 @Transactional과 Spring Data JPA를 활용하면 EntityManager 없이도 가능&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;h3 data-ke-size=&quot;size23&quot;&gt;2.1. INSERT : persist()를 사용하여 엔티티 저장&lt;/h3&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;트랜잭션이 끝나면 JPA가 자동으로 DB에 INSERT SQL 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Member member = new Member();
member.setName(&quot;유진&quot;);

em.persist(member);  // 영속성 컨텍스트에 등록
&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;h3 data-ke-size=&quot;size23&quot;&gt;2.2. SELECT : find()를 사용하여 엔티티 조회&lt;/h3&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;같은 id의 엔티티를 여러 번 조회하면, DB에서 다시 조회하지 않고 같은 객체 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Transcational
public void findUser() {
	User user = entityManager.find(User.class, 1L);
}
&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;h3 data-ke-size=&quot;size23&quot;&gt;2.3. UPDATE : 엔티티 값을 변경하면 자동 반영&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 변경 감지(dirty checking) 기능이 있어 값을 바꾸면 자동으로 UPDATE 쿼리 실행&lt;/li&gt;
&lt;li&gt;persist() 없이 값만 바꿔도 트랜잭션 종료 시 자동 저장 (flush())&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Transactional
public void updateUser() {
	User user = entityManager.find(User.class, 1L);
	user.setName(&quot;홍길동&quot;);
	
	// flush() 자동 실행 -&amp;gt; 변경 내용이 DB에 UPDATE
&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;h3 data-ke-size=&quot;size23&quot;&gt;2.4. DELETE : remove()를 사용하여 엔티티 삭제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;remove()를 호출하면 영속성 컨텍스트에서 제거&lt;/li&gt;
&lt;li&gt;트랜잭션이 끝나면 자동으로 DELETE SQL 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Transactional
public void deleteUser() {
	User user = entityManager.find(User.class, 1L);
	entityManager.remove(user);
	
	// 트랜잭션이 끝나면 DELETE SQL 실행
}
&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;h3 data-ke-size=&quot;size23&quot;&gt;2.5. 관리 해제 : detach()를 사용하여 영속성 컨텍스트에서 분리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA가 이 객체를 관리하지 않으며, 이후에 변경해도 DB에 반영 X&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Transactional
public void detachUser() {
	User user = entityManager.find(User.class, 1L);
	entityManager.detach(user);
	
	user.setName(&quot;김평강&quot;);  // 변경했지만 DB에 반영되지 않음
}
&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;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;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;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;3. Fetch Type : EAGER / LAZY&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. Fetch Type이란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA가 하나의 Entity를 조회를 할 때, 연관관계에 있는 객체들을 어떻게 가져올 것이냐&lt;/li&gt;
&lt;li&gt;Default 값
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@xxToOne에서는 EAGER&lt;/li&gt;
&lt;li&gt;@xxToOne에서는 LAZY&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 즉시 로딩 (Eager)&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;pre class=&quot;css&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.EAGER);
&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;h3 data-ke-size=&quot;size23&quot;&gt;3.3. 지연 로딩 (LAZY)&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;최초 로드 시 실제 엔티티가 아닌 &amp;lsquo;엔티티의 프록시(Proxy)&amp;rsquo; 먼저 로드
&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;프록시가 target 엔티티의 메서드를 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY);
&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@neptunes032/JPA-영속성-컨텍스트란&quot;&gt;https://velog.io/@neptunes032/JPA-영속성-컨텍스트란&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ittrue.tistory.com/254&quot;&gt;https://ittrue.tistory.com/254&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://itconquest.tistory.com/entry/Spring-Boot-JPA-영속성-컨텍스트-완벽-이해&quot;&gt;https://itconquest.tistory.com/entry/Spring-Boot-JPA-영속성-컨텍스트-완벽-이해&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jinyeong-afk/기술-면접-즉시-로딩과-지연-로딩의-차&quot;&gt;https://velog.io/@jinyeong-afk/기술-면접-즉시-로딩과-지연-로딩의-차&lt;/a&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;&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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/372</guid>
      <comments>https://codesyun.tistory.com/entry/Spring-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context-%EC%A0%95%EC%9D%98-%EC%82%AC%EC%9A%A9-FetchType#entry372comment</comments>
      <pubDate>Thu, 11 Sep 2025 16:54:22 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring AOP로 Log 기록하기 : @Around / @Before / @After</title>
      <link>https://codesyun.tistory.com/entry/Spring-Spring-AOP%EB%A1%9C-Log-%EA%B8%B0%EB%A1%9D%ED%95%98%EA%B8%B0-Around-Before-After</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;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byqQpq/btsQstcGhVm/Z1qyOV6wtILdoWV5XsXy7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byqQpq/btsQstcGhVm/Z1qyOV6wtILdoWV5XsXy7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byqQpq/btsQstcGhVm/Z1qyOV6wtILdoWV5XsXy7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyqQpq%2FbtsQstcGhVm%2FZ1qyOV6wtILdoWV5XsXy7k%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;200&quot; height=&quot;136&quot; data-filename=&quot;spring_2.png&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;360&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring AOP&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AOP (Aspect-Oriented Programming)&lt;/b&gt; : 관점 지향 프로그래밍을 지원하는 기술&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;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;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 주요 용어&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;용어&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aspect&lt;/td&gt;
&lt;td&gt;공통적인 기능들을 모듈화한 것을 의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;Aspect가 적용될 대상 (메소드, 클래스 등이 해당)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join point&lt;/td&gt;
&lt;td&gt;Aspect가 적용될 수 있는 시점 (메소드 실행 전, 후 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advice&lt;/td&gt;
&lt;td&gt;Aspect의 기능을 정의한 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point cut&lt;/td&gt;
&lt;td&gt;Advice를 적용할 메소드의 범위를 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;1.2. 주요 어노테이션&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;어노테이션&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Aspect&lt;/td&gt;
&lt;td&gt;해당 클래스를 Aspect로 사용하겠다는 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Before&lt;/td&gt;
&lt;td&gt;대상 메소드가 실행되기 전에 Advice 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterReturning&lt;/td&gt;
&lt;td&gt;대상 메소드가 정상적으로 실행되고 반환된 후 Advice 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterThrowing&lt;/td&gt;
&lt;td&gt;대상 메소드에서 예외가 발생했을 때 Advice 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@After&lt;/td&gt;
&lt;td&gt;대상 메소드가 실행된 후에 Advice 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Around&lt;/td&gt;
&lt;td&gt;대상 메소드 실행 전, 후 또는 예외 발생 시 Advice 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring AOP로 Log 기록하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. Spring AOP 환경 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build.gradle에 의존성 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. AOP 클래스 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2.1. @Before / @After : 메소드 호출 전 / 후 로깅&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.syun.posleep.aspects;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class ApiLoggingAspect {

    @Pointcut(&quot;execution(* com.syun.posleep.web..*(..))&quot;)   // 적용 범위 정의
    public void controller() {
    }
    
    /* 메소드 호출 전 실행 */
    @Before(&quot;controller()&quot;)
    public void logBeforeTest(JoinPoint joinPoint) throws Throwable {
        log.info(&quot;Controller Before&quot;);
    }
    
    /* 메소드 호출 후 실행 */
    @After(&quot;controller()&quot;)
    public void logAfterTest(JoinPoint joinPoint) throws Throwable {
        log.info(&quot;Controller After&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JoinPoint : Aspect가 적용될 시점을 의미
&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;Log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFYJfr/btsQtbJm2X1/uvJKBskWu3WACeAWRaLPfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFYJfr/btsQtbJm2X1/uvJKBskWu3WACeAWRaLPfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFYJfr/btsQtbJm2X1/uvJKBskWu3WACeAWRaLPfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFYJfr%2FbtsQtbJm2X1%2FuvJKBskWu3WACeAWRaLPfK%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;1205&quot; height=&quot;43&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;43&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;h4 data-ke-size=&quot;size20&quot;&gt;2.2.2. @Around : 메소드 호출 전 / 후 / 예외상황 로깅&lt;/h4&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package com.syun.posleep.aspects;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class ApiLoggingAspect {

    @Pointcut(&quot;execution(* com.syun.posleep.web..*(..))&quot;)
    public void controller() {
    }

    @Around(&quot;controller()&quot;)
    public Object logAroundTest(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;Controller Before - Around&quot;);       // 메소드 호출 전
        try {
            Object result = joinPoint.proceed();
            log.info(&quot;Controller Success - Around&quot;);  // 메소드 호출 후(성공)
            return result;
        } catch (Throwable e) {
            log.info(&quot;!! Controller Fail - Around&quot;);  // 메소드 호출 후(예외발생)
            throw e;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Proceeding JoinPoint : JoinPoint를 상속 받아 추가 기능을 제공
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Around에서만 지원&lt;/li&gt;
&lt;li&gt;proceed() 메소드를 통해 target 실행제어가 가능 (반드시 호출해야 타겟이 실행 가능)&lt;/li&gt;
&lt;li&gt;반드시 Object를 리턴
&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;void를 리턴타입으로 할 경우 : 타겟의 응답이 사라져 API가 동작하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GgFgl/btsQr2foPwt/eZhFt8neTetuqY5RJ8aJMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GgFgl/btsQr2foPwt/eZhFt8neTetuqY5RJ8aJMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GgFgl/btsQr2foPwt/eZhFt8neTetuqY5RJ8aJMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGgFgl%2FbtsQr2foPwt%2FeZhFt8neTetuqY5RJ8aJMk%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;1204&quot; height=&quot;45&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;45&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;&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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  References&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://adjh54.tistory.com/133&quot;&gt;https://adjh54.tistory.com/133&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@youjung/Spring-AOP&quot;&gt;https://velog.io/@youjung/Spring-AOP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@jjeongdong/Spring-AOP를-활용한-Logging-구현&quot;&gt;https://velog.io/@jjeongdong/Spring-AOP를-활용한-Logging-구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ivory-room.tistory.com/86&quot;&gt;https://ivory-room.tistory.com/86&lt;/a&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;&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;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;</description>
      <category>Spring</category>
      <author>s_ih_yun</author>
      <guid isPermaLink="true">https://codesyun.tistory.com/371</guid>
      <comments>https://codesyun.tistory.com/entry/Spring-Spring-AOP%EB%A1%9C-Log-%EA%B8%B0%EB%A1%9D%ED%95%98%EA%B8%B0-Around-Before-After#entry371comment</comments>
      <pubDate>Tue, 9 Sep 2025 23:46:21 +0900</pubDate>
    </item>
  </channel>
</rss>