<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩구르르르</title>
    <link>https://henniee.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 05:21:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>해니01_15</managingEditor>
    <image>
      <title>코딩구르르르</title>
      <url>https://tistory1.daumcdn.net/tistory/6097331/attach/6a4d5f29280b4e76bd115214d149a4d8</url>
      <link>https://henniee.tistory.com</link>
    </image>
    <item>
      <title>flutter Unresolved reference 에러 해결</title>
      <link>https://henniee.tistory.com/470</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;pre id=&quot;code_1760422571389&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Running Gradle task 'assembleDebug'...
e: file:///C:/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt:764:21 Unresolved reference: filePermissions
e: file:///C:/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt:765:25 Unresolved reference: user
e: file:///C:/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt:766:29 Unresolved reference: read
e: file:///C:/flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt:767:29 Unresolved reference: write

FAILURE: Build failed with an exception.&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;해당 경로에 들어가서보니까 저 파일은 잘 있는데 Unresolved reference 라니...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것저것 검색 해보니 많은 사람들이 gradle 버전이 안 맞아서 발생하는 오류라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;그래서 android/gradle/wrapper/gradle-wrapper.properties 에 들어가서 distributionUrl의 gradle 버전을 바꾸어 주었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;플러터 최신 버전에는 8.x 가 맞고 나는 7.x를 쓰고 있었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pQYHr/btsQ8zKB3kK/mw8Kp9ZaeYxeCjRakG7oUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pQYHr/btsQ8zKB3kK/mw8Kp9ZaeYxeCjRakG7oUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pQYHr/btsQ8zKB3kK/mw8Kp9ZaeYxeCjRakG7oUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpQYHr%2FbtsQ8zKB3kK%2Fmw8Kp9ZaeYxeCjRakG7oUK%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;1495&quot; height=&quot;309&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;309&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;위와 같이 버전을 변경 했다면 clean과 pub get을 한번 해준 뒤 실행 해준다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760422855371&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flutter clean
./gradlew clean
flutter pub get&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;</description>
      <category>Flutter</category>
      <category>FlutterPlugin</category>
      <category>FlutterPlugin 오류</category>
      <category>FlutterPlugin.kt</category>
      <category>gradle버전오류</category>
      <category>Unresolved reference</category>
      <category>플러터gradle 버전</category>
      <category>플러터실행오류</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/470</guid>
      <comments>https://henniee.tistory.com/470#entry470comment</comments>
      <pubDate>Tue, 14 Oct 2025 15:22:39 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Gradle 프로젝트에서 SMTP 이메일 전송하기</title>
      <link>https://henniee.tistory.com/469</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 이메일 발송 기능을 정리해둔 글이 있지만, 조금 중구난방으로 작성된 부분이 있어서 이번에 Gradle 기반으로 다시 정리해 보려고 한다.&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;&lt;b&gt;[발송용 이메일 계정 준비]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 이메일을 발송에 사용할 이메일 계정이 있어야한다. 이 기능을 위해 새로운 이메일을 만들었고 2단계 인증까지 해주었다. 아래의 글을 따라 인증 후 비밀번호까지 생성해 보자.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1756211672435&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;[SMTP] Gmail을 이용한 초 간단 이메일 인증 구현 - 비밀번호 발급&quot; data-og-description=&quot;우리가 회원가입을 할 때 자주 마주하게 되는 이메일 인증! 가끔 가짜 이메일로 가입 하려고 하면 꼭 이메일 인증을 하라고 해서 귀찮게만 느껴졌는데 이걸 내가 하고 있다니~~~ 아무튼! 메일을 &quot; data-og-host=&quot;henniee.tistory.com&quot; data-og-source-url=&quot;https://henniee.tistory.com/216&quot; data-og-url=&quot;https://henniee.tistory.com/216&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LrSZc/hyZC4hDT3q/58kN5QRhzMX82KXMK7mgL1/img.png?width=800&amp;amp;height=681&amp;amp;face=0_0_800_681,https://scrap.kakaocdn.net/dn/c6kbkG/hyZDavoIX4/YyGDWCibPiHVDjvp1eaoY1/img.png?width=800&amp;amp;height=681&amp;amp;face=0_0_800_681,https://scrap.kakaocdn.net/dn/b2gqI6/hyZC49MBNT/YFi7FZgkRukqSJTVlCSqh1/img.png?width=1387&amp;amp;height=1181&amp;amp;face=0_0_1387_1181&quot;&gt;&lt;a href=&quot;https://henniee.tistory.com/216&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://henniee.tistory.com/216&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LrSZc/hyZC4hDT3q/58kN5QRhzMX82KXMK7mgL1/img.png?width=800&amp;amp;height=681&amp;amp;face=0_0_800_681,https://scrap.kakaocdn.net/dn/c6kbkG/hyZDavoIX4/YyGDWCibPiHVDjvp1eaoY1/img.png?width=800&amp;amp;height=681&amp;amp;face=0_0_800_681,https://scrap.kakaocdn.net/dn/b2gqI6/hyZC49MBNT/YFi7FZgkRukqSJTVlCSqh1/img.png?width=1387&amp;amp;height=1181&amp;amp;face=0_0_1387_1181');&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;[SMTP] Gmail을 이용한 초 간단 이메일 인증 구현 - 비밀번호 발급&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;우리가 회원가입을 할 때 자주 마주하게 되는 이메일 인증! 가끔 가짜 이메일로 가입 하려고 하면 꼭 이메일 인증을 하라고 해서 귀찮게만 느껴졌는데 이걸 내가 하고 있다니~~~ 아무튼! 메일을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;henniee.tistory.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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[환경설정 및 빌드파일 설정]&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties&lt;/p&gt;
&lt;pre id=&quot;code_1756211756008&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=가입한이메일@gmail.com
spring.mail.password=발급비밀번호
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1756211827269&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-mail'
    }&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;&lt;b&gt;[이메일 서비스 만들기]&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send 매서드를 정의해서 메일을 보낼 수 있게 구현한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756211951512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class EmailService {
    private static final Logger log = LoggerFactory.getLogger(EmailService.class); // 로거 추가

    private final JavaMailSender mailSender;

    @Value(&quot;${spring.mail.username}&quot;)
    private String from;

    public EmailService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

   
    public void send(String subject, String body, String... to) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to); 
        message.setFrom(from);
        message.setSubject(subject);
        message.setText(body);
        mailSender.send(message);
        
        log.info(&quot;메일 전송 완료: 제목={}, 수신자={}&quot;, subject, String.join(&quot;, &quot;, to));

    }
    

}&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;&lt;b&gt;[메일 보내기]&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1756212091479&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final EmailService emailService;

emailService.send(&quot;OS 업데이트 알림&quot;, &quot;내용&quot;, &quot;메일 받을 주소1&quot;, &quot;메일받을 주소2&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>spring 스프링</category>
      <category>SMTP</category>
      <category>smtp이메일보내기</category>
      <category>스프링부트</category>
      <category>자바smtp</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/469</guid>
      <comments>https://henniee.tistory.com/469#entry469comment</comments>
      <pubDate>Thu, 28 Aug 2025 22:49:27 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 업데이트 자동 확인: 웹 크롤링으로 이메일 알림 만들기</title>
      <link>https://henniee.tistory.com/468</link>
      <description>&lt;figure id=&quot;og_1756128067516&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;iOS 업데이트 자동 확인: 웹 크롤링으로 이메일 알림 만들기&quot; data-og-description=&quot;잦은 OS 업데이트로 매일매일 홈페이지에 들어가서 확인 하기가 번거로워서 웹 크롤링을 통해 이메일로 해당 업데이트 내용을 전달해주는 프로그램을 만들었다. 웹 크롤링 툴에는 크게 두가지&quot; data-og-host=&quot;henniee.tistory.com&quot; data-og-source-url=&quot;https://henniee.tistory.com/467&quot; data-og-url=&quot;https://henniee.tistory.com/467&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/IYjpQ/hyZDZT1pqw/1gKpfCwYjOno37ikHA7751/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bPB7IY/hyZCX3Dvaa/Zyekvc3HnuIyRiL2Dakvr1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bgW5Pv/hyZDP4V52V/2QVkarwajVnJrqsVhqErk0/img.png?width=2333&amp;amp;height=751&amp;amp;face=0_0_2333_751&quot;&gt;&lt;a href=&quot;https://henniee.tistory.com/467&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://henniee.tistory.com/467&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/IYjpQ/hyZDZT1pqw/1gKpfCwYjOno37ikHA7751/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bPB7IY/hyZCX3Dvaa/Zyekvc3HnuIyRiL2Dakvr1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bgW5Pv/hyZDP4V52V/2QVkarwajVnJrqsVhqErk0/img.png?width=2333&amp;amp;height=751&amp;amp;face=0_0_2333_751');&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;iOS 업데이트 자동 확인: 웹 크롤링으로 이메일 알림 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;잦은 OS 업데이트로 매일매일 홈페이지에 들어가서 확인 하기가 번거로워서 웹 크롤링을 통해 이메일로 해당 업데이트 내용을 전달해주는 프로그램을 만들었다. 웹 크롤링 툴에는 크게 두가지&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;henniee.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 업데이트 여부를 확인하는 크롤링을 구현 했었다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 안드로이드 를 확인 하는 크롤링을 해보려고 한다. \&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[크롤링&amp;nbsp; 대상 사이트 확인]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #9d9d9d; color: #ffffff;&quot;&gt;&amp;nbsp;https://developer.android.com/about/versions/16/release-notes&amp;nbsp;&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;URL 링크를 살펴보면 규칙이 보인다.&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;versions/16 -&amp;gt; 최신 버전번호가 들어감&lt;/li&gt;
&lt;li&gt;release-notes -&amp;gt; 릴리즈 노트 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;즉, 최신 버전 번호만 알 수 있으면 최종 릴리즈노트 링크를 만들 수 있다.&amp;nbsp;&amp;nbsp;&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;size18&quot;&gt;&lt;b&gt;[최신 버전 번호 가져오기]&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 vserions 까지의 주소에 접근 한 후 h3을 찾아 그 안의 img 태그의 alt 를 찾아준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 alt의 끝부분 (숫자) 만 떼서 다시 versions/버전숫자/release-notes&amp;nbsp; 라는 주소로 진입한다.&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;1764&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qkrGB/btsP4woARyV/0k7j3crckkhXJfG4GAOKdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qkrGB/btsP4woARyV/0k7j3crckkhXJfG4GAOKdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qkrGB/btsP4woARyV/0k7j3crckkhXJfG4GAOKdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqkrGB%2FbtsP4woARyV%2F0k7j3crckkhXJfG4GAOKdk%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;1764&quot; height=&quot;1012&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;1012&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;size18&quot;&gt;&lt;b&gt;[릴리즈노트 페이지 진입]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 버전을 알았다면 릴리즈 노트 주소를 만들 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 릴리즈페이지는 앞전의 페이지와 다르게 내부의 탭들이 JavaScript 로 랜더링 되기 때문에 DOM이 모두 그려질 때까지 기다려야 한다.&amp;nbsp;그래서 이 부분에서는 Selenium을 사용하여 원하는 정보를 얻어낸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1833&quot; data-origin-height=&quot;815&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btOwlq/btsP8KZZc5g/jhlWI4XmGDIeVkHk0d0pj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btOwlq/btsP8KZZc5g/jhlWI4XmGDIeVkHk0d0pj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btOwlq/btsP8KZZc5g/jhlWI4XmGDIeVkHk0d0pj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtOwlq%2FbtsP8KZZc5g%2FjhlWI4XmGDIeVkHk0d0pj0%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;1833&quot; height=&quot;815&quot; data-origin-width=&quot;1833&quot; data-origin-height=&quot;815&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;size18&quot;&gt;&lt;b&gt;최종 코드&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756210297646&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private List&amp;lt;AndroidVersionInfo&amp;gt; crawlAndroidWebsite() throws IOException {
		List&amp;lt;AndroidVersionInfo&amp;gt; latestAndroidVersions = new ArrayList&amp;lt;&amp;gt;();
		WebDriver driver = null;
		try {
			// 페이지 접속
			Document doc = Jsoup.connect(Android_RELEASES_URL).get();

			// h3 태그 중 img alt 속성이 &quot;Android&quot;로 시작하는 것 찾기
			Elements h3s = doc.select(&quot;h3:has(img[alt^=Android])&quot;);

			String latestVersionLink = null; // href=&quot;https://developer.android.com/about/versions/16?hl=ko&quot;
			String latestVersionNum = null; // 16

			for (Element h3 : h3s) {
				Element aTag = h3.selectFirst(&quot;a[href]&quot;);
				if (aTag != null) {
					latestVersionLink = aTag.attr(&quot;href&quot;);
					Matcher m = Pattern.compile(&quot;/versions/(\\d+)&quot;).matcher(latestVersionLink);
					if (m.find()) {
						latestVersionNum = m.group(1); // &quot;16&quot;
					}

					break; // 첫 번째(최신)만 추출
				}
			}

			if (latestVersionLink != null) {

				String baseUrl = latestVersionLink + &quot;/release-notes&quot;;

				Document versionDoc = Jsoup.connect(baseUrl).get(); // 릴리즈노트 사이트로 들어가기

				WebDriverManager.chromedriver().setup();
				ChromeOptions options = new ChromeOptions();
				options.addArguments(&quot;--headless=new&quot;); 
				options.addArguments(&quot;--disable-gpu&quot;); 

//				options.addArguments(&quot;--headless=new&quot;); //창 없이 백그라운드 실행 
//				options.addArguments(&quot;--disable-gpu&quot;); // GPU 가속 끄기 
//				options.addArguments(&quot;--no-sandbox&quot;);//샌드박스 비활성화 (서버 환경에서 필요)
//				options.addArguments(&quot;--disable-dev-shm-usage&quot;); //docker에서 크롬 크래시 방지 
//				options.addArguments(&quot;--window-size=1920,1080&quot;); //창 크기 설정 

				driver = new ChromeDriver(options);
				driver.get(baseUrl);

				List&amp;lt;WebElement&amp;gt; tabs = driver.findElements(By.cssSelector(&quot;tab[role=tab] a&quot;));

				if (tabs.isEmpty()) { // 탭이 없을 경우 &amp;rarr; 탭 정보 없이 저장
					latestAndroidVersions.add(new AndroidVersionInfo(latestVersionNum, null, baseUrl));
					System.out.println(&quot;탭없음&quot;);
				} else {
					String latestTab = null;
					double maxVersion = 0.0;

					for (WebElement tab : tabs) {
						String text = tab.getText().trim(); // 예: &quot;베타 4.1&quot;
						if (text.matches(&quot;.*\\d+(\\.\\d+)?&quot;)) {
							double version = Double.parseDouble(text.replaceAll(&quot;[^\\d.]&quot;, &quot;&quot;));
							if (version &amp;gt; maxVersion) {
								maxVersion = version;
								latestTab = latestVersionNum + &quot; &quot; + text;
							}
						}
					}

					if (latestTab != null) {
						latestAndroidVersions.add(new AndroidVersionInfo(latestTab, null, baseUrl));
					} else {
						System.out.println(&quot;최신 업데이트 버전이 없음&quot;);
					}
				}
			} else {
				System.out.println(&quot;링크를 찾을 수 없습니다.&quot;);
			}
		} catch (IOException e) {
			System.out.println(&quot;크롤링 오류: &quot; + e.getMessage());
		} 
		return latestAndroidVersions;
	}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>spring 스프링</category>
      <category>aos업데이트</category>
      <category>os업데이트알림</category>
      <category>SMTP</category>
      <category>개발자사이트</category>
      <category>안드로이드업데이트</category>
      <category>안드로이드업데이트알림</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/468</guid>
      <comments>https://henniee.tistory.com/468#entry468comment</comments>
      <pubDate>Tue, 26 Aug 2025 21:29:24 +0900</pubDate>
    </item>
    <item>
      <title>iOS 업데이트 자동 확인: 웹 크롤링으로 이메일 알림 만들기</title>
      <link>https://henniee.tistory.com/467</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;잦은 OS 업데이트로 매일매일 홈페이지에 들어가서 확인 하기가 번거로워서 웹 크롤링을 통해 이메일로 해당 업데이트 내용을 전달해주는 프로그램을 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEhQp/btsP112ia0t/hJCKQgsMqaXgryKFhakV01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEhQp/btsP112ia0t/hJCKQgsMqaXgryKFhakV01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEhQp/btsP112ia0t/hJCKQgsMqaXgryKFhakV01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEhQp%2FbtsP112ia0t%2FhJCKQgsMqaXgryKFhakV01%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;559&quot; height=&quot;241&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;355&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;웹 크롤링 툴에는 크게 두가지가 있는데, Jsoup과 selenium 이고 둘의 간단한 사용법 이나 차이는 아래의 글에 정리 해 놓았다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1755695284006&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;웹 크롤링 비교: Jsoup vs Selenium, 속도&amp;middot;성능&amp;middot;활용 차이 정리&quot; data-og-description=&quot;웹 크롤링을 할 때 가장 많이 쓰이는 것들이 있다. 웹 스크래핑 라이브러리 Jsoup과 브라우저 자동화 프레임워크 Selenium이다. 오늘은 이 두개를 비교해서 어떨 때 사용 해야 하는지 정리했다. Jsoup&quot; data-og-host=&quot;henniee.tistory.com&quot; data-og-source-url=&quot;https://henniee.tistory.com/466&quot; data-og-url=&quot;https://henniee.tistory.com/466&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ORrsp/hyZylRF4Cb/95bTnqz1ntCY4IJSVjPU4K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/lcGB4/hyZC6yxifE/P4KmKmks6CXFwtXwTJYLc1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://henniee.tistory.com/466&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://henniee.tistory.com/466&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ORrsp/hyZylRF4Cb/95bTnqz1ntCY4IJSVjPU4K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/lcGB4/hyZC6yxifE/P4KmKmks6CXFwtXwTJYLc1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;웹 크롤링 비교: Jsoup vs Selenium, 속도&amp;middot;성능&amp;middot;활용 차이 정리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;웹 크롤링을 할 때 가장 많이 쓰이는 것들이 있다. 웹 스크래핑 라이브러리 Jsoup과 브라우저 자동화 프레임워크 Selenium이다. 오늘은 이 두개를 비교해서 어떨 때 사용 해야 하는지 정리했다. Jsoup&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;henniee.tistory.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;웹 크롤링을 시작하기 전에는 어떤 정보를 어디서 가져올지 정리해야 한다. 나는 iOS 업데이트 정보만 확인하면 되었기 때문에 Apple Developer 사이트 구조를 간단히 파악해 준다.&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;2333&quot; data-origin-height=&quot;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYvhwg/btsP02zBnb0/x7q8NjSnkUXKHOsoqk5230/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYvhwg/btsP02zBnb0/x7q8NjSnkUXKHOsoqk5230/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYvhwg/btsP02zBnb0/x7q8NjSnkUXKHOsoqk5230/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYvhwg%2FbtsP02zBnb0%2Fx7q8NjSnkUXKHOsoqk5230%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;2333&quot; height=&quot;751&quot; data-origin-width=&quot;2333&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지를 살펴보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) section 태그 안에 각각의 업데이트 정보가 들어 있고 &lt;br /&gt;2) 그 안의 h2 태그에서 버전명(iOS 18.x 등) 을 가져온다. &lt;br /&gt;3) 바로 아래의 p 태그에서는 배포 날짜를 가져온다. &lt;br /&gt;4) 맨 마지막에는 릴리즈 노트 링크가 있다. &lt;br /&gt;&lt;br /&gt;즉,&amp;nbsp;크롤링&amp;nbsp;로직은&amp;nbsp;버전&amp;nbsp;&amp;rarr;&amp;nbsp;날짜&amp;nbsp;&amp;rarr;&amp;nbsp;릴리즈&amp;nbsp;노트&amp;nbsp;순서대로&amp;nbsp;추출하면&amp;nbsp;된다.&lt;/p&gt;
&lt;pre id=&quot;code_1755784460704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//사이트 연결하기 
Document doc = Jsoup.connect(APPLE_RELEASES_URL).get();

// 클래스가 .article-content-container 인 selection 요소들을 뽑아준다. 
Elements sections = doc.select(&quot;section.article-content-container&quot;);

List&amp;lt;IosVersionInfo&amp;gt; latestVersions = new ArrayList&amp;lt;&amp;gt;(); //값을 담을 리스트 만들기 
LocalDate latestDate = null; // 날짜 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;MMMM d, yyyy&quot;, Locale.ENGLISH); //날짜 포메팅 

for (Element section : sections) { //selection 뽑아 놓은 요소들을 돌면서 원하는 값들을 매칭 시켜준다
	Element h2 = section.selectFirst(&quot;h2:containsOwn(iOS)&quot;); //iOS 를 가진 버전 이름 
	if (h2 != null) {
	String version = h2.text();

//날짜세팅 
	Element dateElement = section.selectFirst(&quot;p.article-date&quot;); 
		if (dateElement == null)
			continue;

			String dateText = dateElement.text();
			LocalDate currentDate;
			try {
				currentDate = LocalDate.parse(dateText, formatter);
				} catch (DateTimeParseException e) {
					continue;
				}

				// 릴리즈 노트 &amp;lt;a&amp;gt; 태그 추출
			Element releaseNoteLink = section.selectFirst(&quot;a.more:containsOwn(View release notes)&quot;);
			String releaseNoteUrl = releaseNoteLink != null
			? &quot;https://developer.apple.com&quot; + releaseNoteLink.attr(&quot;href&quot;)
			: null;
            
     }
		}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>spring 스프링</category>
      <category>IOS업데이트</category>
      <category>jsoup</category>
      <category>selenium</category>
      <category>웹크롤링</category>
      <category>이메일알림</category>
      <category>크롤링 자동화</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/467</guid>
      <comments>https://henniee.tistory.com/467#entry467comment</comments>
      <pubDate>Thu, 21 Aug 2025 23:08:55 +0900</pubDate>
    </item>
    <item>
      <title>웹 크롤링 비교: Jsoup vs Selenium, 속도&amp;middot;성능&amp;middot;활용 차이 정리</title>
      <link>https://henniee.tistory.com/466</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;웹 크롤링을 할 때 가장 많이 쓰이는 것들이 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;웹 스크래핑 라이브러리 Jsoup과 브라우저 자동화 프레임워크 Selenium이다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;오늘은 이 두개를 비교해서 어떨 때 사용 해야 하는지 정리했다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;95&quot; data-start=&quot;80&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Jsoup&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;438&quot; data-start=&quot;96&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;118&quot; data-start=&quot;96&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주 용도: HTML 문서 파싱&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;199&quot; data-start=&quot;119&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동작 방식:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;199&quot; data-start=&quot;136&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;174&quot; data-start=&quot;136&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HTTP 요청 &amp;rarr; HTML 응답 &amp;rarr; HTML 파싱 (DOM 탐색)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;199&quot; data-start=&quot;177&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버에서 받은 정적인 HTML만 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;322&quot; data-start=&quot;200&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특징:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;322&quot; data-start=&quot;212&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;228&quot; data-start=&quot;212&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;빠름 (브라우저 실행 X)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;248&quot; data-start=&quot;231&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;설치 간단 (JAR 추가만)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;322&quot; data-start=&quot;251&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;단점: 자바스크립트로 그려지는 동적 데이터는 못 봄&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(예: React, Vue, AJAX로 불러오는 내용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;438&quot; data-start=&quot;323&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예시&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1755084208300&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Document doc = Jsoup.connect(&quot;https://example.com&quot;).get(); String title = doc.title();&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-end=&quot;463&quot; data-start=&quot;445&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Selenium&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;875&quot; data-start=&quot;464&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;497&quot; data-start=&quot;464&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주 용도: 브라우저 자동화 + 동적 페이지 크롤링&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;582&quot; data-start=&quot;498&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동작 방식:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;582&quot; data-start=&quot;513&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;563&quot; data-start=&quot;513&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실제 브라우저(Chrome, Firefox 등) 구동 &amp;rarr; 페이지 렌더링 &amp;rarr; DOM 탐색&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;582&quot; data-start=&quot;566&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JS 실행 결과까지 가져옴&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;713&quot; data-start=&quot;583&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;특징:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;713&quot; data-start=&quot;595&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;622&quot; data-start=&quot;595&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자바스크립트 렌더링된 데이터 접근 가능&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;654&quot; data-start=&quot;625&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;버튼 클릭, 스크롤, 로그인 같은 UI 동작 가능&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;684&quot; data-start=&quot;657&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;단점: 느림 (브라우저 띄우고 렌더링해야 함)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;713&quot; data-start=&quot;687&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;환경 세팅이 복잡 (WebDriver 필요)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;875&quot; data-start=&quot;714&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예시&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1755084499018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WebDriver driver = new ChromeDriver(); 
driver.get(&quot;https://example.com&quot;); 
WebElement title = driver.findElement(By.tagName(&quot;h1&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IT개념</category>
      <category>jsoup</category>
      <category>selenium</category>
      <category>스프링웹크롤링</category>
      <category>웹크롤링예제</category>
      <category>웹크롤링하기</category>
      <category>크롤링예제</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/466</guid>
      <comments>https://henniee.tistory.com/466#entry466comment</comments>
      <pubDate>Wed, 13 Aug 2025 20:34:53 +0900</pubDate>
    </item>
    <item>
      <title>synology NAS 로 도커에 스프링 프로젝트 배포하기</title>
      <link>https://henniee.tistory.com/465</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;완성 된 스프링 프로젝트를 빌드 해준다. 프로젝트/build/lib 안에 빌드 된 파일이 있을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754722106742&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./gradlew clean 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;이걸 synologyDrive 에 옮겨 준다.&amp;nbsp; 실행 시 필요한 파일도 같이 업로드 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbVNtP/btsPOFRT1Jv/oHxUZygCmZaibK9vM0035k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbVNtP/btsPOFRT1Jv/oHxUZygCmZaibK9vM0035k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbVNtP/btsPOFRT1Jv/oHxUZygCmZaibK9vM0035k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbVNtP%2FbtsPOFRT1Jv%2FoHxUZygCmZaibK9vM0035k%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;379&quot; height=&quot;361&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;361&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;pre id=&quot;code_1754722321900&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pMVDF/btsPOC8JXiT/LIFuQEpaakUUV62M7MKfdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pMVDF/btsPOC8JXiT/LIFuQEpaakUUV62M7MKfdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pMVDF/btsPOC8JXiT/LIFuQEpaakUUV62M7MKfdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpMVDF%2FbtsPOC8JXiT%2FLIFuQEpaakUUV62M7MKfdk%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;1244&quot; height=&quot;132&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;132&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;터미널을 껐다 켜도 로그를 볼 수 있도록 백그라운 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;nohup (no hangup 의 줄임말)&lt;span&gt; &lt;/span&gt;&lt;/span&gt;실행으로 실행 시켜 줄 것이다. 그리고 자바 프로그램에 파일위치를 커맨드라인 옵션으로 넘겨 줘서 코드 실행 시 확인 해볼 수 있게 할 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754722405552&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nohup java -jar os_update_check-0.0.1-SNAPSHOT.jar --version.file.path=/app/version-store.json &amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 98px;&quot; border=&quot;1&quot; data-end=&quot;1155&quot; data-start=&quot;642&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;명령어&lt;/td&gt;
&lt;td&gt;&amp;nbsp;부분의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;911&quot; data-start=&quot;830&quot;&gt;
&lt;td style=&quot;height: 22px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;862&quot; data-start=&quot;830&quot;&gt;nohup&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot; data-end=&quot;911&quot; data-start=&quot;862&quot; data-col-size=&quot;sm&quot;&gt;터미널 종료해도 프로세스 계속 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;999&quot; data-start=&quot;912&quot;&gt;
&lt;td style=&quot;height: 22px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;945&quot; data-start=&quot;912&quot;&gt;java -jar os_update_check...&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot; data-end=&quot;999&quot; data-start=&quot;945&quot; data-col-size=&quot;sm&quot;&gt;자바 애플리케이션 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1077&quot; data-start=&quot;1000&quot;&gt;
&lt;td style=&quot;height: 22px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1032&quot; data-start=&quot;1000&quot;&gt;--version.file.path=...&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot; data-end=&quot;1077&quot; data-start=&quot;1032&quot; data-col-size=&quot;sm&quot;&gt;프로그램에 전달하는 설정 또는 파일 경로 옵션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1155&quot; data-start=&quot;1078&quot;&gt;
&lt;td style=&quot;height: 22px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1110&quot; data-start=&quot;1078&quot;&gt;&amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot; data-end=&quot;1155&quot; data-start=&quot;1110&quot; data-col-size=&quot;sm&quot;&gt;표준 출력과 표준 에러를 app.log 파일에 기록하며 백그라운드 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드로 실행 시킨 로그를 보고 싶으면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754722673314&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat app.log&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;기존 프로세스를 멈추고 싶다면&lt;/p&gt;
&lt;pre id=&quot;code_1754722702653&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kill 307&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;실행 중인 파일이 궁금하다면&lt;/p&gt;
&lt;pre id=&quot;code_1754722725882&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jobs -l&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>서버</category>
      <category>NAS</category>
      <category>synology</category>
      <category>Synology NAS</category>
      <category>나스</category>
      <category>백그라운드서버실행</category>
      <category>시놀로지나스</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/465</guid>
      <comments>https://henniee.tistory.com/465#entry465comment</comments>
      <pubDate>Sun, 10 Aug 2025 13:59:17 +0900</pubDate>
    </item>
    <item>
      <title>2025년 반기 회고</title>
      <link>https://henniee.tistory.com/464</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://henniee.tistory.com/441&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025년 신년 계획글&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753103815954&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;2024년 회고 및 2025년 계획 발표&quot; data-og-description=&quot;제목이 다소 거창하고 뒤늦은 회고 같지만 그럴만한 타당한 이유가 있는 회고 글이기도 하다. 2024년 연말은 뒤숭숭하고 미래를 예측할 수 없어 한 없이 의욕을 잃게 되는 그런 시기였다. 그래서 &quot; data-og-host=&quot;henniee.tistory.com&quot; data-og-source-url=&quot;https://henniee.tistory.com/441&quot; data-og-url=&quot;https://henniee.tistory.com/441&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ktaiv/hyZncfzt4B/8mdYUpOhk11GBkqBjP2FFk/img.png?width=800&amp;amp;height=784&amp;amp;face=0_0_800_784,https://scrap.kakaocdn.net/dn/biV05E/hyZm9Xsqlq/4GHDYQ6fkyAeFHaOkRQ0I0/img.png?width=800&amp;amp;height=784&amp;amp;face=0_0_800_784,https://scrap.kakaocdn.net/dn/dQxYMi/hyZndyPmO8/AmjKVdoG2kV9kSQmUf9fTK/img.png?width=1240&amp;amp;height=1216&amp;amp;face=0_0_1240_1216&quot;&gt;&lt;a href=&quot;https://henniee.tistory.com/441&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://henniee.tistory.com/441&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ktaiv/hyZncfzt4B/8mdYUpOhk11GBkqBjP2FFk/img.png?width=800&amp;amp;height=784&amp;amp;face=0_0_800_784,https://scrap.kakaocdn.net/dn/biV05E/hyZm9Xsqlq/4GHDYQ6fkyAeFHaOkRQ0I0/img.png?width=800&amp;amp;height=784&amp;amp;face=0_0_800_784,https://scrap.kakaocdn.net/dn/dQxYMi/hyZndyPmO8/AmjKVdoG2kV9kSQmUf9fTK/img.png?width=1240&amp;amp;height=1216&amp;amp;face=0_0_1240_1216');&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;2024년 회고 및 2025년 계획 발표&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;제목이 다소 거창하고 뒤늦은 회고 같지만 그럴만한 타당한 이유가 있는 회고 글이기도 하다. 2024년 연말은 뒤숭숭하고 미래를 예측할 수 없어 한 없이 의욕을 잃게 되는 그런 시기였다. 그래서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;henniee.tistory.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;&lt;span style=&quot;color: #333333;&quot;&gt;눈깜짝할 사이 벌써 6개월이라는 시간이 흘렀다. 아니 사실은 7개월.. 아무튼 6개월간의 다이어리를 쭉 보고 그동안 있었던 일을 나열하니 지피티는 이렇게 나의 반기를 정의했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt; 일도 하고 놀기도 하고 관리하는 만능 인간의 6개월 &lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;맞지 맞지 .. 만능 인간이 되기 위해 지난 6개월동안 했던 일을 되돌아 보자면 이렇다.&amp;nbsp;&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;WWCKorea행사&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;249&quot; data-start=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3월 WWCKorea 데모데이에 운영진으로 함께 참여했다. 직접 기획을 주도한 행사는 아니었지만, 다른 운영진 분들과 협업하며 어떻게 하면 보다 편안한 소통 환경을 만들 수 있을지, 상대방의 기분을 상하지 않게 거절하는 방법은 무엇일지, 그리고 예기치 못한 상황에서 우선순위를 정해 문제를 해결하는 방식 등 다양한 경험과 배움을 얻을 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;380&quot; data-start=&quot;251&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;또한, 연사자들의 이야기를 들으며 새로운 인사이트를 얻었고, 앞으로 하고 싶은 일들에 대한 동기 또한 새롭게 다질 수 있었다. 특히 처음 개발자가 되었을 때의 열정과 목표를 다시금 떠올리게 해준, 의미 깊은 시간이었다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;여행&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;빠질 수 없는 테마, 여행.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;올해 반기동안에는 홍콩, 싱가폴, 발리 아시아권 여행을 다녔다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;홍콩에서는 최악의 습도를 만났지만 덕분에 낭만적인 뷰를 보게 되는 운 좋은 일이 있었다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;싱가폴에 사는 친구를 보러 갔다가 정말 좋은 호텔에서도 묵어보게 되었고&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;발리에서는 드디어 진짜로 물 공포증을 이겨 내었다. (수영을 일년 했지만 물은 여전히 무서웠었음) &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;낯선 곳에 발을 디딜 때마다, 익숙한 일상 속에선 보지 못했던 내 모습을 마주하게 된다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여행은 늘 그렇게, 나를 조금 더 알아가는 계기가 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;사이드프로젝트&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;친구들과 사이드프로젝트를 진행 했다.&amp;nbsp; 혹시 궁금하다면 여기 &lt;a style=&quot;color: #333333;&quot; href=&quot;https://github.com/LeadingLog/reading-log&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;리딩로그&lt;/span&gt;&amp;nbsp;&lt;/a&gt;를 클릭해서 한번 보시면 좋겠습니다~&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;간단하게 말하면 독서기록 관리&amp;nbsp;&lt;/span&gt;서비스인데,&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;사실 요즘 이런&amp;nbsp;&lt;/span&gt;앱이&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;많이 나오기도 했고, 또 책은 잘&amp;nbsp;&lt;/span&gt;안 읽는&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;편이라 막막했지만 이번에는 처음&amp;nbsp;&lt;/span&gt;도전해 보는&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;서버와 오랜만에&amp;nbsp;&lt;/span&gt;백 앤드&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;개발을 맡아서 시간&amp;nbsp;&lt;/span&gt;가는 줄&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt; 모르고 재밌게 했던 프로젝트였다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;사실 AI 서비스를 접목시켜서 조금 더 체계적인 관리가 가능하게&amp;nbsp;&lt;/span&gt;디벨롭도&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;해보고 싶고,&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;플러터로&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;웹앱 변환을 해서 스토어에&amp;nbsp;&lt;/span&gt;출시해 보고&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;싶었디만&amp;nbsp;&lt;/span&gt;여름 동안&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;야근을 밥 먹듯 하게 되면서 2차 개발에&amp;nbsp;&lt;/span&gt;참여할&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt; 수 없었다...&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;야근&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;하고 싶은 일을 하면 야근이&amp;nbsp;&lt;/span&gt;재밌어지는 게&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;신기하다. 분명히 다른 프로젝트 할 때는 야근 자체가 너무 싫었는데 이번 프로젝트는 야근도 나쁘지 않았고 주말 출근도 괜찮았다.&amp;nbsp;&lt;/span&gt;완성된&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;프로젝트를 사용자가 잘&amp;nbsp;&lt;/span&gt;사용해 줬으면&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt; 좋겠다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;월디페&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;애니마가 한국에 왔다니, 안 가볼 수 없었다. 가장 궁금했던 건 &amp;ldquo;한국에서, 그것도 야외에서 애니마의 몰입형 비주얼을 어떻게 구현할까?&amp;rdquo;였다. 생각보다 스크린 규모도 크고 구성도 훌륭했지만, 약간의 아쉬움도 있었다. 돔 형태로 좀 더 3D에 가까운 몰입감을 주었더라면 어땠을까 하는 생각이 남았다. 그래도 전반적으로 즐겁고 인상적인 경험이었다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;발표 도전&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 마이크로소프트 X 아이인위가 주관한 [테크톡투게더] 행사에 다녀왔다. 두 개의 발표 세션과 함께 Growlog라는 발표 세션이 있었는데, 이 자리에서 나도 발표를 맡았다. 그동안 진행했던 프로젝트들을 간략히 소개하며, &amp;ldquo;저 이렇게 바쁘고 열정적으로 살고 있습니다&amp;rdquo;를 살짝 어필하는 시간이었다. 발표를 좋게 봐주신 분들 덕분에 소셜링도 하고, 즐겁게 교류할 수 있는 뜻깊은 하루였다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;소셜링&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;앞서 마이크로소프트 행사에서 만난 분들과 따로 날짜를 잡아 커피챗을 했다. 행사 때는 나를 보며 &amp;ldquo;열심히 살아서 멋있다&amp;rdquo;라고 말씀해 주셨지만, 막상 대화를 나눠보니 나는 정말 기본만 하고 있었을 뿐이었다. 그분들은 나보다 훨씬 부지런하고, 꼼꼼하게 하루를 채워가고 있었다. 그 열정과 체력에 연신 감탄하며, 나도 더 배우고 열린 자세를 유지하는 사람이 되어야겠다고 다짐했다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;스터디&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AI 툴을 직접 써보고 싶었지만 혼자 하려니 의지가 잘 안 생겨, 아예 스터디를 열어 Cursor AI로 작은 개발 프로젝트를 진행했다. 참가자 대부분이 비전공자라 처음엔 툴 사용에 다소 어색해했지만, 하나씩 문제를 해결해가며 즐거워하는 모습을 보니 나까지 뿌듯했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;운동&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;123&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;누군가 &amp;ldquo;회사 다니면 살기 위해 운동한다&amp;rdquo;라고 했는데, 정말 공감했다. 하루 종일 앉아 있고, 삐딱한 자세로 있다 보니 근육이 사라지는 기분이 들어 올해 처음으로 헬스를 시작했다. 나름 근육도 좀 붙은 것 같고&amp;hellip;? 수영도 드디어 호흡법을 터득했다. 이제 몇 바퀴를 돌아도 숨이 차지 않는다 그러다 친구에게 1500m 대회가 있다는 얘기를 듣고, 내년에는 꼭 참가해봐야겠다고 결심했다. 벌써 너무 설레쥬ㅠ 그리고 올해는 계속 미뤄왔던 프리다이빙 자격증도 반드시 취득할 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/464</guid>
      <comments>https://henniee.tistory.com/464#entry464comment</comments>
      <pubDate>Sat, 9 Aug 2025 15:03:10 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트 두 배열에서 공통 객체 찾기 Array.prototype.some()</title>
      <link>https://henniee.tistory.com/463</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 두개의 배열이 있을 때, 두 배열에 모두 존재하는 객체를 찾아 반환해야 하는 코드를 짜야 했었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752939711582&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = [
  {name: &quot;jake&quot;, age: &quot;11&quot;, school: &quot;tes&quot;},
  {name: &quot;luna&quot;, age: &quot;13&quot;, school: &quot;tes&quot;},
  {name: &quot;leo&quot;, age: &quot;7&quot;, school: &quot;sin&quot;}
];

const b = [
  {name: &quot;jake&quot;, age: &quot;11&quot;, school: &quot;tes&quot;, add: &quot;13st&quot;},
  {name: &quot;luna&quot;, age: &quot;15&quot;, school: &quot;tes&quot;, location: &quot;downtown&quot;},
  {name: &quot;leo&quot;, age: &quot;7&quot;, school: &quot;sing&quot;, city: &quot;london&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;그래서 사용하는게 바로 &lt;span style=&quot;background-color: oklch(0.9628 0.007 106.52); color: oklch(0.3039 0.04 213.68); text-align: left;&quot;&gt;Array.prototype.some() &lt;/span&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68); text-align: left;&quot;&gt;이다.&amp;nbsp;&lt;/span&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;[some 함수]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;some()함수는 배열에서 조건을 하나라도 만족하는 요소가 있으면 true를 반환하는 고차함수이다.&amp;nbsp; 콜백 함수를 인자로 받고 이 함수가 true를 반환하는 첫번째 요소를 찾으면 즉시 멈추고 true 를 반환하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제를 보면 num =&amp;gt; num &amp;gt; 3 이라는 콜백함수를 인자로 받았다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752940115744&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4];
// 배열에 3보다 큰 값이 있나?
console.log(arr.some(num =&amp;gt; num &amp;gt; 3)); // true&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;[적용하기]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 아까 위에 있는 a 함수와 b 함수를 비교하는 코드를 보자&lt;/p&gt;
&lt;pre id=&quot;code_1752940533041&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 비교에 사용할 키 목록: 각 객체에서 'name'과 'age' 속성을 기준으로 비교함
const matchEle = [&quot;name&quot;, &quot;age&quot;]; 

// 두 객체의 특정 key들의 값이 모두 일치하는지 확인하는 함수
const allEleMatched = (obj1, obj2, keys) =&amp;gt; 
  keys.every(key =&amp;gt; obj1[key] === obj2[key]);

// 배열 a 안의 요소 중에서, 배열 b 안의 요소와 matchEle 기준으로 일치하는 것이 하나라도 있는지 확인
// 일치하는 객체가 하나라도 있으면 true 반환
const matchingObject = a.some(objA =&amp;gt;
  b.some(objB =&amp;gt; allEleMatched(objA, objB, matchEle))
);

// 배열 a 안에서, 배열 b의 어떤 객체와 matchEle 기준으로 일치하는 객체만 필터링하여 추출
// 결과는 배열 형태로 반환됨
const matches = a.filter(objA =&amp;gt;
  b.some(objB =&amp;gt; allEleMatched(objA, objB, matchEle))
);

console.log(matches); 
// [{name: &quot;jake&quot;, age: &quot;11&quot;, school: &quot;tes&quot;},
//  {name: &quot;leo&quot;, age: &quot;7&quot;, school: &quot;sin&quot;}]


//만약에 배열이 아닌 일치하는 객체의 특정 요소만 뺴오고 싶다면 
//map으로 뽑아내면 [&quot;jake&quot;, &quot;leo&quot;] 
const matchedNames = a
  .filter(objA =&amp;gt; b.some(objB =&amp;gt; allEleMatched(objA, objB, matchEle)))
  .map(obj =&amp;gt; obj.name);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Front/javascript</category>
      <category>.some()</category>
      <category>Array.prototype.some()</category>
      <category>some()</category>
      <category>배열일치</category>
      <category>자바스크립트some</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/463</guid>
      <comments>https://henniee.tistory.com/463#entry463comment</comments>
      <pubDate>Mon, 21 Jul 2025 22:13:05 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Actions에서 &amp;quot;sudo: a terminal is required&amp;quot; 오류 해결 방법 &amp;ndash; SSH 배포 자동화 시 필수 체크포인트</title>
      <link>https://henniee.tistory.com/462</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;[문제]&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lta1C/btsOYSMemkY/URPtX1PX0jm0KtB3bthWJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lta1C/btsOYSMemkY/URPtX1PX0jm0KtB3bthWJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lta1C/btsOYSMemkY/URPtX1PX0jm0KtB3bthWJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLta1C%2FbtsOYSMemkY%2FURPtX1PX0jm0KtB3bthWJk%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;1960&quot; height=&quot;182&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;CI/CD 자동화 도중 GitHub Actions의 appleboy/ssh-action을 이용해 원격 리눅스 서버에 접속하고 sudo 명령을 실행 하려고 했는데 위와 같은 오류가 발생하게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;sudo 명령어가 패스워드를 입력 받기 위한 터미널을 요구 하는데, Github Actions 환경은 터미널 세션이 없는 비대화식 환경이기 때문에 비밀번호를 입력 할 수 없어 실패한 상황과 내가 작성 해 놓은 경로에 파일이나 디렉토리를 찾을 수 없다는 오류도 나타났다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[원인]&lt;/h4&gt;
&lt;pre id=&quot;code_1751292401880&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Deploy to GCP instance
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.GCP_HOST }}
        username: ${{ secrets.GCP_USERNAME }}
        key: ${{ secrets.GCP_PRIVATEKEY }}
        port: 22
        script: |
          sudo docker login -u ${{ secrets.DOCKERHUB_ID }} -p ${{ secrets.DOCKERHUB_TOKEN }}
          sudo docker pull ${{ secrets.DOCKERHUB_ID }}/readinglog-app:latest
          cd /home/${{ secrets.GCP_USERNAME }}/readingLog
          sudo docker-compose down || true
          sudo docker-compose up -d --remove-orphans
          sudo docker image prune -f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내가 작성한 배포 gradle 을 보면 cd /home/... 이라는 구문에서 파일을 찾지 못해 비밀번호 입력에 대한 오류 역시 발생 한 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[해결]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;리눅스에서 내 계정의 홈 디렉토리 확인 방법 명령을 통해 정확한 경로를 알아냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;1. 현재 로그인한 계정명 확인&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751642637598&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;whoami&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;2. 사용자 홈 디렉토리 확인&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751642693443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo $HOME 

//결과 예시
readinglog:x:1001:1001::/home/readinglog:/bin/bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[수정]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;echo $HOME 으로 출력 된 결과를 다시 넣어 실행 해주니 오류 없이 잘 들어간 것을 확인 할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1751291554700&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: GCP에 배포
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.GCP_HOST }}
          username: ${{ secrets.GCP_USERNAME }}
          key: ${{ secrets.GCP_PRIVATEKEY }}
          port: 22
          script: |
            sudo docker login -u ${{ secrets.DOCKERHUB_ID }} -p ${{ secrets.DOCKERHUB_TOKEN }}
            sudo docker pull ${{ secrets.DOCKERHUB_ID }}/readinglog-app:latest
            cd /home/readingLog
            sudo docker-compose down || true
            sudo docker-compose up -d --remove-orphans
            sudo docker image prune -f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>git</category>
      <category>appleboy오류</category>
      <category>ci/cd 자동화</category>
      <category>GCP</category>
      <category>gitaction오류</category>
      <category>sudo</category>
      <category>깃액션오류</category>
      <category>리눅스</category>
      <category>리눅스디렉토리찾기</category>
      <category>리눅스명령어</category>
      <category>리눅스환경</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/462</guid>
      <comments>https://henniee.tistory.com/462#entry462comment</comments>
      <pubDate>Sat, 5 Jul 2025 00:27:40 +0900</pubDate>
    </item>
    <item>
      <title>Spring CORS 설정: WebConfig 작성부터 wildcard '*', withCredentials 문제 해결까지</title>
      <link>https://henniee.tistory.com/461</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;[왜 WebConfig를 작성해야 할까?]&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;프론트엔드와 백엔드가 분리된 환경에서는 각각의 도메인이 다르기 때문에 브라우저는 보안상의 이유로 자동으로 요청을 막는다. 이걸 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;CORS(Cross-Origin Resource Sharing)라고&lt;/span&gt; 한다.&amp;nbsp;&amp;nbsp;&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;&lt;span style=&quot;color: #666666;&quot;&gt;예를 들어&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;프론트앤드의 DNS 주소는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;https://project-test인데&lt;/span&gt;&amp;nbsp; 프론트가 백에게 API를 요청할 때는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;https://api-project-test&lt;/span&gt;로 넘겨 주게 된다. 이 경우, 브라우저는 서버에 직접 요청을 보내기 전에 OPTIONS 메서드로 사전요청(preflight)을 보낸다. 그런데 백엔드에서는&amp;nbsp; &quot;응답 가능해~&quot;라고 명시적으로 허용하지 않으면, 브라우저는 요청 자체를 막아버리는 것이다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;이때 필요한게 바로 WebConfig를 통한 CORS 설정이다.&amp;nbsp;&lt;/span&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;&lt;span style=&quot;color: #666666;&quot;&gt;[webConfig 작성법]&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750939883471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig {

	@Bean
	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping(&quot;/**&quot;) // 모든 경로에 대해
					.allowedOrigins(&quot;https://reading-log-zeta.vercel.app&quot;) // 프론트 도메인만 허용
					.allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;PATCH&quot;) // 허용할 메서드
					.allowCredentials(true) // 인증정보(Cookie 등) 허용
					.allowedHeaders(&quot;*&quot;) // 어떤 헤더든 허용
					.maxAge(3600); // preflight 요청 캐싱 시간 (1시간)
			}
		};
	}
}&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;&lt;span style=&quot;color: #666666;&quot;&gt;이 설정이 적용 되면 ① &lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;배포된 프론트가 API 요청을 자유롭게 보낼 수 있고 ② 인증/세션 기반 요청도 credentials: true로 제대로 작동하고 ③ OPTIONS 요청에 제대로 응답하게되어 preflght 실패도 나지 않을 것이다.&amp;nbsp;&lt;/span&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;&lt;span style=&quot;color: #666666;&quot;&gt;[ TIPS]&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;① 여러 Origin을 지원하고 싶다면 allowedOrigins 대신. allowedOriginPatterns을 사용하고 쉼표로 나눠준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750940006925&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.allowedOriginPatterns(&quot;https://*.vercel.app&quot;, &quot;http://localhost:3000&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;&lt;span style=&quot;color: #666666;&quot;&gt; &lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;②&lt;/span&gt; 도메인 와일드카드 사용시 credentials을 사용할 수 없음으로 주의하자. 이렇게 사용했다면 이런 오류가 뜰 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;txc-textbox&quot; style=&quot;box-sizing: border-box; margin: 0px; background-color: #ffffff; border: #000000 2px solid; padding: 6px;&quot;&gt;
&lt;p style=&quot;font-size: 16px; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; Access&amp;nbsp;to&amp;nbsp;XMLHttpRequest&amp;nbsp;at&amp;nbsp;'https://api.example.com/data'&amp;nbsp;from&amp;nbsp;origin&amp;nbsp;'https://frontend.com' has&amp;nbsp;been&amp;nbsp;blocked&amp;nbsp;by&amp;nbsp;CORS&amp;nbsp;policy:&amp;nbsp;&lt;b&gt;The&amp;nbsp;value&amp;nbsp;of&amp;nbsp;the&amp;nbsp;'Access-Control-Allow-Origin'&amp;nbsp;header&amp;nbsp;in&amp;nbsp;the&amp;nbsp;response&amp;nbsp;must&amp;nbsp;not&amp;nbsp;be&amp;nbsp;the&amp;nbsp;wildcard&amp;nbsp;'*'&amp;nbsp;when&amp;nbsp;the&amp;nbsp;request's&amp;nbsp;credentials&amp;nbsp;mode&amp;nbsp;is&amp;nbsp;'include'. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750940483346&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;         allowedOrigins(&quot;*&quot;) 설정           
     &amp;rarr; 모든 도메인 허용 (와일드카드 사용)     

                     │
                     ▼
     ❌ allowCredentials(true) 사용 불가!
     (브라우저에서 보안상 차단됨)

─────────────────────────────────────────────

 allowedOrigins(&quot;https://corstest.com&quot;) 설정 
  &amp;rarr; 정확한 도메인만 허용 (정적 값)            

                     │
                     ▼
     ✅ allowCredentials(true) 사용 가능!
     (인증 쿠키, 세션 등 주고받을 수 있음)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>spring 스프링</category>
      <category>cors에러</category>
      <category>WebConfig</category>
      <category>webconfig구성</category>
      <category>webconfig작성</category>
      <category>withcredentials 에러</category>
      <category>스프링cors에러</category>
      <category>스프링withcredentials 에러</category>
      <category>스프링부트webconfig</category>
      <author>해니01_15</author>
      <guid isPermaLink="true">https://henniee.tistory.com/461</guid>
      <comments>https://henniee.tistory.com/461#entry461comment</comments>
      <pubDate>Thu, 26 Jun 2025 21:38:56 +0900</pubDate>
    </item>
  </channel>
</rss>