<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>e_jink0</title>
        <link>https://velog.io/</link>
        <description>기록남기기</description>
        <lastBuildDate>Wed, 11 Mar 2026 13:37:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>e_jink0</title>
            <url>https://velog.velcdn.com/images/e_jink0/profile/0930de8b-1c37-441b-8b74-2c50d1e453e5/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. e_jink0. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/e_jink0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Ubuntu 홈서버 구축(2. tailscale로 ssh접속하기)]]></title>
            <link>https://velog.io/@e_jink0/Ubuntu-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%952.-tailscale%EB%A1%9C-ssh%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@e_jink0/Ubuntu-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%952.-tailscale%EB%A1%9C-ssh%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Mar 2026 13:37:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/c6ef59f5-c1a8-4494-ade0-9ceccac61ba0/image.png" alt="">
이렇게 퓨어해진 나의 데스크톱...</p>
<p>TailScale을 설치해서 동료들이 나의 홈서버에 접속해서 작업할 수 있도록 해보자.</p>
<h1 id="홈서버에-tailescale설치">홈서버에 TaileScale설치</h1>
<pre><code>curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up</code></pre><p>설치하고 sudo tailscale up을 하면
To authenticate, visit:
<a href="https://login.tailscale.com/xxxxxxx">https://login.tailscale.com/xxxxxxx</a>
이런식으로 url이 뜨는데 여기에 접속을 해서 github로 로긘을 한다.</p>
<h1 id="맥북에-tailscale설치하고-접속하기">맥북에 TailScale설치하고 접속하기</h1>
<p>그럼 이제 내 맥북이 내 홈서버에 붙어야한다.
맥북에도 TailScale을 설치하고 홈서버에 붙어보자
<a href="https://tailscale.com/download">https://tailscale.com/download</a>
위 사이트에 들어가서 맥에 tailScale을 깔고
서버랑 같은 계정으로 로그인해야 한다.
<img src="https://velog.velcdn.com/images/e_jink0/post/ab430912-3b20-4553-aa2f-484e25aa1e8c/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/0a30ddd3-6940-4418-aa76-63df375e5acf/image.png" alt="">
접속 완료되면 이렇게 접속된다고 뜬다</p>
<h1 id="동료-초대하기">동료 초대하기</h1>
<p>Users탭에서 동료를 초대한다
<img src="https://velog.velcdn.com/images/e_jink0/post/3a4bc827-2083-4733-b3a7-3bd1308b334e/image.png" alt=""></p>
<p>동료에게도 sudo tailscale up을 해서 github계정으로 로그인하게 한다음 homeserver로 로그인한 계정으로 붙을 수 있도록 해야한다.</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/3a8d0a68-953b-4699-ad6b-64fdbf4eeb89/image.png" alt=""></p>
<p>그리고 동료에게 ssh키(.pub)를 받아서 해당 키를 서버에저장한다.
동료에게 ssh-keygen -t ed25519 를해서 나온파일을 달라고 하면된다.
vi ~/.ssh/authorized_keys
위 명령어로 동료에게 받은 파일을 전부긁어서 붙여넣기한다.</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/e6704e32-6566-493a-ae94-0a64c5f2043e/image.png" alt="">
vscode에 ssh config파일 수정해주고 접속하면
tailscale에 붙어있는 pc들만 접속할 수 있다.</p>
<p>이제 Cloudflare로 네트워크 구성을 할 차례다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu 홈서버 구축(1. window 밀고 ubuntu깔기)]]></title>
            <link>https://velog.io/@e_jink0/Ubuntu-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@e_jink0/Ubuntu-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Thu, 05 Mar 2026 00:11:37 GMT</pubDate>
            <description><![CDATA[<h1 id="왜-홈서버를-구축하게-되었는가">왜 홈서버를 구축하게 되었는가?</h1>
<p>그동안 AWS 무료 티어를 활용해서 서버를 운영해왔다.</p>
<p>매년 계정을 새로 만들어서 무료 티어를 이어 쓰는 방식이었다.</p>
<p>예를 들면 이런 식이다.
    •    wepinlab2024
    •    wepinlab2025
    •    wepinlab2026</p>
<p>매년 새로운 계정을 생성하고, 서버를 옮기고, 도메인을 다시 연결하고, 환경을 재구성했다.</p>
<p>처음에는 잘 동작했다.</p>
<p>그런데 어느 순간부터 이상했다.</p>
<p>새 계정을 만들어 로그인했는데도
무료 티어가 적용되지 않았다.</p>
<p>아마도 AWS에서 계정 패턴이나 결제 정보 기반으로
중복 무료 사용을 감지한 것 같았다.</p>
<p>결국 깨달았다.</p>
<p>“이건 지속 가능한 방식이 아니다.”</p>
<p>매년 서버를 이사 다니는 것도 번거롭고
계정 관리도 점점 복잡해졌다.</p>
<p>예전에 게임 하려고 샀던 데스크탑이 계속 놀고있었기에, 이를 홈서버로 구축해보려고 마음을 먹게 되었다.</p>
<h1 id="windows-삭제-→-ubuntu-server-설치">Windows 삭제 → Ubuntu Server 설치</h1>
<p>ubuntu 설치는 꽤나 간단했다. 기존 Windows를 밀고 Ubuntu Server 24.04 LTS 설치하였다.</p>
<h2 id="ubuntu-iso파일-받기">Ubuntu iso파일 받기</h2>
<p>현재(26.03.04)기준 제일 최신 LTS를 다운받으면 된다.
<img src="https://velog.velcdn.com/images/e_jink0/post/4f7d3d38-f136-47fb-9836-628247236867/image.png" alt=""></p>
<h2 id="rufus-다운받고-usb굽기">Rufus 다운받고 USB굽기</h2>
<p><a href="https://rufus.ie/ko/">https://rufus.ie/ko/</a>
<img src="https://velog.velcdn.com/images/e_jink0/post/407033e5-a784-4886-9490-fbfc50c0f164/image.png" alt="">
필자의 데스크탑은 Window x64이기에 첫번째 릴리스 표준(rufus-4.13.exe)으로 받았다.</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/31f3a995-553b-479d-bd64-2cc83504bbc8/image.png" alt="">
장치는 USB를 선택하고, 부트 유형은 아까 다운받은 iso파일을 선택한다. 이외에는 건드리지않는다.
시작 누른 후 모든 옵션에서 예 선택하면된다.</p>
<h2 id="bios에서-boot우선순위-바꿔서-usb로-부팅하기">BIOS에서 boot우선순위 바꿔서 USB로 부팅하기</h2>
<p>요새는 BIOS가 UEFI로 업그레이드 되어서 UI도 좀더 쉽게 바뀌었다. USB로 부팅하기로 바꾼 후 운영체제 설치를 진행하면 된다.
<img src="https://velog.velcdn.com/images/e_jink0/post/33b94e43-c757-4932-ae18-a56211028156/image.png" alt="">
퓨어한 모습^^ 기분이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DBeaver로 DB migration 하기]]></title>
            <link>https://velog.io/@e_jink0/DBeaver-DB-migration</link>
            <guid>https://velog.io/@e_jink0/DBeaver-DB-migration</guid>
            <pubDate>Sat, 18 Oct 2025 12:29:10 GMT</pubDate>
            <description><![CDATA[<h3 id="db-dump하기">DB dump하기</h3>
<p>DBeaver에서 마이그레이션 할 db 우클릭 &gt; 도구 &gt; dump databases 클릭
<img src="https://velog.velcdn.com/images/e_jink0/post/ba505c18-6564-49d2-aa2a-05ba669b0fdc/image.png" alt="">
모든 테이블 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/b970ac2d-f29b-4468-b8cb-4f4c3c74bda0/image.png" alt="">
경로와 이름설정
<img src="https://velog.velcdn.com/images/e_jink0/post/47992287-9609-46a1-9ccd-39cd498ce665/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/7085a3cf-cc1c-4606-9a4e-8c5806b921fa/image.png" alt="">
이런 오류 뜨면 로컬에 mysql-client 깔기
이거는 덤프할때 mysql-client를 쓰는데 그경로를 지정해주지 않아서 발생하는것
mariaDB는 <a href="mailto:mysql-client@8.0">mysql-client@8.0</a>을 깔아야 함.</p>
<blockquote>
<p>$ brew install <a href="mailto:mysql-client@8.0">mysql-client@8.0</a>
$ which mysql-client</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/93ecdcca-c35b-4750-944d-2cf5e9a113c2/image.png" alt="">
local client로 경로 추가해주기 
<img src="https://velog.velcdn.com/images/e_jink0/post/58910251-5cff-4cc6-b3c5-de1009ea6e10/image.png" alt="">
홈추가 &gt; Finder떴을때 /opt 경로 접근할때는 단축어 <strong>command+shift+G</strong> 눌러서 접근하기
<img src="https://velog.velcdn.com/images/e_jink0/post/343f2011-bc1c-41ce-b3bf-d0c2fd137a70/image.png" alt="">
경로는 /opt/homebrew/Celler/mysql-client/8.0버전 폴더를 선택하기
<img src="https://velog.velcdn.com/images/e_jink0/post/c8adcf3d-4b14-4f56-9acd-b25abcbfa7fc/image.png" alt=""></p>
<p>그럼 이렇게 추가가 잘 되었습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/f10c9dae-3339-4db6-ac7e-00e8491ee360/image.png" alt=""></p>
<p>그래서 Start를 하면 ?
<img src="https://velog.velcdn.com/images/e_jink0/post/a666fec9-a6a3-4f78-88e3-46cb6e5507c1/image.png" alt="">
잘되었습니다.</p>
<p>Finder를 켜서 잘 저장되었는지 확인
<img src="https://velog.velcdn.com/images/e_jink0/post/2bea0584-cf2c-4012-ad69-506ac5672858/image.png" alt=""></p>
<h3 id="db-restore하기">DB Restore하기</h3>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/c6d13242-b3e0-4196-a2cd-3e411a04cdcd/image.png" alt=""></p>
<p>restore할 db우클릭 &gt; 도구 &gt; Resotre database &gt; 파일설정하고 &gt; start
<img src="https://velog.velcdn.com/images/e_jink0/post/307c3fe1-fef1-4799-aa81-8a30bff09fc8/image.png" alt=""></p>
<p>만약 아래와 같은 에러가 난다면 </p>
<blockquote>
<p>Task &#39;MySQL restore&#39; started at Sat Oct 18 21:10:56 KST 2025
ERROR 1227 (42000) at line 762: Access denied; you need (at least one of) the SET USER privilege(s) for this operation</p>
</blockquote>
<p>터미널을 열어서 덤프에서 DEFINER 제거/치환 후 복원하는 작업을 진행해주어야 한다.
이 에러는 덤프 파일 안에 DEFINER/SQL SECURITY DEFINER가 들어 있어 복원 시
현재 사용자에게 SET USER 동적 권한이 없어서 나는 에러다.</p>
<blockquote>
<p>sed -E -i &#39;&#39; <br>  -e &#39;s/DEFINER=<code>[^</code>]+<code>@</code>[^<code>]+</code>/DEFINER=CURRENT_USER/g&#39; <br>  -e &#39;s/SQL SECURITY DEFINER/SQL SECURITY INVOKER/g&#39; <br>  /path/to/dump_db.sql</p>
</blockquote>
<p>그리고 재시도 하면
<img src="https://velog.velcdn.com/images/e_jink0/post/69642820-0525-410e-a328-8463ad6ddf7e/image.png" alt=""></p>
<p>retore완료
<img src="https://velog.velcdn.com/images/e_jink0/post/1575f3f5-aeed-4c85-acd2-46390ad9053b/image.png" alt=""></p>
<h3 id="cli로-덤프-하는법">CLI로 덤프 하는법</h3>
<p>간혹 DBeaver로 하다가 도저히 안되면 CLI로도 할 수도 있다. 이건 참고용</p>
<blockquote>
<p> sudo /opt/homebrew/Cellar/mysql-client/8.0/bin/mysqldump <br>--skip-lock-tables --column-statistics=0 --add-drop-table --disable-keys --extended-insert <br>-u admin -p --host=127.0.0.1 --port=3306 prod &gt; /Users/jinkyeong/dumpDB/prod_backup.sql</p>
</blockquote>
<p><code>mysqldump경로</code>    MySQL 데이터를 SQL 파일로 백업하는 CLI 도구 보통 /opt/homebrew/Callar 하위에 있습니다.
<code>--skip-lock-tables</code>    테이블 잠금 방지 (읽기 전용 백업 시 사용)
<code>--column-statistics=0</code>    일부 MySQL 버전 호환성 해결용 옵션
<code>--add-drop-table</code>    테이블 생성 전에 DROP TABLE을 먼저 넣음
<code>--disable-keys</code>    덤프 중 키 인덱스 비활성화 (성능 개선)
<code>--extended-insert</code>    여러 행을 한 INSERT문에 담아 SQL 사이즈 줄임
<code>-u admin -p</code> admin 유저로 로그인, -p는 비번은 나중에 입력
<code>--host=127.0.0.1</code> 로컬 호스트 (AWS RDS라면 엔드포인트 적기)
<code>--port=3306</code>    로컬에서 포트 3306으로 MySQL 접속
<code>prod</code>    백업 대상 DB 이름
<code>&gt; /Users/jinkyeong/dumpDB/prod_backup.sql</code> 덤프할 경로 이름 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] React프로젝트 S3에 배포하기(+ Github Actions)]]></title>
            <link>https://velog.io/@e_jink0/AWS-React%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S3%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Github-Actions</link>
            <guid>https://velog.io/@e_jink0/AWS-React%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S3%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Github-Actions</guid>
            <pubDate>Sun, 01 Jun 2025 15:42:40 GMT</pubDate>
            <description><![CDATA[<h1 id="0-사전-준비물">0. 사전 준비물</h1>
<ul>
<li>FE 프로젝트 (React)</li>
</ul>
<h1 id="1-s3-생성하기">1. S3 생성하기</h1>
<h2 id="1-1-버킷-생성">1-1. 버킷 생성</h2>
<p>이름이랑 ACL 비활성화됨 체크해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/296a2a46-9473-466f-8359-89aec33f73b2/image.png" alt="">
모든 퍼블릭 엑세스 차단 체크해제 해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/54e678a8-b955-440e-890d-73e52f08280b/image.png" alt="">
나머지는 디폴트값 으로 설정해주고 만들기
<img src="https://velog.velcdn.com/images/e_jink0/post/5f9276be-260a-4fc4-b234-aa607ca8cd1f/image.png" alt=""></p>
<h2 id="1-2-버킷-정적-웹사이트-호스팅-설정">1-2. 버킷 정적 웹사이트 호스팅 설정</h2>
<p>버킷 상세 &gt; 속성 &gt; 정적 웹사이트 호스팅 설정해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/8bd90070-11ae-4498-bbc8-a0303bc28244/image.png" alt=""></p>
<p><del>위 사진에 Amplify Hosting이라는 것이 있다. 이는 CI/CD 파이프라인, 다양한 프레임워크 지원, 서버리스 백엔드 통합 기능을 제공해주는 것인데 이번 포스팅은 github actions연동이기에... Amplify Hosting는 다음 포스팅에 다뤄보도록 하겠습니다. 🤓</del></p>
<p>정적 웹 사이트 호스팅 활성화 체크해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/850e256e-34eb-4914-a42b-5bd55dcaf6e0/image.png" alt=""></p>
<p>인덱스 문서- index.html 써주고 변경 사항 저장하기, 엔드포인트 확인
<img src="https://velog.velcdn.com/images/e_jink0/post/40abe7c2-bf36-4726-b72f-7cf6ec4d8b96/image.png" alt=""></p>
<h2 id="1-3-버킷-정책-설정">1-3. 버킷 정책 설정</h2>
<p>버킷 권한 &gt; 버킷 정책 편집
<img src="https://velog.velcdn.com/images/e_jink0/post/3c7a5a4e-6b47-45e3-9640-a4805674e62f/image.png" alt=""></p>
<pre><code>{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Sid&quot;: &quot;Statement1&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Principal&quot;: &quot;*&quot;,
            &quot;Action&quot;: &quot;s3:GetObject&quot;,
            &quot;Resource&quot;: &quot;arn:aws:s3:::bucketname/*&quot;
        }
    ]
}</code></pre><blockquote>
<p>&quot;Version&quot;: &quot;2012-10-17&quot;
정책 버전. 항상 &quot;2012-10-17&quot;을 사용하면 됨 (현재 기준 최신이자 안정된 버전).</p>
<p>&quot;Sid&quot;: &quot;Statement1&quot;
Statement ID로, 사람이 구분하기 쉽게 붙이는 이름 (필수는 아님).</p>
<p>&quot;Effect&quot;: &quot;Allow&quot;
이 정책의 효과를 설정. &quot;Allow&quot;: 허용, &quot;Deny&quot;: 거부</p>
<p>&quot;Principal&quot;: &quot;*&quot;
누가 접근 가능한지 지정. *은 사용자(익명 사용자 포함)를 의미. 인터넷에 공개된 공개 버킷이라는 뜻</p>
<p>&quot;Action&quot;: &quot;s3:GetObject&quot;
허용하는 작업의 종류. GetObject-브라우저에서 정적 파일을 읽는 것만 허용</p>
<p>&quot;Resource&quot;: &quot;arn:aws:s3:::bucketname/*&quot;
정책이 적용되는 리소스</p>
</blockquote>
<h1 id="2-aws-ssl-무료-인증서-생성">2. AWS SSL 무료 인증서 생성</h1>
<p>Q1. 왜 SSL인증서를 발급받아야 하나요?
✅ S3의 정적 웹사이트 호스팅 기능을 사용하면, <a href="http://bucketname.s3-website-">http://bucketname.s3-website-</a>&lt;region&gt;.amazonaws.com 형태의 엔드포인트가 생성됩니다. 이 엔드포인트는 HTTP만 지원하고 HTTPS는 지원하지 않습니다.
사용자가 https:// 로 접속할 수 있도록 하려면 CloudFront를 이용해 S3 버킷을 프록시 처리하고, SSL 인증서를 연결해야 합니다.
예) <a href="https://mydomain.xxx">https://mydomain.xxx</a> -&gt; <a href="http://bucketname.s3-website-">http://bucketname.s3-website-</a>&lt;region&gt;.amazonaws.com
따라서 SSL 인증서는 https:// 도메인으로 접속하기 위해 CloudFront 배포에 연결되어야 하며,
일반적으로는 <strong>AWS Certificate Manager(ACM)</strong>를 통해 발급받아 사용합니다.</p>
<p>Q2. CloudFront는 뭐고 왜 사용하는거죠?
✅ CloudFront란? <strong>컨텐츠(파일, 동영상 등)를 빠르게 전송하게 해주는 서비스</strong> 입니다. 
<img src="https://velog.velcdn.com/images/e_jink0/post/6fd89c6f-640b-4237-b4f5-5f81ae754209/image.png" alt="">
컨텐츠(파일, 동영상)는 S3라는 곳에 저장이 됩니다. 하지만 그 S3 저장소가 한국에 있다고 가정했을때, 한국 사용자는 S3와 거리가 가까우므로 데이터를 빠르게 전송받을 수 있습니다. 하지만 미국에 있는 사용자가 S3로부터 데이터를 전송받으려면 거리가 멀어 시간이 오래 걸리게 됩니다.</p>
<p>이런 문제를 해결하기 위해 전세계 곳곳에 <strong>컨텐츠(파일, 동영상)의 복사본을 저장해놓을 수 있는 임시 저장소를 구축</strong>합니다. 그러면 미국에 있는 사용자가 컨텐츠를 전송받고 싶을 때, 가장 가까운 임시 저장소에서 컨텐츠를 가져오면 훨씬 속도가 빨라진다.
이런 형태의 서비스를 보고 <strong>CDN(Content Delivery Network)</strong>이라고 합니다. 그래서 CloudFront를 CDN 서비스라고도 표현합니다.</p>
<p>✅ CloudFront는 왜 사용하는 걸까?
위의 설명에 따르면 CloudFront는 <strong>컨텐츠를 전송 받는 성능을 향상시키기 위해 사용</strong>하게 됩니다. 그리고 <strong>HTTPS를 적용</strong>하려면 CloudFront를 사용해야만 합니다. S3에는 HTTPS를 적용시키는 기능을 제공하지 않습니다. HTTPS를 적용함으로 <strong>보안을 한층 강화</strong>할 수 있다는 장점이 있습니다.</p>
<p>이러한 장점들 덕분에 S3와 CloudFront를 같이 써서 웹 서비스를 배포할 수 있는 것입니다.</p>
<h2 id="2-1-acm에서-인증서-발급받기">2-1. ACM에서 인증서 발급받기</h2>
<p>CloudFront에서 사용 가능한 인증서는 반드시 <strong>미국 동부(버지니아 북부) 리전(us-east-1)</strong> 에 있어야 하므로 리전을 버지니아 북부로 이동한 후 인증서 생성하기.</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/ff3b9e19-9f74-47fe-b4bf-0c1e308cb3fb/image.png" alt="">
퍼블릭 인증서 요청 체크하고 다음
<img src="https://velog.velcdn.com/images/e_jink0/post/f83ae00f-1af5-4b22-9c18-045923c4075c/image.png" alt="">
완전히 정규화된 도메인이름에 본인의 도메인 이름 넣기
<img src="https://velog.velcdn.com/images/e_jink0/post/d0691a22-7f39-4f82-b655-c0cc21636081/image.png" alt="">
키 알고리즘은 RSA 2048 선택하고 요청하기
<img src="https://velog.velcdn.com/images/e_jink0/post/e40c9745-7cd3-4c5d-ab9c-052e6ed47158/image.png" alt="">
이제 이 만들어진 인증서를 검증해야 합니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/5bf4eb98-166c-4963-b8b3-87423faa7c90/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/2f17f7be-62d7-4a91-b90b-819c363996d4/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/11b690f1-496c-47c4-8c31-4a7097df4191/image.png" alt="">
3분~10분정도 기다리면
<img src="https://velog.velcdn.com/images/e_jink0/post/2cd2e090-022c-4ea1-8a31-7d83d201cfaa/image.png" alt=""></p>
<p>발급 완</p>
<h1 id="3-cloudfront-생성">3. CloudFront 생성</h1>
<h2 id="3-1-cloudfront-배포생성하기">3-1. CloudFront 배포생성하기</h2>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/a0173886-51cb-40b7-8293-1063da205901/image.png" alt=""></p>
<p>싱글 웹사이트 앱 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/f7168f32-7a3e-40bb-a208-fcf67ba56bd7/image.png" alt=""></p>
<p>Origin domain에서 내가 만든 s3버킷 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/b313f95d-660e-4241-b546-78880c0e94a5/image.png" alt="">
선택하면 웹사이트 엔드포인트 사용 노티가 뜨는데 사용한다고 버튼 클릭하기</p>
<p>HTTP만 해당, 80번포트 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/890e47e3-0a18-4a6e-9aa8-933847f32853/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/dcd6715a-c53c-49e1-a2d6-aaa3940eed5e/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/f2798486-83ff-4911-9b96-07b48481df12/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/fa49b6ea-8ea1-467a-9cd2-6ad908221ecb/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/1f672b37-e5a0-4985-b136-e846a862e3d9/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/89c647d9-26a7-42bc-b21f-477eebf358d2/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/31e187a5-4d7c-40b8-9405-b7b5d1977bf9/image.png" alt="">
대체 도메인 이름에 본인 도메인 넣기
<img src="https://velog.velcdn.com/images/e_jink0/post/7fd8c7e9-f180-4f04-b40a-c06bee7773ec/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/744cb690-f972-45f7-a833-d82913d45ff1/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/678a8959-60c8-4314-8886-b4f4531f1622/image.png" alt="">
항목이 너무많아서 위 사진대로 선택 후 배포 생성 클릭</p>
<h1 id="4-route53에-레코드-추가">4. Route53에 레코드 추가</h1>
<p>별칭 on하고 트래픽 라우팅 대상을 CloudFront 배포에 대한 별칭 추가 선택하고 하단에 배포 선택란을 누르면 자동으로 이전에 등록했던 cloud front가 뜹니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/3def346a-2864-4169-aae8-050a7444afa7/image.png" alt=""></p>
<h1 id="5-iam-secret-key-발급">5. IAM Secret Key 발급</h1>
<p>S3에 코드 또는 파일을 올릴 수 있는 권한은 ec2(backend)와 github actions입니다.
ec2(backend)에서는 클라이언트에서 이미지를 업로드 했을때, 프로필 사진을 변경했다거나, 이미지가 들어간 게시물을 업로드 했다거나 했을때 api를 통해서 업로드를 할 수 있게 됩니다.
이번 포스팅은 github actions설정을 하여서 자동배포 환경을 만들어 볼 것입니다.</p>
<h2 id="5-1-사용자-그룹-만들기">5-1. 사용자 그룹 만들기</h2>
<p>IAM &gt; 사용자 &gt; 사용자 생성하기
<img src="https://velog.velcdn.com/images/e_jink0/post/35343237-a87b-42cb-8635-cc5efe4b76a8/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/80fad94d-b6dc-46ad-83af-1d04c0421a37/image.png" alt=""></p>
<p>직접 정책 연결로 하나씩 관리할 수 있지만 그룹으로 그룹 전체에 권한을 적용할 수 있습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/ae2da424-2cd2-4252-8a18-74aa503f7394/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/813f8cd1-d927-4ce1-891b-2d18cfa9d788/image.png" alt=""></p>
<p>추가할 정책: AmazonS3FullAccess , AWSCodeDeployFullAccess , AmazonSSMFullAccess, AmazonSSMReadOnlyAccess, cloudfront_createInvalidation권한 추가
<strong>cloudfront_createInvalidation</strong> 이 항목은 위 사진의 &#39;정책 설정&#39; 버튼틀 클릭해서 직접 설정해줍니다.</p>
<pre><code>{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;AllowCIWrite&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;s3:PutObject&quot;,
        &quot;s3:DeleteObject&quot;,
        &quot;s3:ListBucket&quot;
      ],
      &quot;Resource&quot;: [
        &quot;arn:aws:s3:::bucketname&quot;,
        &quot;arn:aws:s3:::bucketname/*&quot;
      ]
    },
    {
      &quot;Sid&quot;: &quot;AllowCloudFrontInvalidation&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: &quot;cloudfront:CreateInvalidation&quot;,
      &quot;Resource&quot;: &quot;*&quot;
    }
  ]
}</code></pre><p>이렇게 생성해주면
<img src="https://velog.velcdn.com/images/e_jink0/post/8cd437df-2ba8-4447-9e9e-179aa5c8cb2e/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/dc16ce1e-8fcb-4c92-b6dc-deec5a234fb5/image.png" alt=""></p>
<p>권한 정책에 검색하면 뜨게 됩니다. 그리고 추가해줍니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/2b5768a3-dc08-4c66-86cb-825cc68a73e1/image.png" alt=""></p>
<p>잘 만들어졌네요
<img src="https://velog.velcdn.com/images/e_jink0/post/e6c48099-f4f9-4048-8fb3-b4e33fc96217/image.png" alt=""></p>
<h2 id="5-2-엑세스-키-발급받기">5-2. 엑세스 키 발급받기</h2>
<p>생성한 사용자 → 보안 자격 증명 → 액세스 키 → 액세스 키 만들기
<img src="https://velog.velcdn.com/images/e_jink0/post/7548d193-1326-4706-b370-200882cd548e/image.png" alt=""></p>
<p>AWS 외부에서 실행되는 애플리케이션 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/3fba35f7-5249-4092-b0e5-e3484346519b/image.png" alt=""></p>
<p>생성한 후 페이지는 다시 올 수 없으니 .csv파일 다운로드 해두기
<img src="https://velog.velcdn.com/images/e_jink0/post/d03db733-f887-45db-94c4-2ec0c480d1ac/image.png" alt=""></p>
<h1 id="6-github-actions-설정하기">6. github actions 설정하기</h1>
<h2 id="6-1-secrets-설정하기">6-1. Secrets 설정하기</h2>
<p>레포지토리 설정 &gt; Secrets And Variables &gt; Actions로 이동 해서 Repository secrets 항목에다가 추가해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/7b202e5e-90a1-428c-bb01-8ad876980b04/image.png" alt="">
<code>AWS_ACCESS_KEY</code> = S3 권한이 있는 유저 엑세스 키 (.csv에 있음)
<code>AWS_SECRET_ACCESS_KEY</code> = S3 권한이 있는 유저 비밀 엑세스 키 (.csv에 있음)
<code>AWS_REGION</code> = ap-northeast-2
<code>S3_DEV_REPO</code> = 정적 리소스 배포할 S3 버킷 이름 (예. <code>myS3Bucket</code>)
<code>CLOUDFRONT_DEV_ID</code> = CloudFront ID이름 (예. E43YM3452OA)</p>
<p>배포 성공/실패 여부를 slack이나 디스코드같은 메신저로 자동 발송해주는 기능을 추가할때도 여기다가 key를 넣는데
이거는 다음 포스팅에서 다뤄보도록 하겠습니다. 👀</p>
<h2 id="6-2-프로젝트-내부에-yml파일-만들기">6-2. 프로젝트 내부에 .yml파일 만들기</h2>
<p><strong>.github/workflows/filename.yml 경로</strong>에 .yml 만들기</p>
<pre><code>name: ELLIE WEB DEVELOPMENT DEPLOY

# Controls when the workflow will run
on:
  push:
    branches: [main] #push 반응을 할 브런치

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called &quot;build&quot;
  build_dev:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v3

      # node 버전 통일해야 할 경우 이와 같이 노드 버전 지정.
      - uses: actions/setup-node@v3
        with:
          node-version: 20.17.0
          cache: npm

      # aws credentials 생성
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Install dependencies
        run: npm install

      # npm 패키지 설치 &amp; 리액트 빌드
      - name: install npm dependencies &amp; build react
        env:
          AWS_ENV: dev
          CI: false
          REACT_APP_AWS_SSM_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
          REACT_APP_AWS_SSM_SECRET_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          REACT_APP_AWS_SSM_REGION: ${{ secrets.AWS_REGION }}
        run: |
          CI=false
          npm ci 
          npm run build

      # AWS S3에 빌드 결과 업로드
      - name: Deploying to S3 BUCKET
        run: aws s3 sync ./dist/ s3://${{ secrets.S3_DEV_REPO }} --delete

      # cloudfront 캐시 무효화
      - name: Invalidate cache for CloudFront
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DEV_ID }} --paths &quot;/*&quot;</code></pre><p>.yml파일을 푸시하고 레포지토리의 Actions 탭으로 가면 Actions이 도는것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/5cc7320f-852b-4a5c-a06b-6cf6ac3ef070/image.png" alt=""></p>
<p>5트만에 성공했네요. 🤓
성공하고 도메인 치면 잘 들어가집니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/568b5220-4439-4a5a-82df-e6366bcdadd8/image.png" alt=""></p>
<h1 id="7-결과적으로">7. 결과적으로</h1>
<p>BE배포와 FE배포를 나눠서 하나의 도식화로 보자면 이런 구조입니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/79a1ba8c-d3d9-49c7-9610-305b3098cdd3/image.png" alt=""></p>
<p>실제 현업에서는 dev와 prod를 나눠서 위 구조보다 좀더 복잡하게 되는데 일단 한 사이클은 돌았으니, 위 내용 기반으로 업그레이드 하면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] Nginx 와 Let's Encrypt로 https 설정하기]]></title>
            <link>https://velog.io/@e_jink0/AWS-Nginx-%EC%99%80-Lets-Encrypt%EB%A1%9C-https-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@e_jink0/AWS-Nginx-%EC%99%80-Lets-Encrypt%EB%A1%9C-https-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 28 May 2025 12:17:19 GMT</pubDate>
            <description><![CDATA[<h1 id="0-사전-준비물">0. 사전 준비물</h1>
<ul>
<li>가비아에서 구매한 도메인</li>
</ul>
<h1 id="1-route-53에-도메인-연결">1. Route 53에 도메인 연결</h1>
<p>이전 포스팅에서 <a href="http://3.36.34.232:8080/swagger-ui/index.html#">http://3.36.34.232:8080/swagger-ui/index.html#</a> 와 같이 날것의 URL로 접속 해보고 마무리가 되었는데, 이번에는 https로 설정해주면서 도메인까지 적용해보도록 할것이다.</p>
<p>Q1. 날것의 URL에서 https만 적용하면 안되나요?
-&gt; 안된다. https는 SSL/TLS 인증서 기반의 암호화를 사용하는데, 이 인증서는 <strong>도메인(domain name)</strong>에 발급되는 것이지, IP 주소에 발급되지 않기 때문이다.</p>
<p>Q2. 왜 IP에는 인증서를 못받나요?
-&gt; <strong>Let&#39;s Encrypt, DigiCert, GlobalSign 등 주요 인증기관(CA)</strong>은 도메인 소유권을 확인한 뒤에만 인증서를 발급합니다. IP 주소는 소유권을 증명하기 어렵기 때문에 인증서를 발급해주지 않습니다.</p>
<h2 id="1-1-route-53에-호스팅-영역-생성">1-1. Route 53에 호스팅 영역 생성</h2>
<p>호스팅 영역 체크
<img src="https://velog.velcdn.com/images/e_jink0/post/dfdd3ade-e94d-4e28-9426-e846bdd2a1a1/image.png" alt=""></p>
<p>도메인 이름에 가비아에서 구매한 도메인 이름 넣기
<img src="https://velog.velcdn.com/images/e_jink0/post/b28417f7-602a-4efc-8fb4-cffee57faf4d/image.png" alt=""></p>
<p>호스팅 영역 생성 완
<img src="https://velog.velcdn.com/images/e_jink0/post/63e6ba25-3eb9-4004-acea-fdbb906873c3/image.png" alt=""></p>
<p>호스팅 영역에 들어오면 이미 2개의 레코드가 생성되어있다.
✅ <strong>NS</strong>는 <strong>도메인을 등록한 등록기관(예: 가비아)</strong> 에 설정해줘야 해당 도메인 질의가 Route 53으로 위임됩니다.
✅ <strong>SOA</strong>는 도메인의 기본 정보와 권한을 가진 DNS 서버에 대한 정보를 포함합니다.
포함 정보:</p>
<ul>
<li>권한을 가진 네임서버(NS) 주소</li>
<li>관리자 이메일 주소</li>
<li>시리얼 번호 (변경 시 증가)</li>
<li>캐시 TTL, 재시도 시간 등 DNS 운영 관련 값들
보통은 SOA 레코드를 직접 수정하지 않으며, Route 53에서 자동 관리됩니다.</li>
</ul>
<p>✅ <strong>A</strong>는 도메인 이름을 IPv4 주소로 연결해주는 DNS(Domain Name System) 레코드입니다. 예를 들어서 api.ellieworld.shop을 3.36.34.232로 바꿔주는겁니다.</p>
<h2 id="1-2-가비아에-네임서버-등록">1-2. 가비아에 네임서버 등록</h2>
<p>도메인을 가비아에서 발급받았기때문에 Route53의 호스팅영역의 NS코드를 가비아의 네임서버에 등록시킵니다.</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/ce63147a-934a-4796-92c4-c4eb784b1744/image.png" alt="">
이 값을
<img src="https://velog.velcdn.com/images/e_jink0/post/ac89f8cf-42f3-4c87-ac16-f3ddb78c4a5a/image.png" alt="">
이렇게 셋팅해주고 셋팅해줄때 뒤에 .은뺍니다.</p>
<h2 id="1-3-생성된-호스팅-영역에-ec2-연결">1-3. 생성된 호스팅 영역에 EC2 연결</h2>
<p>api.ellieworld.shop 레코드 추가
<img src="https://velog.velcdn.com/images/e_jink0/post/11b6c6b5-7e6f-4c0e-926c-fa63c0cb6af5/image.png" alt=""></p>
<p>레코드 유형은 A, 값은 EC2의 public IP
<img src="https://velog.velcdn.com/images/e_jink0/post/37bbbfc1-6047-4952-b11a-24844d583ebb/image.png" alt=""></p>
<p>nginx를 80번 포트로 실행할것이라 보안그룹의 인바운드 규칙에 80번을 열어줍니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/8b73ed83-8cb3-474d-b4fb-bc66b3a3b1e2/image.png" alt="">
IPv4와 IPv6 규칙을 둘다 추가해주어야 합니다.</p>
<p>Q. 왜 그렇게 해야하나요?
-&gt; AWS 보안 그룹이 각 IP 프로토콜 버전에 대해 별도로 규칙을 처리하기 때문입니다. 하나의 규칙에 0.0.0.0/0(IPv4)와 ::/0(IPv6)을 같이 넣을 수 없습니다.</p>
<h2 id="1-4-nginx-리버스-프록시-서버-설정">1-4. nginx 리버스 프록시 서버 설정</h2>
<p>서버에 접속해서 nginx를 설치 해줍니다.</p>
<blockquote>
<p>$ sudo apt install nginx</p>
</blockquote>
<p> /etc/nginx/sites-available 이 위치에 가면 default파일이 있는데 이 파일을 수정해야합니다.</p>
<blockquote>
<p>$ sudo vi /etc/nginx/sites-available/default</p>
</blockquote>
<p>vi를 해서 안에 내용을 보면 뭔가 엄청많습니다. 그렇지만 하나씩 수정해보도록 하겠습니다.</p>
<pre><code>upstream dev-api-server {
    server 3.36.34.232:8080;
}</code></pre><p>위코드를 server{} 상단에 추가해줍니다. 물론 저 ip는 ec2 public ip입니다.</p>
<p>그리고 80포트 옆 default_server변수를 지워줍니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/09e1fdc1-ca2f-4efb-b361-81323f332ba7/image.png" alt=""></p>
<p>그리고 아래로 내려가서 location{} 을 찾습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/27c980fe-9666-43ba-9a29-6a112263c981/image.png" alt=""></p>
<p>location변수 위에 server_name이 있는데 여기에 api 도메인을 적습니다. 저는 api.elieworld.shop입니다. <del>가비아에서 500원주고 샀습니다.</del></p>
<p>그리고 location{} 내부에 proxy정보들을 넣어줍니다.</p>
<p>root /var/www/html;
줄부터 index index.html... 요 줄도 삭제해줍니다. 
<img src="https://velog.velcdn.com/images/e_jink0/post/bc53ca68-f391-4ba4-99dd-71538cc80a3d/image.png" alt=""></p>
<p>최종적으로 이렇게만 수정하면 됩니다.</p>
<p>✅ 이 작업의 의미:
api.ellieworld.shop으로 들어오는 요청을 받아서
실제로는 <a href="http://3.36.34.232:8080">http://3.36.34.232:8080</a> (즉, 백엔드 서버)로 전달
그리고 결과를 다시 클라이언트에게 응답
👉 이게 바로 리버스 프록시 역할</p>
<p>default파일을 저장하고 여기서 /etc/nginx 경로로 가서 폴더 리스트를 보면
<img src="https://velog.velcdn.com/images/e_jink0/post/3075b7b1-344a-4f5e-aea8-7b3aee30b6de/image.png" alt="">
sites-available, sites-enabled가 있죠 방금 제가 수정한 파일은
sites-available/default 입니다. 이미 심볼릭 링크가 sites-enabled에 있을겁니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/db3fe345-a43f-4de7-9b48-7cac7c819803/image.png" alt=""></p>
<p>심볼릭 링크는 windows의 바로가기와 비슷한 개념입니다. enabled한 사이트의 conf파일을 지정해주는거죠.
Nginx는 sites-enabled/ 안의 설정 파일만 읽기 때문에,
실제 설정은 sites-available/에 두고,
쓸 설정만 링크로 연결해주는 방식입니다.</p>
<p>원래는 default를 쓰지않고 커스텀 해서 사용하겠다 하면 저 심볼릭 링크를 연결해주어야 합니다.</p>
<p>새로 conf를 만든 후에 심볼릭 링크 연결 CLI</p>
<blockquote>
<p>sudo ln -s /etc/nginx/sites-available/new_setting.conf /etc/nginx/sites-enabled/</p>
</blockquote>
<p>구성이 잘 되었는지 테스트 해봅시다.</p>
<blockquote>
<p>$ sudo nginx -t</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/5f923248-08b8-4960-9e12-183fc96a71f2/image.png" alt=""></p>
<p>nginx 재시작을 합니다.</p>
<blockquote>
<p>$ sudo systemctl restart nginx</p>
</blockquote>
<p>nginx가 다시 켜졌는지 확인합니다.</p>
<blockquote>
<p>$ systemctl status nginx.service</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/3444b1ca-cff6-4641-8cd1-e9b2957da45c/image.png" alt=""></p>
<h2 id="1-5-lets-encrypt-인증서-발급받기">1-5. Let&#39;s Encrypt 인증서 발급받기</h2>
<p>certbot을 설치합니다
✅ Let&#39;s Encrypt: 무료 인증서를 발급해주는 인증 기관(CA)
✅ Certbot: Let&#39;s Encrypt와 통신하여 인증서를 신청, 갱신, 설치하는 프로그램(도구)</p>
<blockquote>
<p>$ sudo apt install certbot python3-certbot-nginx -</p>
</blockquote>
<p>발급받기전에 Route53 &gt; api도메인 A레코드로 등록해놓았는지 체크
<img src="https://velog.velcdn.com/images/e_jink0/post/07f356ab-a0f8-451d-a6fb-5cf85129e117/image.png" alt=""></p>
<p>https인증서를 발급받습니다.</p>
<blockquote>
<p>sudo certbot --nginx -d api.ellieworld.shop</p>
</blockquote>
<p>email을 적을때는 aws로그인 계정을 적습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/334ef677-8d18-495e-96b3-7f5be7515ecf/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/91b9941f-e526-442f-964a-8b0d8b0781fe/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/4f9e7822-08a1-4644-9262-76214d9c3dac/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/604e006c-cc0a-412a-9a5f-51cfce1354b0/image.png" alt=""></p>
<p>그러면
<img src="https://velog.velcdn.com/images/e_jink0/post/170ef46c-4cfa-4868-8ded-ea43702b2da5/image.png" alt=""></p>
<p>발급받은 인증서 위치</p>
<blockquote>
<p>/etc/letsencrypt/live</p>
</blockquote>
<p>원하는 url이 나왔네요 👏 <a href="https://api.ellieworld.shop/swagger-ui/index.html#">https://api.ellieworld.shop/swagger-ui/index.html#</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] SpringBoot 프로젝트 EC2배포하기]]></title>
            <link>https://velog.io/@e_jink0/AWS-SpringBoot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-EC2%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@e_jink0/AWS-SpringBoot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-EC2%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Mon, 26 May 2025 09:59:50 GMT</pubDate>
            <description><![CDATA[<h1 id="0-사전-준비물">0. 사전 준비물</h1>
<ul>
<li>새로만든 aws계정</li>
<li><a href="https://github.com/EJinK0/AskAnything-API#">github BE 프로젝트</a> (spring boot 3.x.x, jdk 17) </li>
</ul>
<h1 id="1-ec2-생성">1. EC2 생성</h1>
<h2 id="1-1-리전-선택하기">1-1. 리전 선택하기</h2>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/2f78e5ed-3f26-4f5f-8e38-2be325b554a0/image.png" alt=""></p>
<p>서울로 선택</p>
<h2 id="1-2-인스턴스-생성하기">1-2. 인스턴스 생성하기</h2>
<p>인스턴스 시작
<img src="https://velog.velcdn.com/images/e_jink0/post/bbf41fc4-e69e-4d6b-9037-3e5845750e2d/image.png" alt=""></p>
<p>서버 이름 짓기
<img src="https://velog.velcdn.com/images/e_jink0/post/74fd2865-4bc3-4f2d-895b-17b11c3e224e/image.png" alt=""></p>
<p>os선택하기 ami는 프리티어로
<img src="https://velog.velcdn.com/images/e_jink0/post/b3581fcb-6cb6-4e74-adc9-75e2401657c1/image.png" alt=""></p>
<p>[2025.10 업데이트]
프리티어 6개월 + 200USD 크래딧으로 바뀌면서 AMI선택 범위가 늘어났다.
<img src="https://velog.velcdn.com/images/e_jink0/post/81da1818-1179-4228-ace7-b49393242f95/image.png" alt="">
그렇지만 DeepLearning은 아직 써보진 않았기때문에 24.04또는 22.04 를 선택.
두가지의 차이는 아래와 같다</p>
<p><strong>22.04 LTS</strong></p>
<ul>
<li>이미 2년 이상 서비스되어 가장 안정적인 LTS 버전으로 평가받습니다.</li>
<li>대부분의 서버용 패키지(Spring Boot, Nginx, Docker, Node, Java 등)가 공식적으로 완벽 호환됩니다.</li>
<li>기업 환경이나 운영 서버에 많이 사용됩니다.</li>
</ul>
<p><strong>24.04 LTS</strong></p>
<ul>
<li>최신 기술 스택(예: Python 3.12, Kernel 6.8, systemd 255)을 지원하지만, 일부 패키지(특히 오래된 배포판 기준으로 빌드된 것들)는 아직 완벽히 검증되지 않았을 수 있습니다.</li>
<li>최신 버전 테스트나 개인 프로젝트용엔 적합하지만, 운영 안정성이 절대적으로 중요하면 22.04를 추천합니다.</li>
</ul>
<p>인스턴스 유형은 t2.micro 프리티어 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/ebca186a-ffdf-4f2a-a2f1-8190fd18dd40/image.png" alt=""></p>
<p>keypair 생성
<img src="https://velog.velcdn.com/images/e_jink0/post/1a799d09-170b-4553-9ae7-9a6611874800/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/4bae3e58-83f8-4108-854b-b143a5c6704c/image.png" alt=""></p>
<p>VPC설정은 기본값 그대로. 보안그룹도 설정된 값 그대로. 나중에 추가할것임
<img src="https://velog.velcdn.com/images/e_jink0/post/2aeac26a-1b1f-4ddf-a336-32bb2a1728f4/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/c30b31b5-d71f-4740-92d0-1e1a4293a3b6/image.png" alt=""></p>
<p>스토리지는 최대 30GB를 지원해주기때문에 30으로 해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/96592322-da13-4c16-ae69-02d4d0b074b3/image.png" alt=""></p>
<p>결과적으로
<img src="https://velog.velcdn.com/images/e_jink0/post/39faee1f-f0ac-40e6-bc4a-a182c4f733ac/image.png" alt=""></p>
<p>만들기
<img src="https://velog.velcdn.com/images/e_jink0/post/c67d8fa8-587f-47d1-8b10-962cc283828d/image.png" alt=""></p>
<h1 id="2-ec2-접속">2. EC2 접속</h1>
<h2 id="2-1-vscode에서-sshconfig수정-후-접속">2-1. vscode에서 ~/.ssh/config수정 후 접속</h2>
<p>단축키: command + shift + P 
<img src="https://velog.velcdn.com/images/e_jink0/post/10728e74-050a-4af4-a7ae-84a50efbae0b/image.png" alt=""></p>
<p>필자는 root directory에 awsSSH를 모아놓고 관리하는 편이다.
<img src="https://velog.velcdn.com/images/e_jink0/post/423cb3f0-7ab8-48e6-8beb-7bd34c17aca7/image.png" alt=""></p>
<p>이런식으로 복붙하면 편하다
<img src="https://velog.velcdn.com/images/e_jink0/post/27c24293-e8fe-482e-9394-60ad72bc7f90/image.png" alt=""></p>
<p>Permission Denied뜨면 .pem권한 바꿔주기
<img src="https://velog.velcdn.com/images/e_jink0/post/9ffe4f65-0fac-4926-90a5-f22d2274aa9b/image.png" alt=""></p>
<blockquote>
<p>% chmod 400 myKey_250523.pem
<img src="https://velog.velcdn.com/images/e_jink0/post/0d5f456a-1acd-47c9-aad8-b36a63465721/image.png" alt=""></p>
</blockquote>
<p>접속 성공
<img src="https://velog.velcdn.com/images/e_jink0/post/5df1550f-3e14-4344-aea4-a8b989fbccfc/image.png" alt=""></p>
<h1 id="3-탄력적-ip-설정">3. 탄력적 IP 설정</h1>
<p>인스턴스를 껐다가 키면 public IP가 변경된다. 이를 방지하기 위해 고정 IP를 받는것이다. 이 기능은 원래 무료였으나, 2024년 2월부터 과금 대상이 되었다.
<a href="https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/?utm_source=chatgpt.com">aws 공식지원 홈페이지</a>
<img src="https://velog.velcdn.com/images/e_jink0/post/88749f8e-8018-4233-8050-39dbbbf4bfda/image.png" alt="">
정말 슬프게도 계속 서비스를 돌릴 계획이면 한달에 약 5천원(0.005 USD × 24시간 × 30일 = 3.6 USD/월) 정도 나오는 셈이다. 무료티어인듯 무료티어 아닌 무료티어인 셈이다.</p>
<p>그래서 필자가 운영 중인 Wepin 서비스도 개발과 운영 두 개의 서버를 분리하여 사용하고 있었지만, 어느 순간부터 퍼블릭 IP 사용에 대한 과금이 발생하는 것을 확인하고, 운영 서버의 퍼블릭 IP는 과감히 해제하고 터널링 방식으로 전환하게 되었다.</p>
<p>그럼에도 불구하고 설정해보자</p>
<h2 id="3-1-ec2--네트워크-및-보안--탄력적-ip-할당-받기">3-1. EC2 &gt; 네트워크 및 보안 &gt; 탄력적 IP 할당 받기</h2>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/a73fb264-0c00-42c4-9e69-a6b738e551fc/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/44dd2880-12e0-4873-b0f3-09042a9e80cf/image.png" alt="">
전부 기본값으로 셋팅하고 할당받기</p>
<h2 id="3-2-탄력적-ip-연결하기">3-2. 탄력적 IP 연결하기</h2>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/94e60e09-a346-4ed1-99c4-1e4b7a7d4c72/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/e90a3acf-2eb4-4fc5-bd97-8b8be90fe6aa/image.png" alt="">
방금 생성한 인스턴스 선택하고 연결 </p>
<h1 id="4-배포-전-준비">4. 배포 전 준비</h1>
<h2 id="4-1-git-clone하기">4-1. git clone하기</h2>
<p>먼저 git설치하기</p>
<blockquote>
<p>$ sudo apt-get install git</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/3272c2d7-3794-4f7f-9926-2233c2cd1718/image.png" alt=""></p>
<p>ssh키 만들기</p>
<blockquote>
<p>$ ssh-keygen -t rsa -C github계정 메일(<a href="mailto:mygitgubid@github.com">mygitgubid@github.com</a>)
<img src="https://velog.velcdn.com/images/e_jink0/post/f98ab471-67a5-428a-a1a9-a6e331fe22ad/image.png" alt=""></p>
</blockquote>
<p>만든 id_rsa.pub을 cat 명령어로 확인 후 github의 setting에 추가하기</p>
<blockquote>
<p>$ cat id_rsa.pub</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/f9835597-b550-498e-8374-a5e3c02e8201/image.png" alt=""></p>
<p>그리고 git clone 을 해줍니다.</p>
<blockquote>
<p>$ git clone 레포지토리ssh주소</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/ce9c7c15-13b3-4bb6-b3f5-a3849c8a6f54/image.png" alt=""></p>
<h2 id="4-2-jdk다운로드-받기">4-2. jdk다운로드 받기</h2>
<p>jdk-17.0.1을 사용했기때문에 서버에도 똑같은 버전을 받아줍니다.</p>
<blockquote>
<h4 id="wget으로-다운로드">wget으로 다운로드</h4>
<p>$ wget <a href="https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1+12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz">https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1+12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz</a></p>
<h4 id="압축-해제">압축 해제</h4>
<p>$ tar -xvzf OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz</p>
<h4 id="usrlibjvm-폴더로-이동시키기">/usr/lib/jvm 폴더로 이동시키기</h4>
<p>$ sudo mv jdk-17.0.1+12 /usr/lib/jvm/jdk-17.0.1</p>
<h4 id="java-설정">java 설정</h4>
<p>sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-17.0.1/bin/java 1100</p>
<h4 id="java-버전-확인하기">java 버전 확인하기</h4>
<p>$ java -version
<img src="https://velog.velcdn.com/images/e_jink0/post/d3356586-ecde-4a62-bd03-879b842ffdc4/image.png" alt=""></p>
</blockquote>
<h2 id="4-3-applicationyml-파일-만들기">4-3. application.yml 파일 만들기</h2>
<p><code>application.yml</code> 와 같은 민감한 정보가 포함된 파일은 Git으로 버전 관리를 하지 않는게 일반적이다. 따라서 <code>application.yml</code> 파일은 별도로 EC2 인스턴스에 올려주어야 한다.
<code>src/main/resources/application.yml</code> 경로에 만들어주도록 하자.
<img src="https://velog.velcdn.com/images/e_jink0/post/d1658d04-7b3c-4c87-b18e-eade83bf2521/image.png" alt=""></p>
<h2 id="4-4-메모리-스왑-해주기">4-4. 메모리 스왑 해주기</h2>
<p>작고 소중한 서버의 Ram용량을 늘려주지않으면 빌드중 서버가 먹통이 될 수 있다. 1GB를 4GB로 늘려주자.</p>
<blockquote>
<p>$ sudo fallocate -l 4G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ sudo swapon --show</p>
</blockquote>
<p>위 명령어들을 차례대로 실행해주자
<img src="https://velog.velcdn.com/images/e_jink0/post/04d15b6a-8027-4372-b257-0ed97b5e8ee4/image.png" alt=""></p>
<p>정상적으로 스왑이 되었는지 확인</p>
<blockquote>
<p>$ free -m</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/33526d28-147c-48af-91d6-cd800f0ceb86/image.png" alt=""></p>
<p>혹시나 재부팅 할 경우가 생길수도 있으니 부팅 되었을때 자동으로 스왑 되도록 설정</p>
<blockquote>
<p>$ sudo vi /etc/fstab
맨 마지막줄에 아래 한줄 추가하고 저장
/swapfile swap swap defaults 0 0</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/6f8513cd-75ed-4756-8a2e-053a023a23ab/image.png" alt=""></p>
<h2 id="4-5-rds연동하기">4-5. RDS연동하기</h2>
<p>RDS검색해서 데이터베이스 생성하기
<img src="https://velog.velcdn.com/images/e_jink0/post/c2e4827f-6cdf-40de-86e0-f355ab938d78/image.png" alt=""></p>
<p>자신이 사용하는 DB선택하기
<img src="https://velog.velcdn.com/images/e_jink0/post/d9365c67-538c-4621-85d2-6963e76db9c9/image.png" alt=""></p>
<p>엔진 버전은 로컬MariaDB와 맞추면 좋겠지만 현재 로컬은 11.7.2이므로 제일 높은 버전으로 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/5eaf9ec6-6d0b-443d-a49b-4c490fc7e0d0/image.png" alt=""></p>
<p>db이름 적어주고 자체관리 체크
<img src="https://velog.velcdn.com/images/e_jink0/post/0e8cca0e-a09f-4e76-9532-036258a3d7cf/image.png" alt=""></p>
<p>암호 지정해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/3a7ffafe-8e93-41d7-81ae-ae3c9f26143c/image.png" alt=""></p>
<p>인스턴스 구성 기본값 해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/d2a9b8d0-019d-4ecc-b84b-d94022d62a22/image.png" alt=""></p>
<p>스토리지 기본값 해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/f54f182c-8d98-4bd5-a8bc-c3ddb3a6885a/image.png" alt=""></p>
<p>연결도 기본값으로 셋팅하고 퍼블릭 엑세스만 예 체크
<img src="https://velog.velcdn.com/images/e_jink0/post/20497712-8ec2-4ac6-adf0-6e6ff783c0d1/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/503a7b21-5f40-43cc-923a-1b53a24cb06d/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/3304870c-edb2-417b-aa50-b9ee3663c02d/image.png" alt=""></p>
<p>데이터베이스 인증, 모니터링 기본값 해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/a7fe15e6-086d-470a-ac66-2a6bbb23c6de/image.png" alt=""></p>
<p>생성후 DB전용 인바운드 규칙 만들기
ec2 &gt; 보안그룹 &gt; 보안그룹 생성
<img src="https://velog.velcdn.com/images/e_jink0/post/0450ebf1-abe1-4133-85be-cc08a5284037/image.png" alt=""></p>
<p>다시 RDS로 와서 DB인스턴스 수정을 클릭하고 3306 인바운드가 들어가있는 보안그룹 추가
<img src="https://velog.velcdn.com/images/e_jink0/post/2f5aaa25-6b6b-4d5b-86df-8e593ce09b97/image.png" alt=""></p>
<p>파라미터 그룹 추가
<img src="https://velog.velcdn.com/images/e_jink0/post/b75ac6d1-0b76-4d84-b41d-12562942cf10/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/06f01a9d-ef08-4f11-ac8f-11ec1f3afc54/image.png" alt=""></p>
<p>생성후 편집 클릭 &gt; character검색어에 쳐서 아래 속성들 utf8mb4로 설정</p>
<ul>
<li><code>character_set_client</code></li>
<li><code>character_set_connection</code></li>
<li><code>character_set_database</code></li>
<li><code>characater_set_filesystem</code></li>
<li><code>characater_set_results</code></li>
<li><code>character_set_server</code>
utf8mb4를 사용하는 이유는 ‘한글’ 뿐만 아니라 ‘이모티콘’도 지원이 가능하도록 하기 위해서다.
<img src="https://velog.velcdn.com/images/e_jink0/post/b4fe795a-22c5-4e6d-87a5-474fc0645ab0/image.png" alt=""></li>
</ul>
<p>collation검색어에 쳐서 아래 속성들 utf8mb4_unicode_ci로 설정</p>
<ul>
<li><code>collation_connection</code></li>
<li><code>collation_server</code>
utf8mb4_unicode_ci은 정렬, 비교 방식을 나타냅니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/351616ce-ce4f-4039-8c93-a812ea702cac/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/b29a4891-3002-4de3-af11-74afc4021f42/image.png" alt="">
timezone을 <code>Asia/Seoul</code>로 설정
<img src="https://velog.velcdn.com/images/e_jink0/post/bacddb5f-8572-45ab-8268-6a9158c60469/image.png" alt=""></li>
</ul>
<p>DB인스턴스 수정해서 파라미터 그룹을 방금 만든 그룹으로 설정
<img src="https://velog.velcdn.com/images/e_jink0/post/b4e95094-3099-47d5-8c8f-4fee1d077444/image.png" alt=""></p>
<p>DBeaver로 잘 연결되나 확인
<img src="https://velog.velcdn.com/images/e_jink0/post/5e9c026c-08fe-4cc2-b092-03b668f05267/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/23af40f4-0e46-44bc-8523-f430933ff7a9/image.png" alt=""></p>
<h2 id="4-6-ec2-인바운드-규칙-수정">4-6. EC2 인바운드 규칙 수정</h2>
<p>api 포트를 8080으로 했기때문에 인바운드 규칙에 8080을 추가
<img src="https://velog.velcdn.com/images/e_jink0/post/abf1f743-fcff-429c-a8ca-607e1a2d0375/image.png" alt="">
TCP, 8080, 0.0.0.0/0 (또는 허용된 IP 범위) 으로 추가해주기</p>
<h1 id="5-배포하기">5. 배포하기</h1>
<h2 id="5-1-build하기">5-1. build하기</h2>
<blockquote>
<p>$ ./gradlew clean build
$ cd build/libs/
$ java -jar anything-0.0.1-SNAPSHOT.jar</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/1fdab765-1d26-4fc6-bbda-55964917b5df/image.png" alt=""></p>
<h2 id="5-1-접속해보기">5-1. 접속해보기</h2>
<p>https는 아직 설정을 안해주었기때문에 http로 접속해야한다.
<img src="https://velog.velcdn.com/images/e_jink0/post/916cc309-659e-4575-83bc-51152fc02fd2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] Too many connections... 원인 분석 And 해결 (Feat. 순환참조)]]></title>
            <link>https://velog.io/@e_jink0/JPA-Too-many-connections...-%EC%9B%90%EC%9D%B8-%EB%B6%84%EC%84%9D-And-%ED%95%B4%EA%B2%B0-Feat.-%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9</link>
            <guid>https://velog.io/@e_jink0/JPA-Too-many-connections...-%EC%9B%90%EC%9D%B8-%EB%B6%84%EC%84%9D-And-%ED%95%B4%EA%B2%B0-Feat.-%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9</guid>
            <pubDate>Thu, 22 May 2025 13:35:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/61fc466b-5b26-4de3-ae22-a75e24710bb8/image.png" alt=""></p>
<h3 id="서론">서론</h3>
<p>master 브랜치에서는 정상 작동하던 API가 develop 브랜치에서는 오류를 뿜어낸다는 점이었습니다.
처음에는 어디서 문제가 발생했는지 전혀 감을 잡지 못했는데, 그 이유는 바로 JDK 버전이 11에서 17로 업그레이드되며 대대적인 변화가 있었기 때문입니다.
Deprecated된 코드들도 전부 교체되었고, 이로 인해 변경된 코드의 양이 너무 많아 단순히 diff만으로는 원인을 찾기 어려운 상황이었습니다.
그래서 &quot;일단 이 버그부터 해결해보자!&quot;는 마음으로 파고들었고, 결국 원인을 찾아 해결까지 해냈습니다! 😂
<strong>yml파일에 connections 수를 늘려보아도 해결이 되지 않았습니다. 결과적으로는 코드내에 버그가 원인이었습니다.</strong></p>
<h3 id="1-원인-불필요한-db-커넥션-증가">1. 원인: 불필요한 DB 커넥션 증가</h3>
<p>처음에는 다른 누군가가 계속... 우리 개발DB에 연결 시도를 하는걸까?? 라는 생각을 했지만 그건 아니었습니다.ㅎ
알고 보니 특정 객체에서 너무 많은 커넥션을 소모하고 있었습니다. </p>
<p>그 중 하나는 바로 <code>@Data</code> 어노테이션이었습니다.</p>
<blockquote>
<p><code>@Data</code>는 <code>@ToString()</code>도 포함하기 때문에 연관된 객체를 계속 불러오면서 예상보다 많은 쿼리가 실행되는 문제가 있었습니다.  </p>
</blockquote>
<p>특히 <code>Feed</code> ↔ <code>Pin</code> 간 양방향 연관관계에서 순환 참조로 인해 디버깅도 느려졌고, 예상치 못한 DB 접근이 생겼습니다.
<del>이 부분을 제가 단번에 캐치해내지 못했었죠..!</del> 🤨</p>
<ol>
<li>@ToString 같은 메서드에서 무한 루프 발생
Feed.toString() → Pin.toString() → 다시 Feed.toString()...</li>
<li>디버깅 시 불필요한 필드 조회로 지연 로딩 발생 가능성 매우 큼</li>
</ol>
<p>-&gt; 여기서 엄~청 느린 디버깅 속도를 체감하였습니다. 한줄 내려갈때 약 15초 정도 걸린거같습니다.
<strong>➡ 해결:</strong> <code>@Data</code> 대신 <code>@Getter</code>, <code>@Setter</code>를 명시적으로 사용하고, <code>@ToString</code>은 필요한 곳에만 명시적으로 적용을 하였습니다.</p>
<blockquote>
<p>결과적으로 @Data → @ToString() 포함됨 → 불필요한 DB 접근 + 순환참조 -&gt; 지연 로딩 → 성능 저하 또는 예외 발생 Too many Connections........ 로그를 찍게 된 것이었습니다.</p>
</blockquote>
<p>이 내용은 실제로 Lombok 공식 홈페이지에 기재되어있는걸 확인하실수 있습니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/7f3e4df5-4e4e-4b98-8566-926da1ac61fb/image.png" alt=""></p>
<h3 id="2-원인-연관관계-필드-설정-누락">2. 원인: 연관관계 필드 설정 누락</h3>
<p><code>Feed</code> 객체 내부에 <code>setPinList()</code>로 데이터를 주입하는 과정에서 각 <code>Pin</code> 객체에 <code>Feed</code>를 역으로 설정하지 않아 <code>feed_id</code>가 <code>null</code>로 남아 있었습니다.<br>해당 컬럼은 <code>NOT NULL</code> 제약 조건이 있었기 때문에, JPA가 이를 채우기 위해 지속적으로 DB 접근을 시도했던 것으로 추정됩니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/ea1f5fa5-c80d-425a-a671-85ef7ea2319b/image.png" alt="">
저 에러로그가 API호출 할때마다 계속 발생했었죠. 😤 어디서 계속 null값을 넣고있을까...</p>
<p>❗ 약식으로 오류가 발생했던 객체의 구조를 명시해보면</p>
<pre><code>Feed {
    ...
    Pin: {
        ...
        FeedID: xxx  &lt;- 이게 Not null
    }
    ...
}</code></pre><p>요런 형식이 됩니다. 여기서 feedService.createFeed()을 호출을 하고
pinService.createPin(pinList, feed)를 호출하게 되는데 이때 
PIN객체를 구성하는 코드에서
pin.setFeedId(feed.id) &lt;- 이 코드가 없었던 것이었죠!!</p>
<p>즉, PIN 테이블에 insert 하려는 시점에 feedId가 null이라서 DB에서 NOT NULL 오류가 발생한겁니다.
참 간단한 내용이었지만.. 이 Feed_Id column을 많은 테이블에서 참조하고 있었던것이 문제였네요.</p>
<p><strong>➡ 해결:</strong> <code>pin.setFeedId(feed.id)</code> 호출을 추가하여 연관관계를 명확히 하였고 결과적으로는 Too many Connection...에서 벗어나게 되었습니다. 👏</p>
<hr>
<h3 id="📌-교훈">📌 교훈</h3>
<ul>
<li><code>@Data</code>는 편하지만, 연관관계가 많은 엔티티에는 피하자.</li>
<li><code>@ToString</code>은 조심해서 사용하고, 지연 로딩(LAZY) 필드는 절대 포함하지 말자.</li>
<li>연관관계 설정은 양방향일 경우 <strong>양쪽 모두</strong> 정확히 설정해야 한다.</li>
<li><code>Too many connections</code>는 커넥션 풀 설정 문제가 아닌 <strong>코드의 잘못된 DB 접근</strong>일 수 있다.</li>
</ul>
<h3 id="📚-참고사이트">📚 참고사이트</h3>
<p><a href="https://projectlombok.org/features/Data">Lombok 공식사이트</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제 - 프로세스와 스레드, 컨텍스트 스위칭, 동기화,  데드락, 스레드풀 정리]]></title>
            <link>https://velog.io/@e_jink0/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%99%80-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%9C%84%EC%B9%AD-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%8D%B0%EB%93%9C%EB%9D%BD-%EC%8A%A4%EB%A0%88%EB%93%9C%ED%92%80-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@e_jink0/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%99%80-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%9C%84%EC%B9%AD-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%8D%B0%EB%93%9C%EB%9D%BD-%EC%8A%A4%EB%A0%88%EB%93%9C%ED%92%80-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 19 May 2025 14:05:36 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-프로세스와-스레드의-차이는">1️⃣ 프로세스와 스레드의 차이는?</h2>
<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>프로세스 (Process)</th>
      <th>스레드 (Thread)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>정의</td>
      <td>실행 중인 프로그램</td>
      <td>프로세스 내 작업 단위</td>
    </tr>
    <tr>
      <td>메모리</td>
      <td>독립된 메모리 공간 사용</td>
      <td>프로세스의 메모리 공유</td>
    </tr>
    <tr>
      <td>생성 비용</td>
      <td>상대적으로 큼 (자원 별도 필요)</td>
      <td>가벼움 (자원 공유)</td>
    </tr>
    <tr>
      <td>안정성</td>
      <td>한 프로세스 종료돼도 다른 프로세스에 영향 없음</td>
      <td>한 스레드 오류 시 전체 프로세스 영향 가능성 있음</td>
    </tr>
    <tr>
      <td>예시</td>
      <td>크롬, VSCode, Discord 등 실행 프로그램</td>
      <td>크롬에서 여러 탭을 동시에 여는 구조</td>
    </tr>
  </tbody>
</table>

<blockquote>
<p>💡 스레드는 프로세스의 작업 단위로, 여러 스레드가 하나의 프로세스 내에서 CPU를 공유하면서 동시에 실행될 수 있습니다.</p>
</blockquote>
<h2 id="2️⃣-컨텍스트-스위칭이란">2️⃣ 컨텍스트 스위칭이란?</h2>
<p>컨텍스트 스위칭(Context Switching)은 운영체제가 CPU를 A 스레드에서 B 스레드로 전환할 때 발생하는 상태 저장/복원 작업입니다.
스레드마다 레지스터, 프로그램 카운터 등의 상태(컨텍스트)를 가짐
A 스레드의 상태를 저장하고 B 스레드의 상태를 불러오는 과정
이 과정에서 CPU 리소스를 소비하므로 너무 잦으면 오버헤드 발생</p>
<blockquote>
<p>💡 멀티태스킹이 가능하게 해주는 기술이지만, 너무 잦으면 성능 저하의 원인이 됩니다.</p>
</blockquote>
<h2 id="3️⃣-멀티스레드-환경에서-동기화는-왜-필요한가">3️⃣ 멀티스레드 환경에서 동기화는 왜 필요한가?</h2>
<p>멀티스레드 환경에서는 여러 스레드가 공유 자원(예: 전역 변수, 파일, DB)에 동시에 접근할 수 있기 때문에,
데이터 충돌이나 상태 불일치(race condition)가 발생할 수 있습니다.</p>
<pre><code>class ThreadEx21 {
    public static void main(String args[]) {
        Runnable r = new RunnableEx21();
        new Thread(r).start();
        new Thread(r).start();
    }
}

class Account {
    private int balance = 1000;

    public  int getBalance() {
        return balance;
    }

    public void calc(int data){
        if(balance &gt;= data) {
            try { Thread.sleep(1000);} catch(InterruptedException e) {}
            balance -= data;
        }
    }
}

class RunnableEx21 implements Runnable {
    Account acc = new Account();

    public void run() {
        while(acc.getBalance() &gt; 0) {
            int randomData = (int)(Math.random() * 3 + 1) * 100;
            acc.calc(randomData);
            System.out.println(&quot;balance:&quot;+acc.getBalance());
        }
    } // run()
}

실행 결과:
balance:800
balance:700
balance:500
balance:500
balance:200
balance:0
balance:-200</code></pre><p>위 코드를 실행하면 balance값이 음수가 된다. 
한 쓰레드가 calc()의 if문에서 조건식을 만족해서 data를 뺀순간, 다른 스레드에게 제어권이 넘어가서 다른 쓰레드가 또 if문의 조건식을 만족해서 data를 빼버리는 순간이 온다.
그래서 결국 balance값은 음수가 되서 프로그램이 종료된다.
-&gt; calc() 함수에 synchronized키워드를 붙여서 한 번에 하나의 스레드만 임계 영역을 실행할 수 있게 만듭니다.</p>
<blockquote>
<p>임계 영역(Critical Section)은 둘 이상의 스레드가 동시에 접근할 경우 문제를 일으킬 수 있는 코드 구간입니다.</p>
</blockquote>
<pre><code>...
public synchronized void calc(int data){
    if(balance &gt;= data) {
        try { Thread.sleep(1000);} catch(InterruptedException e) {}
        balance -= data;
    }
}
...

실행결과
balance:900
balance:700
balance:400
balance:200
balance:100
balance:100
balance:100
balance:100
balance:100
balance:100
balance:100
balance:0
balance:0</code></pre><p>이렇게 해줌으로써, calc()가 호출되면 calc()가 종료되어 lock이 반납될때까지 다른 쓰레드는 calc()를 호출하더라도 대기 상태에 머무르게 된다.</p>
<blockquote>
<p>💡 동기화는 여러 스레드가 동시에 공유 자원에 접근할 때 데이터 정합성을 보장하기 위해 필요합니다.</p>
</blockquote>
<h2 id="4️⃣-데드락deadlock이란-방지-방법은">4️⃣ 데드락(Deadlock)이란? 방지 방법은?</h2>
<p>데드락(Deadlock)은 두 개 이상의 스레드가 서로가 가진 자원을 기다리며 무한 대기 상태에 빠지는 현상입니다.</p>
<p>예시:</p>
<pre><code>class Shared {
    synchronized void methodA(Shared other) {
        System.out.println(Thread.currentThread().getName() + &quot; calls methodA&quot;);
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        other.methodB(this);  // 여기서 데드락 가능
    }

    synchronized void methodB(Shared other) {
        System.out.println(Thread.currentThread().getName() + &quot; calls methodB&quot;);
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        other.methodA(this);  // 여기서 데드락 가능
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        Shared s1 = new Shared();
        Shared s2 = new Shared();

        new Thread(() -&gt; s1.methodA(s2), &quot;Thread-1&quot;).start();
        new Thread(() -&gt; s2.methodB(s1), &quot;Thread-2&quot;).start();
    }
}</code></pre><p>→ 서로가 서로를 기다리며 무한 대기 → 데드락 발생</p>
<p>✅ 데드락 발생 4가지 조건 (전부 충족 시 발생)</p>
<blockquote>
<p><strong>상호 배제(Mutual Exclusion)</strong>
– 자원은 한 번에 하나의 스레드만 사용 가능
<strong>점유 대기(Hold and Wait)</strong>
– 자원을 점유한 상태에서 다른 자원을 기다림
<strong>비선점(No Preemption)</strong>
– 자원을 강제로 뺏을 수 없음
<strong>환형 대기(Circular Wait)</strong>
– 자원을 서로 물고 물고 있는 순환 대기 상태</p>
</blockquote>
<p>방지 방법</p>
<ol>
<li><p>자원 획득 순서를 일관되게 정함</p>
<pre><code>class Shared {
 void methodA(Shared other) {
     synchronized (this) {
         System.out.println(Thread.currentThread().getName() + &quot; acquired lock on this&quot;);
         try { Thread.sleep(100); } catch (InterruptedException e) {}

         synchronized (other) {
             System.out.println(Thread.currentThread().getName() + &quot; acquired lock on other&quot;);
         }
     }
 }

 void methodB(Shared other) {
     // methodA와 동일한 순서로 락 획득
     synchronized (this) {
         System.out.println(Thread.currentThread().getName() + &quot; acquired lock on this&quot;);
         try { Thread.sleep(100); } catch (InterruptedException e) {}

         synchronized (other) {
             System.out.println(Thread.currentThread().getName() + &quot; acquired lock on other&quot;);
         }
     }
 }
}
</code></pre></li>
</ol>
<pre><code>2. 타임아웃 설정</code></pre><p>Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();</p>
<p>if (lock1.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lock2.tryLock(1, TimeUnit.SECONDS)) {
            try {
                // 작업
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}</p>
<p>```</p>
<blockquote>
<p>💡 데드락은 리소스를 차례로 요청하다가 서로 물고 물려서 아무 것도 못하는 상태로, 이를 방지하려면 자원 접근 순서나 타임아웃 설정이 중요합니다.</p>
</blockquote>
<h2>📚 참고 링크</h2>
<ul>
  <li><a href="https://cafe.naver.com/javachobostudy" target="_blank">남궁성의 코드초보스터디</a></li>
</ul>]]></description>
        </item>
        <item>
            <title><![CDATA[Zustand란? Redux보다 더 쉬운 전역 상태 관리 라이브러리]]></title>
            <link>https://velog.io/@e_jink0/Zustand%EB%9E%80-Redux%EB%B3%B4%EB%8B%A4-%EB%8D%94-%EC%89%AC%EC%9A%B4-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</link>
            <guid>https://velog.io/@e_jink0/Zustand%EB%9E%80-Redux%EB%B3%B4%EB%8B%A4-%EB%8D%94-%EC%89%AC%EC%9A%B4-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</guid>
            <pubDate>Fri, 16 May 2025 01:54:31 GMT</pubDate>
            <description><![CDATA[<h2>✅ 개요</h2>
<p>
React에서 상태 관리는 언제나 중요한 고민거리다.<br />
그동안 계속 <strong>Redux</strong>를 사용해왔지만,
복잡한 보일러플레이트와 러닝커브 때문에 다른 대안을 찾기도 합니다.
</p>
<p>
오늘은 그런 대안 중 하나인 <strong>Zustand</strong>에 대해 소개하고, 
왜 Zustand가 요즘 주목받고 있는지, Redux와 비교해서 어떤 점이 더 좋은지 알아보자.
</p>

<h2>🌿 Zustand란?</h2>
<p>
Zustand(독일어로 "상태"라는 뜻)는 
<a href="https://github.com/pmndrs" target="_blank">Poimandres</a> 팀에서 만든 
<strong>작고 빠르며 직관적인 상태 관리 라이브러리</strong>이다.
</p>
<ul>
  <li>React 전용 상태 관리 라이브러리</li>
  <li>Context API 없이도 전역 상태를 관리할 수 있음</li>
  <li>Redux보다 훨씬 간단한 문법과 사용법</li>
</ul>

<h2>🤔 왜 Zustand를 사용할까?</h2>
<h3>1. 간단한 사용법</h3>
<p>Redux처럼 action, reducer, dispatch 같은 복잡한 구조가 필요 없다.<br />
<strong>단일 파일로 상태 관리 가능</strong>하며, 코드 양도 매우 적다.</p>

<h3>2. Provider 불필요</h3>
<p>Context 없이도 전역 상태를 사용할 수 있어서 코드가 더 간결해진다.</p>

<h3>3. 불필요한 리렌더링 최소화</h3>
<p>필요한 상태만 구독할 수 있기 때문에 <strong>성능 최적화</strong>가 자연스럽게 된다.</p>

<h3>4. 중간 크기 프로젝트에 최적</h3>
<p>복잡한 아키텍처 없이도 빠르게 상태 공유가 가능하므로 <strong>중소 규모 프로젝트</strong>에 특히 적합하다.</p>

<blockquote>
<p>실제로 회사에서 앱 소개용 웹페이지를 소규모로 구축하게 되면서 프론트엔드 개발을 담당하게 되었다. 무료 이용권 등록, 앱스토어 이동 등 비교적 단순한 기능 위주였지만, UI 상의 상태 관리가 필요한 부분이 있어 Zustand를 도입해 실무에 적용해보았다. 🤓</p>
</blockquote>
<h2>🆚 Redux vs Zustand 비교</h2>
<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Redux</th>
      <th>Zustand</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>사용 복잡도</td>
      <td>높음 (action, reducer, dispatch 필요)</td>
      <td>낮음 (create 한 줄로 끝)</td>
    </tr>
    <tr>
      <td>보일러플레이트</td>
      <td>많음</td>
      <td>거의 없음</td>
    </tr>
    <tr>
      <td>Provider 필요 여부</td>
      <td>필수</td>
      <td>불필요</td>
    </tr>
    <tr>
      <td>러닝 커브</td>
      <td>높음</td>
      <td>매우 낮음</td>
    </tr>
    <tr>
      <td>Devtools 지원</td>
      <td>잘됨</td>
      <td>지원됨 (옵션 설정 필요)</td>
    </tr>
    <tr>
      <td>미들웨어</td>
      <td>공식적으로 제공</td>
      <td>조합 가능 (persist, devtools 등)</td>
    </tr>
  </tbody>
</table>

<h2>🛠 Zustand 기본 예제</h2>

<pre><code>// store.js
import { create } from &#39;zustand&#39;;

const useCounterStore = create((set) =&gt; ({
  count: 0,
  increase: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),
  decrease: () =&gt; set((state) =&gt; ({ count: state.count - 1 })),
}));

export default useCounterStore;</code></pre><pre><code>// App.jsx
import useCounterStore from &#39;./store&#39;;

export default function App() {
  const { count, increase, decrease } = useCounterStore();

  return (
    &lt;div&gt;
      &lt;h1&gt;Count: {count}&lt;/h1&gt;
      &lt;button onClick={increase}&gt;+1&lt;/button&gt;
      &lt;button onClick={decrease}&gt;-1&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre><h2>💾 persist 미들웨어로 상태 유지하기</h2>
<p>Zustand는 <code>persist</code> 미들웨어를 통해 <strong>localStorage에 상태를 저장</strong>할 수 있다.</p>


<pre><code>import { create } from &#39;zustand&#39;;
import { persist } from &#39;zustand/middleware&#39;;

const useStore = create(
  persist(
    (set) =&gt; ({
      token: &#39;&#39;,
      setToken: (token) =&gt; set({ token }),
    }),
    {
      name: &#39;auth-storage&#39;, // localStorage key 이름
    }
  )
);</code></pre><h2>🔍 Devtools 연결도 가능</h2>

<pre><code>import { create } from &#39;zustand&#39;;
import { devtools } from &#39;zustand/middleware&#39;;

const useStore = create(
  devtools((set) =&gt; ({
    count: 0,
    increase: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),
  }), { name: &#39;CounterStore&#39; })
);</code></pre><h2>✅ 마무리</h2>
<p>
Zustand는 복잡한 상태 관리 없이도 충분히 강력한 전역 상태를 구성할 수 있게 해줍니다.<br />
<strong>Redux가 너무 무겁거나, 작은 프로젝트에서 빠르게 전역 상태를 다뤄야 할 때</strong>  
Zustand는 매우 훌륭한 선택이다.
</p>


<h2>📚 참고 링크</h2>
<ul>
  <li><a href="https://docs.pmnd.rs/zustand/getting-started/introduction" target="_blank">Zustand 공식 문서</a></li>
  <li><a href="https://github.com/pmndrs/zustand" target="_blank">Zustand GitHub</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[github student 인증 + intellij ultimate 무료 사용]]></title>
            <link>https://velog.io/@e_jink0/github-student-%EC%9D%B8%EC%A6%9D-intellij-ultimate-%EB%AC%B4%EB%A3%8C-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@e_jink0/github-student-%EC%9D%B8%EC%A6%9D-intellij-ultimate-%EB%AC%B4%EB%A3%8C-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Tue, 12 Nov 2024 00:08:14 GMT</pubDate>
            <description><![CDATA[<h1 id="1github에-대학교-email-등록하기">1.github에 대학교 email 등록하기</h1>
<ul>
<li>github 로그인 &gt; settings &gt; email 탭으로 이동해서 학생 email등록하기
<img src="https://velog.velcdn.com/images/e_jink0/post/71ec5b5f-719a-4f93-8525-fba3b1893d32/image.png" alt=""></li>
</ul>
<h1 id="2-education에-학생신분-증명서-업로드하기">2. education에 학생신분 증명서 업로드하기</h1>
<ul>
<li><a href="https://education.github.com/discount_requests/application">https://education.github.com/discount_requests/application</a>
위 링크에 접속해서 학생신분 증명</li>
<li>캡처하지는 못했지만
사진해상도가 1024x960 이하여야 하고 jpeg 또는 png여야하고, 크기가 1mb 이하여야 함 😇</li>
<li>나는 무려 5트만에 성공함 <del>4번실패</del>
<img src="https://velog.velcdn.com/images/e_jink0/post/1df11104-0a47-4f1d-8d7e-7853fdee99a5/image.png" alt=""></li>
</ul>
<h1 id="3-사진이-업로드되면-2일정도-기다리기">3. 사진이 업로드되면 2일정도 기다리기..</h1>
<ul>
<li>그로부터 약 2일후에 승인메일이 오고 승인메일 오면 상태가 이렇게 바뀜
<img src="https://velog.velcdn.com/images/e_jink0/post/49ff5557-33d7-4826-82ba-713e77eb5380/image.png" alt=""></li>
<li>내 레포지토리 메인에도 pro 뱃지가 생김 ㄲ ㅑ
<img src="https://velog.velcdn.com/images/e_jink0/post/6e9525c5-f1e7-439d-9e83-d087d36428dd/image.png" alt=""></li>
</ul>
<h1 id="4-intellij에-로그인해서-승인코드-받기">4. intellij에 로그인해서 승인코드 받기</h1>
<ul>
<li>github로 로그인하고 학생인증을 클릭함 (캡쳐를 못했어유)
<img src="https://velog.velcdn.com/images/e_jink0/post/3efb1124-d339-497f-aa5f-3c89809803d0/image.png" alt="">
그럼 이런 이메일을 받고 인증을 하고 ...
<img src="https://velog.velcdn.com/images/e_jink0/post/74d8d7bb-5b95-46b9-9a72-cf62c95b645d/image.png" alt="">
인증 코드를 겓또..... ㄲ ㅑ </li>
</ul>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/c6207ed7-2bbf-419b-8276-d47576fb22a5/image.png" alt="">
잘 쓰겟 읍니다 ! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apple push notification service (APNs) key 설정 + firebase]]></title>
            <link>https://velog.io/@e_jink0/Apple-push-notification-service-APNs-key-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@e_jink0/Apple-push-notification-service-APNs-key-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 05 Nov 2024 00:23:27 GMT</pubDate>
            <description><![CDATA[<p>APNs란?
APNs는 Apple push notification service의 줄임말로, iOS에 표시되는 푸시알림 기능이다. 애플 개발자 페이지에서 인증서와 키를 설정해야만 사용할 수 있다.</p>
<p>Apple developer사이트에 앱을 등록했다는 가정하에 진행하도록 하겠다.</p>
<h1 id="1-apns-인증서-발급하기">1. APNs 인증서 발급하기</h1>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/161cde37-4fe1-4022-889d-998a8243c501/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/beab2839-7d4e-4262-9ea7-4fa3e8653ebf/image.png" alt="">
Services의 Apple Push Notification service SSL을 선택
<img src="https://velog.velcdn.com/images/e_jink0/post/59f27df5-f34a-49b5-8e9c-0cecdd74b283/image.png" alt="">
앱 ID 선택하기
<img src="https://velog.velcdn.com/images/e_jink0/post/96113b88-c5a5-4dbd-abe2-dd3c255433b2/image.png" alt="">
키체인 접근 프로그램 열기
<img src="https://velog.velcdn.com/images/e_jink0/post/b6610623-0f59-48af-8c1b-adf85cf725c1/image.png" alt="">
인증 기관에서 인증서 요청 클릭해서 진행하기
(dev, prod 나뉘어져있으면 두번 발급받아야함)
<img src="https://velog.velcdn.com/images/e_jink0/post/60c57065-93d1-4429-85b1-6eec78c2153b/image.png" alt="">
사용자 이메일주소 : apple 계정
일반 이름: 본인 앱 이름 적기 (이름_dev, 이름_prod)
CA 이메일은 안적어도되고 디스크에 저장됨 으로 선택하면 다운로드 받아짐
<img src="https://velog.velcdn.com/images/e_jink0/post/2c58f519-71d7-4568-85a8-966a92c7f437/image.png" alt="">
이런식으로 요청키가 받아진다
<img src="https://velog.velcdn.com/images/e_jink0/post/d5030693-c381-4a9d-94ae-9071447eb0b0/image.png" alt="">
위 페이지에 요청키를 업로드한다
<img src="https://velog.velcdn.com/images/e_jink0/post/f2019588-ddb8-4e0c-a121-a667d487ff6e/image.png" alt="">
그러면 APNs 인증서가 생성되었다. Download를 눌러서 파일로 저장하자.
<img src="https://velog.velcdn.com/images/e_jink0/post/3f45395e-42ce-49cd-ba12-35df6d1244bc/image.png" alt=""></p>
<p>Certificates, Identifiers &amp; Profiles &gt; keys로 넘어가서 새로운 키 발급받기
<img src="https://velog.velcdn.com/images/e_jink0/post/b1be6b54-96e1-41c5-8ad4-392d28b8c50b/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/b3bad7d7-29af-44a0-aa90-7393d86d203b/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/0bc5ae5b-49ec-41dc-8519-ae58becd9661/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/1b7b4cd9-079e-419c-9992-b8db8469a0d6/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/3e1000ab-9c2d-4f05-b6ec-417ace30aaef/image.png" alt="">
다운로드 받아서 .p8 파일 받기
<img src="https://velog.velcdn.com/images/e_jink0/post/93e17617-2112-4b79-9ec3-7eeb503bea50/image.png" alt=""></p>
<p>Identifiers &gt; 앱 이름클릭 &gt; push Notifications edit 눌러서 SSL 키 설정해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/fcd68b76-0c9a-44c9-83bf-2b5310b53f3e/image.png" alt=""><img src="https://velog.velcdn.com/images/e_jink0/post/7bbfc5a3-def6-4788-b6ff-ba652a4ad549/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/ad6e8207-63f3-46f0-9b81-ce700f663b8c/image.png" alt="">
아까 다운받은 cer파일 업로드하기 (dev, prod 각각 업로드하기)</p>
<h1 id="2-xcode-설정해주기">2. xcode 설정해주기</h1>
<p>xcode &gt; project name &gt; target &gt; signing &amp; Capabilites 선택하고 +Capability 해서 Push Notification, Background Modes 추가해주기
<img src="https://velog.velcdn.com/images/e_jink0/post/7bb42190-50c7-4a8d-a6a8-4670ff1cdc0a/image.png" alt="">
Background Modes 에 Background fetch, Remote notification 체크해주기</p>
<h1 id="3-firebase-console-에서-설정하기">3. firebase console 에서 설정하기</h1>
<p>firebase 콘솔 &gt; 프로젝트 설정 &gt; 클라우드 메시징 탭 &gt; Apple 앱 구성에 인증키(.p8) 업로드하기
<img src="https://velog.velcdn.com/images/e_jink0/post/81099047-912e-4db0-8ba9-30a6fca02bae/image.png" alt=""></p>
<h1 id="4-코드-추가해서-메시징-처리가-잘-되는지-확인해보기">4. 코드 추가해서 메시징 처리가 잘 되는지 확인해보기</h1>
<ul>
<li><p>ios badge 카운트 처리시 사용한 라이브러리는 &#39;notifee&#39; 라는 라이브러리를 사용함</p>
</li>
<li><p>App.tsx</p>
<ul>
<li><p>foreground처리부</p>
<pre><code>const unsubscribe = messaging().onMessage(async remoteMessage =&gt; {
//console.log(&#39;get message!!!&#39;);
Alert.alert(&#39;A new FCM message arrived!&#39;, JSON.stringify(remoteMessage));
const channelId = await createChannel();

await notifee.displayNotification({
  title: remoteMessage.notification?.title,
  body: remoteMessage.notification?.body,
  android: {
      channelId: channelId,
  },
});
});</code></pre></li>
</ul>
</li>
<li><p>index.js</p>
<ul>
<li>background 처리부<pre><code>messaging().setBackgroundMessageHandler(async remoteMessage =&gt; {
await notifee.incrementBadgeCount();
const count = await notifee.getBadgeCount();
console.log(&#39;Badge count incremented by 1 to: &#39;, count);
});</code></pre></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/92fe060d-52d3-4c8a-b561-172ffdb84120/image.PNG" alt="">
foreground</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/95716351-08c9-4801-9c79-5842c0e20dcc/image.PNG" alt="">
background</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RN Webview에 localhost띄우기]]></title>
            <link>https://velog.io/@e_jink0/React-Native-Webview%EC%97%90-localhost%EB%9D%84%EC%9A%B0%EA%B8%B0</link>
            <guid>https://velog.io/@e_jink0/React-Native-Webview%EC%97%90-localhost%EB%9D%84%EC%9A%B0%EA%B8%B0</guid>
            <pubDate>Fri, 25 Oct 2024 09:44:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/8ff4726c-be94-4360-ada1-5768f3fa0b09/image.png" alt=""></p>
<p>보통 웹을 RN으로 말아서 앱으로 만들경우, 해당 webView에 설정된 uri의 인증서 이슈가 조금이라도 있으면 해당 Url로 절대 들여보내주지 않는다.
그렇지만 웹은 신뢰할 수 없는 페이지 입니다 ! 라고 하면서 들여는 보내주는걸 간간히 봤을것이다.</p>
<p>당연한거 아닌가? 라고 생각할지 모르겠지만 !! 
개발자들은 당연한게 아니다 !! 왜냐면 RN통신부를 작업할때는 RN에 로컬호스트를 띄워야 좀더 편한 테스트가 가능하기 때문이다.</p>
<p>일단 필자의 개발 환경이다.
웹뷰: jsp, os: macos, simulator: iphone 16 pro</p>
<p>차근차근 실행했던 단계별로 나열해보도록 하겠다.</p>
<ol>
<li>http를 https 로 바꿔주기
이것은 말그대로 로컬환경의 인증서를 만들어서 해당 인증서를 가지고 로컬실행을 하는것이다.</li>
</ol>
<ul>
<li>mkcert, nss 깔기<blockquote>
<p>$ brew install mkcert
$ brew install nss</p>
</blockquote>
</li>
</ul>
<p><del>brew가 없다면 깔아주도록 하자.</del></p>
<ul>
<li><p>mkcert 환경 등록해주기</p>
<blockquote>
<p>$sudo mkcert -install</p>
</blockquote>
</li>
<li><p>인증서 발급받기
Java에서는 PKCS 형식의 인증키, 공개키가 하나로 묶인 형식을 이용하기때문에 .p12로 생성</p>
<blockquote>
<p>$ mkcert -pkcs12 -p12-file keystore.p12 localhost 127.0.0.1 &quot;*.myexample.domain&quot;</p>
</blockquote>
</li>
</ul>
<p>이렇게 하면 
<img src="https://velog.velcdn.com/images/e_jink0/post/f4645ccc-b78d-4a96-959b-b302810697b7/image.png" alt="">
이런식으로 만들어졌다고 뜨고 기본 password는 &#39;changeit&#39;이다.</p>
<ul>
<li><p>해당 인증서를 프로젝트에 추가하기
필자의 프로젝트는 springboot/jsp 형태였어서 myapp-local.yml 파일에 추가해주었다.
추가 예시</p>
<pre><code>server:
ssl:
  enabled: true
  key-store: ${user.home}/keystore.p12
  key-store-type: PKCS12
  key-store-password: changeit</code></pre></li>
<li><p>app코드에 uri셋팅하기</p>
<pre><code>&lt;WebView
  source={&#39;https://myip:8083&#39;}
/&gt;</code></pre><p>ip는 public ip 로 집어넣기
wifi 설정 &gt; 현재 연결된 wifi 세부사항에 들어가면 현재 public ip를 알수있다.</p>
</li>
</ul>
<p>그렇게 하면 일단 오류메시지만 덩그러니 있는 페이지가 뜬다.
<img src="https://velog.velcdn.com/images/e_jink0/post/cde6e895-2dde-4c4a-a15f-309a1c43706d/image.png" alt=""></p>
<ul>
<li>node_module/react-native-webview 다이렉트로 수정해버리기
저거는 인증서가 유효하지 않다 라는것이고.. 
나는 단지.. 로컬로 띄워서 RN메시징을 테스트 해보고싶었을 뿐이고...
로컬환경에서 인증서도 만들었고....
<del>흑흑흑흑 그냥 무시 해주면 안돼 ??? 외않되?</del></li>
</ul>
<p>그래서 아예 ssl유효 인증을 구분하는 코드를 바꿔버렸다. ㅎㅎ
react-native-webview 라이브러리는 버전마다 코드가 바뀔수도 있으니
필자가 수정한것을 토대로 코드를 찾아서 직접 수정하길 바란다.. ! 굳럭 ㅠ</p>
<ul>
<li>RNCWebViewImpl.m 파일 수정하기</li>
<li><em>didFailProvisionalNavigation*</em>를 전체검색해서 그안의 내부를 다 주석처리 해버리자.
이 코드들은 에러가 나타나면 화면에 흰화면+에러로그를 출력해주는 코드들이다.
<img src="https://velog.velcdn.com/images/e_jink0/post/063d68b6-bc3c-49ec-9f32-d8ea017e2460/image.png" alt=""></li>
</ul>
<p><strong>didReceiveAuthenticationChallenge</strong>를 전체검색해서 내부를 바꾸자 </p>
<pre><code>if ([[challenge protectionSpace] serverTrust] != nil &amp;&amp; customCertificatesForHost != nil &amp;&amp; host != nil) {
...
if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodHTTPBasic) {
...</code></pre><p>저 위의 두개의 if문을 전체 주석처리 해버리자. 그리고 아래의 코드를 집어넣기</p>
<pre><code>// 모든 서버 인증서를 신뢰하도록 설정
  if ([[challenge protectionSpace] serverTrust] != nil) {
    SecTrustRef trust = [[challenge protectionSpace] serverTrust];
    NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    return;
  }

  // 기본 인증 처리 (HTTP Basic)
  if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodHTTPBasic) {
    NSString *username = [_basicAuthCredential valueForKey:@&quot;username&quot;];
    NSString *password = [_basicAuthCredential valueForKey:@&quot;password&quot;];
    if (username &amp;&amp; password) {
      NSURLCredential *credential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistenceNone];
      completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
      return;
    }
  }</code></pre><p>그러고 빌드하면 화면이 잘 나온다....
<img src="https://velog.velcdn.com/images/e_jink0/post/c92f65a8-e0da-4fcf-99b7-e38275de72a2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RN Webview 나이스 인증]]></title>
            <link>https://velog.io/@e_jink0/RN-Webview-%EB%82%98%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@e_jink0/RN-Webview-%EB%82%98%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Fri, 18 Oct 2024 00:36:50 GMT</pubDate>
            <description><![CDATA[<p>😇 문제상황</p>
<ul>
<li>web에서 팝업 형태로 뜨던것이 앱으로 빌드를 하면서 팝업형태가 아닌 인앱브라우저 내부 탭 전환이 이루어져 나이스 인증 페이지가 뜨지 않음</li>
<li>결과적으로는 흰 화면만 노출됨</li>
<li>RN내부 webview는 jsp로 구성되어있음</li>
</ul>
<p>🥹 해결 방법</p>
<ol>
<li><p>나이스인증 전용 webView 스택을 하나 만든다</p>
<pre><code>&lt;RootStack.Screen
 name=&quot;NicePayWebView&quot;
 component={NicePayWebViewConitainer}
 options={{
     animationEnabled: true, // 애니메이션 비활성화
 }}
/&gt;</code></pre></li>
<li><p>webview &gt; RN으로 나이스페이 창을 열것이다 라는 postMessage를 보낸다 encData를 같이 넘겨준다</p>
<pre><code>window.ReactNativeWebView?.postMessage(
 JSON.stringify({type: &#39;callCheckPlusSubmit&#39;, value: encData})
);</code></pre></li>
<li><p>RN에서 webview로부터 받은 encData를 가지고 source를 구성해서 NicePayWebView 스택을 쌓는다 </p>
<pre><code>const sourceURL = (
{
 uri: &#39;https://nice.checkplus.co.kr/CheckPlusSafeModel/checkplus.cb&#39;,
 method: &#39;POST&#39;,
 headers: {&#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;},
 body: `m=checkplusService&amp;EncodeData=${data.value}`,
});
</code></pre></li>
</ol>
<p>const pushAction = StackActions.push(&#39;NicePayWebView&#39;, {
    sourceURL: sourceURL,
    encodeData: data.value
});
navigation.dispatch(pushAction);</p>
<pre><code>
4. 인증진행이 완료되면 인증 완료 URL로 이동되면서 encodeData, successYn 데이터를 받는다. 데이터를 받아서 다시 RN으로 넘긴다</code></pre><p>// mobile일때
if(util.getUserAgent() === &#39;android&#39; || util.getUserAgent() === &#39;ios&#39;) {
    window.ReactNativeWebView?.postMessage(
        JSON.stringify({type: &#39;nicePayInSignin&#39;, value: {
            encodeData: encodeData,
            successYn: successYn
        }})
    );
} else { // web일때
    opener.resultCheckPlus(encodeData, successYn);
}</p>
<pre><code>
5. RN에서 인증 완료 데이터를 받으면 NicePayWebView에서 기존의 WebView로 이동한다</code></pre><p>navigation.navigate(&#39;defaultWebView&#39;, {
    encodeData: data.value.encodeData,
    success: data.value.successYn
});</p>
<pre><code>
6. defaultWebView로 이동되면서 전달받은 param 값을 webview로 내려준다</code></pre><p> webview?.current?.postMessage(
    JSON.stringify({
        type: &#39;nicePayInSignin&#39;,
        encodeData: route.params.encodeData,
        success: route.params.success
    }),
);</p>
<pre><code>![](https://velog.velcdn.com/images/e_jink0/post/b7fcd57b-268e-4d88-86fd-b557b6ee4b2e/image.gif)
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[React Native 빌드 에러 해결기록]]></title>
            <link>https://velog.io/@e_jink0/React-Native-%EB%B9%8C%EB%93%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@e_jink0/React-Native-%EB%B9%8C%EB%93%9C-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Mon, 14 Oct 2024 23:59:17 GMT</pubDate>
            <description><![CDATA[<h1 id="3-cmake-error">3. CMake Error</h1>
<h2 id="✅-문제-현상">✅ 문제 현상</h2>
<p>React Native android 빌드 중 Cmake관련 오류가 잔뜩 발생.
<img src="https://velog.velcdn.com/images/e_jink0/post/293d082e-cfff-4311-88ea-3b65fcfa9482/image.png" alt=""></p>
<h2 id="🔍-원인-분석">🔍 원인 분석</h2>
<ul>
<li>React Native가 0.71 이후 버전에서 JSI, Hermes, TurboModules 등 C++ 기반의 네이티브 기능을 도입하면서 CMake 설정이 필수적으로 요구되는 경우가 많아짐.</li>
<li>에러 로그를 보면 &quot;.../node_modules/react-native-gesture-handler/android/build/generated/source/codegen/jni/&quot; which is not an existing directory. 라고 하는 것을 보아하니 해당 경로에 jni폴더가 없어서 나는 에러였다.</li>
<li>결과적으로 npm install을 해서 자동적으로 source/codegen/jni/ 이 경로까지 만들어져야하는데 그러지 못했던 것임.</li>
</ul>
<h2 id="🛠️-해결-방법">🛠️ 해결 방법</h2>
<h3 id="node_modules-및-설정-관련된-폴더들-초기화-및-generatecodegenartifactsfromschema-실행">node_modules 및 설정 관련된 폴더들 초기화 및 generateCodegenArtifactsFromSchema 실행</h3>
<blockquote>
<p>$ rm -rf node_modules
$ npm install
$ cd android
$ ./gradlew clean
$ ./gradlew generateCodegenArtifactsFromSchema
$ npx react-native run-android</p>
</blockquote>
<h2 id="✅-결과">✅ 결과</h2>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/c68d89b5-03ad-477b-8c4c-f6257dda9910/image.png" alt="">
warning은 많지만 일단 성공 😬</p>
<h2 id="💡-참고-팁">💡 참고 팁</h2>
<ul>
<li>결과적으로 <code>./gradlew generateCodegenArtifactsFromSchema</code> 이 명령어를 통해서 해결하였는데, 이 명령은 React Native 프로젝트의 GraphQL 또는 TypeScript schema 등을 기반으로 자동 생성되는 코드/헤더 파일과 jni 폴더 구조를 만들어 줍니다.
<a href="https://reactnative.dev/docs/the-new-architecture/using-codegen">https://reactnative.dev/docs/the-new-architecture/using-codegen</a>
위 공식문서를 보면 일단 1차적으로 codegen에 대해서 나옵니다. 쉽게 얘기해서 네이티브 모듈과 컴포넌트를 자동으로 연결해주는 코드 생성도구입니다. 수동으로 작성하면 번거로운 C++, Java, Objective-C 코드를 Flow나 TypeScript 기반의 JS 사양(spec) 파일만 작성하면 자동으로 생성해 주는 것 입니다. 쉽게 말해, <strong>&quot;JS로 네이티브 연동 구조를 선언하면, 나머지 연결 코드는 자동 생성된다&quot;</strong> 는 개념입니다. 내부적으로 TurboModules와 연동되는 코드, CMakeLists.txt 등이 이 명령을 통해 생성됩니다.
<img src="https://velog.velcdn.com/images/e_jink0/post/2c3a625a-abd9-4336-b36f-2930c6135c0d/image.png" alt="">
<img src="https://velog.velcdn.com/images/e_jink0/post/80ce059e-85fc-4db3-8ca1-114bdb7f7bc4/image.png" alt="">
jni하위에 CMakeLists.txt를 포함한 폴더 및 파일들의 구조가 doc에 있으니 한번 훑어보시길 바랍니다. 😎</li>
</ul>
<h1 id="2-duplicate-class-error">2. Duplicate Class Error</h1>
<h2 id="✅-문제-현상-1">✅ 문제 현상</h2>
<p>React Native 프로젝트 빌드 중 다음과 같은 에러가 발생했다</p>
<blockquote>
<p>Execution failed for task &#39;:app:checkDebugDuplicateClasses&#39;.
A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
Duplicate class android.support.v4.app.INotificationSideChannel found in modules core-1.9.0.aar -&gt; core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0.aar -&gt; support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.app.INotificationSideChannel$Stub found in modules core-1.9.0.aar -&gt; core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0.aar -&gt; support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
...</p>
</blockquote>
<h2 id="🔍-원인-분석-1">🔍 원인 분석</h2>
<p>에러 메시지를 보면 두 가지 라이브러리 간의 클래스 충돌이 원인이다</p>
<p><code>androidx.core:core:1.9.0</code>
<code>com.android.support:support-compat:26.1.0</code></p>
<p>이 두 라이브러리는 각각 androidx와 android.support 계열이다.
AndroidX로 마이그레이션한 프로젝트에서 여전히 일부 종속 라이브러리나 코드가 android.support를 참조하고 있다면 이러한 충돌이 발생한다.</p>
<p>즉, AndroidX와 android.support 라이브러리를 동시에 사용할 수 없기 때문에 충돌이 발생한다.</p>
<h2 id="🛠️-해결-방법-1">🛠️ 해결 방법</h2>
<p>React Native 프로젝트를 완전히 AndroidX로 전환하기 위한 설정을 android/gradle.properties 파일에 추가한다</p>
<blockquote>
<p>android.useAndroidX=true
android.enableJetifier=true</p>
</blockquote>
<p><code>android.useAndroidX=true</code> AndroidX 라이브러리를 사용하도록 설정
<code>android.enableJetifier=true</code> 기존 android.support 기반 라이브러리를 AndroidX로 자동 변환해줌 (Jetifier)</p>
<p>추가로 build.gradle에서 혹시 com.android.support 계열 라이브러리가 명시적으로 추가되어 있다면 제거하거나 androidx 버전으로 교체해주어야 한다.</p>
<h2 id="✅-결과-1">✅ 결과</h2>
<p>위 설정을 적용한 후 ./gradlew clean 후 다시 빌드를 실행하니,
충돌 없이 정상적으로 앱이 빌드되었다.</p>
<h2 id="💡-참고-팁-1">💡 참고 팁</h2>
<p>Jetifier는 마이그레이션 호환성을 위해 존재하지만, 이후 사용하는 라이브러리가 모두 AndroidX를 지원한다면 enableJetifier는 false로 설정해도 된다.</p>
<p>가능하면 외부 라이브러리도 최신 AndroidX 지원 버전으로 유지하는 것이 좋다.</p>
<h1 id="1-hermes-engine-error">1. hermes-engine error</h1>
<p>message: Command PhaseScriptExecution failed with a nonzero exit code</p>
<p>보통 빌드할때 react-native start CLI로 실행하는데, building... 이후로 아무 동작하지 않을때는 xcode로 빌드하게 된다.</p>
<p>빌드할때 hermes-engine error가 뜨면서 아무 동작하지 않을때는 xcode 의 env파일을 지워보자.. (이걸로는 해결했지만 다른 해결법이 있다면 그 해결법도 포스팅해보겠음.)</p>
<p>root디렉토리 터미널에서 아래 커맨드 실행하기</p>
<blockquote>
<p>$ rm ./ios/.xcode.env.local</p>
</blockquote>
<p>이걸로도 안된다면 로그를 확인하기 </p>
<p>Showing Recent Messages</p>
<blockquote>
<p>/Users/jklee/myreactproj/myapp/ios/Pods/../../node_modules/react-native/scripts/react-native-xcode.sh: line 175: /Users/jklee/myreactproj/myapp/ios/Pods/hermes-engine/destroot/bin/hermesc: No such file or directory</p>
</blockquote>
<p>필자는 계속 Pods/hermes-engine/destroot/bin/hermesc 파일을 찾지 못한다는 에러와 함께 Command PhaseScriptExecution failed with a nonzero exit code 에러가  떴기때문에...</p>
<p>targets &gt; myapp &gt; Build Settings &gt; User-Defined 에서 
Use_HERMES = false로 바꿔서 빌드해보았더니 거짓말처럼 빌드가 잘되서 
다시 true로 바꿨더니 거짓말처럼 또 폴더를 잘찾아서 빌드가 되었다</p>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/65958aec-8456-46f2-a667-382e820a3fbf/image.png" alt=""></p>
<h1 id="👾-기타-명령어-모음">👾 기타 명령어 모음</h1>
<h3 id="xcode의-캐시-데이터빌드-캐시를-완전히-삭제하는-명령어">Xcode의 캐시 데이터(빌드 캐시)를 완전히 삭제하는 명령어</h3>
<blockquote>
<p>rm -rf ~/Library/Developer/Xcode/DerivedData</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 트랜지션 - window minimize, maximize]]></title>
            <link>https://velog.io/@e_jink0/css-%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98-window-minimize-maximize</link>
            <guid>https://velog.io/@e_jink0/css-%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98-window-minimize-maximize</guid>
            <pubDate>Fri, 09 Aug 2024 03:51:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/39a2f6a1-abcb-4f9f-a810-6c818f14bb76/image.gif" alt=""></p>
<h3 id="window-추가하기">window 추가하기</h3>
<pre><code>&lt;div class=&quot;window&quot;&gt;
    &lt;div class=&quot;title-bar&quot;&gt;
        &lt;div class=&quot;buttons&quot;&gt;
            &lt;div class=&quot;close&quot;&gt;&lt;/div&gt;
                &lt;div class=&quot;minimize&quot;&gt;&lt;/div&gt;
                &lt;div class=&quot;zoom&quot;&gt;&lt;/div&gt;
            &lt;/div&gt;
            &lt;div class=&quot;title&quot;&gt;Window Title&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;content&quot;&gt;
            Window Content
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre><h3 id="css-스타일-추가하기">css 스타일 추가하기</h3>
<pre><code>.window {
    width: 300px;
    height: 300px;
    background-color: #fff;
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 6;
    transition: 
        transform 1s ease-in-out, 
        opacity 1s ease-in-out,
        right 1s ease-in-out,
        bottom 1s ease-in-out;
}
.window.minimized {
    opacity: 0 !important;
    z-index: 2;
}
.window.minimized.hidden {
    display: none;
}</code></pre><ul>
<li>움직이면서 투명해져야하고 우측 하단에 있는 floating메뉴 뒤로 숨어야 하기때문에 transition 속성에 transform, opacity, right, bottom 을 준다.</li>
</ul>
<h3 id="javascript-코드-추가하기">javascript 코드 추가하기</h3>
<pre><code>const minimizeButton = $(&#39;.minimize&#39;);
const photoContainer = $(&#39;.photo-container&#39;);
const myWindow = $(&#39;.window&#39;);

minimizeButton.on(&#39;click&#39;, function(e) {
     if (!myWindow.hasClass(&#39;minimized&#39;)) {
        const chatbotRect = photoContainer[0].getBoundingClientRect();
        const containerRect = myWindow[0].getBoundingClientRect();

        const translateX = chatbotRect.right - containerRect.right - 20;
        const translateY = chatbotRect.bottom - containerRect.bottom - 40;

        myWindow.css(&#39;display&#39;, &#39;block&#39;);
        myWindow.css(&#39;transform&#39;, `translate(${translateX}px, ${translateY}px) scale(0.1)`);
        myWindow.css(&#39;right&#39;, &#39;20px&#39;);
        myWindow.css(&#39;bottom&#39;, &#39;98px&#39;);
    }

    myWindow.addClass(&#39;minimized&#39;);

    setTimeout(() =&gt; {
        myWindow.addClass(&#39;hidden&#39;);
    }, 100);
});

photoContainer.on(&#39;click&#39;, function(e) {
    myWindow.css(&#39;transform&#39;, &#39;translate(-50%, -50%)&#39;);
    myWindow.css(&#39;right&#39;, &#39;unset&#39;);
    myWindow.css(&#39;bottom&#39;, &#39;unset&#39;);

    myWindow.removeClass(&#39;minimized&#39;);

    setTimeout(() =&gt; {
        myWindow.removeClass(&#39;hidden&#39;);
    }, 100);
});</code></pre><ul>
<li>반응형이기때문에 window와 우측 하단의 floating메뉴 사이의 거리를 구해야 한다. 어떤 화면에서든 floating메뉴 쪽으로 샥~ 숨어야 한다.</li>
<li>getBoundingClientRect()를 사용해서 window와 floating메뉴의 right, bottom 값을 각각 구하고 움직여야 할 translateX값과 translateY값을 구한다.</li>
<li>translateX값과 translateY값을 transform값에 설정한 후 myWindow.addClass(&#39;minimized&#39;)하면!! 스르륵 하면서 minimize가 된다.</li>
<li>다시 maximize하고싶으면 window를 화면 중앙에 보여줘야 하니까 translate(-50%, -50%)를 주면서 right, bottom 값을 unset으로 주면 다시 maximize가 된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 애니메이션 - 텍스트 그라데이션]]></title>
            <link>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98</guid>
            <pubDate>Thu, 08 Aug 2024 06:17:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/1c6f04fd-f511-4cf9-8c65-c71db0bcbe91/image.gif" alt=""></p>
<h3 id="텍스트-추가">텍스트 추가</h3>
<pre><code>&lt;div style=&quot;padding: 100px;&quot;&gt;
    &lt;span class=&quot;kmi-chat-content-loading&quot;&gt;
        매우매우 로딩중입니다....
    &lt;/span&gt;
&lt;/div&gt;</code></pre><h3 id="css-추가">css 추가</h3>
<pre><code>@keyframes gradient-move {
    0% {
        background-position: 200% 0;
    }
    100% {
        background-position: -200% 0;
    }
}
.kmi-chat-content-loading {
    background: linear-gradient(90deg, rgba(72, 78, 86, 0.2) 0%, rgba(72, 78, 86, 1) 50%, rgba(72, 78, 86, 0.2) 100%);
    background-size: 200% 200%;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
    animation: gradient-move 5s linear infinite;
}</code></pre><ul>
<li><p>일단 background에 gradient를 주는데 애니메이션으로 background-position값을 계속 바꿀것이기 때문에 이어지는 효과를 주기 위해서 50% 가운데부분을 제일 opacity를 높게주고 0퍼센트와 100퍼센트는 opacity를 작게준다.</p>
</li>
<li><p>속성 설명</p>
</li>
</ul>
<p><strong>-webkit-background-clip: text;</strong>
설명: 이 속성은 요소의 배경 이미지나 그라데이션이 텍스트의 형태를 따라 잘려나가도록 설정한다. 즉, 배경이 텍스트의 모양에 맞춰져서 표시된다.</p>
<p><strong>-webkit-text-fill-color: transparent;</strong>
설명: 이 속성은 텍스트의 색상을 투명하게 만든다. 이렇게 하면 텍스트의 색상은 보이지 않고 배경이 텍스트를 통해 보이게 된다.</p>
<p><strong>background-clip: text;</strong>
설명: -webkit-background-clip: text;의 표준 속성으로, 동일하게 배경 이미지나 그라데이션을 텍스트 모양으로 잘라서 적용한다.</p>
<p><strong>text-fill-color: transparent;</strong>
설명: -webkit-text-fill-color: transparent;의 표준 속성으로, 동일하게 텍스트 색상을 투명하게 만든다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 애니메이션 - 서랍 효과 주기]]></title>
            <link>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%84%9C%EB%9E%8D-%ED%9A%A8%EA%B3%BC-%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%84%9C%EB%9E%8D-%ED%9A%A8%EA%B3%BC-%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 08 Aug 2024 05:20:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/dbaa9f21-6e9d-4776-ab92-43f7606a1866/image.gif" alt=""></p>
<h3 id="서랍-효과를-줄-div-추가하기">서랍 효과를 줄 div 추가하기</h3>
<pre><code>&lt;div class=&quot;drawer-container&quot;&gt;
    &lt;div class=&quot;drawer-text-wrapper&quot;&gt;
        &lt;span&gt;서랍 효과ㅏㅏㅏㅏㅏㅏㅏㅏ&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre><h3 id="css-적용하기">css 적용하기</h3>
<pre><code>@keyframes fadeInOut {
    0% {
        opacity: 0;
        transform: translateX(30px);
        width: 196px;
    }
    20% {
        opacity: 1;
        transform: translateX(0);
        width: 246px;
    }
    80% {
        opacity: 1;
        transform: translateX(0);
        width: 246px;
    }
    100% {
        opacity: 0;
        transform: translateX(30px);
        width: 196px;
    }
}
.drawer-container {
    bottom: 105px;
    right: 46px;
    position: fixed;
    z-index: 1;
    opacity: 0;
    animation: fadeInOut 6s forwards infinite;
    width: 196px;
}
.drawer-text-wrapper {
    background-color: #3f659b;
    padding: 10px 68px 10px 12px;
    border-radius: 50px;
    display: flex;
    white-space: nowrap;
}
.drawer-text-wrapper span{
    font-size: 1rem;
    font-family: &quot;NotoSansKRMedium&quot;;
    color: #ffffff;
}</code></pre><ul>
<li>서랍이 열렸다가 닫히는 느낌을 주기위해 0, 20, 80, 100 으로 나눠서 효과를 줌.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 애니메이션 - 은은한 강조 효과 주기]]></title>
            <link>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%9D%80%EC%9D%80%ED%95%9C-%EA%B0%95%EC%A1%B0-%ED%9A%A8%EA%B3%BC-%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%9D%80%EC%9D%80%ED%95%9C-%EA%B0%95%EC%A1%B0-%ED%9A%A8%EA%B3%BC-%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 08 Aug 2024 01:57:28 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/22af5ebe-9b22-4a6c-b3d4-f294930c022b/image.gif" alt=""></p>
<h3 id="bg를-넣을-div-넣기">bg를 넣을 div 넣기</h3>
<pre><code> &lt;div&gt;
        &lt;div class=&quot;photo-container&quot; style=&quot;--direction:normal;&quot;&gt;
            ...
        &lt;/div&gt;

        &lt;div class=&quot;bg-animation&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;</code></pre><h3 id="css-적용하기">css 적용하기</h3>
<pre><code>@keyframes animloader {
    0% {
        transform: scale(0.65);
        opacity: 0.9;
    }    
    100% {
        transform: scale(1);
        opacity: 0;
    }
}
.bg-animation {
    bottom: 166px;
    right: 88px;
    position: fixed;
    z-index: 1;
}
.bg-animation::after,
.bg-animation::before {
    content: &#39;&#39;;  
    box-sizing: border-box;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background-color: rgba(56, 155, 247);
    position: absolute;
    left: 0;
    top: 0;
    opacity: 0;
    animation: animloader 1.8s linear infinite;
}
.bg-animation::after {
    animation-delay: 0.8s;
}</code></pre><ul>
<li>::before, ::after 이것은 CSS에서 사용하는 가상 요소로서, 이 가상 요소들은 주로 추가적인 디자인 요소를 추가하거나, 스타일링을 위해 사용된다.</li>
<li>필자는 ::after 가상 요소에 0.8초 딜레이를 주어서 두겹으로 보여지게 하는(?) 효과를 주었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[css 애니메이션 - 왼쪽으로 무한히 이동하는 이미지]]></title>
            <link>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%99%BC%EC%AA%BD%EC%9C%BC%EB%A1%9C-%EB%AC%B4%ED%95%9C%ED%9E%88-%EC%9D%B4%EB%8F%99%ED%95%98%EB%8A%94-%EC%9D%B4%EB%AF%B8%EC%A7%80</link>
            <guid>https://velog.io/@e_jink0/css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%99%BC%EC%AA%BD%EC%9C%BC%EB%A1%9C-%EB%AC%B4%ED%95%9C%ED%9E%88-%EC%9D%B4%EB%8F%99%ED%95%98%EB%8A%94-%EC%9D%B4%EB%AF%B8%EC%A7%80</guid>
            <pubDate>Thu, 08 Aug 2024 01:18:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/e_jink0/post/744d871f-fbe4-4133-a5e7-85a4fbf0ae14/image.gif" alt=""></p>
<h3 id="사진-넣기">사진 넣기</h3>
<pre><code> &lt;div class=&quot;photo-container&quot; style=&quot;--direction:normal;&quot;&gt;
    &lt;div class=&quot;photo-wrapper&quot;&gt;
        &lt;img class=&quot;a&quot;/&gt;
        &lt;img class=&quot;b&quot;/&gt;
        &lt;img class=&quot;a&quot;/&gt;
        &lt;img class=&quot;b&quot;/&gt;
    &lt;/div&gt;
 &lt;/div&gt;</code></pre><ul>
<li>필자는 2장을 할것이라 2장을 이어서 2번 넣었다.</li>
</ul>
<h3 id="css적용하기">css적용하기</h3>
<pre><code>@keyframes slide {
    0% { transform: translateX(0); }
    25% { transform: translateX(-25%); }
    50% { transform: translateX(-25%); }
    75% { transform: translateX(-50%); }
    100% { transform: translateX(-50%); }
}
.photo-container {
    width: 56px;
    height: 56px;
    bottom: 98px;
    right: 20px;
    position: fixed;
    border-radius: 50%;
    overflow: hidden;
    background-color: #C7EAFF;
    box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.2);
    z-index: 2;
}
.photo-wrapper {
    display: flex;
    width: calc(100% * 4);
    height: 100%;
    animation: 3s linear infinite 2s slide;
}
.photo-wrapper img {
    width: 25%;
    height: 100%;
    object-fit: cover;
}
.photo-wrapper img.a {
    content: url(./images/example1.png);
}
.photo-wrapper img.b {
    content: url(./images/example2.jpg);
}</code></pre><ul>
<li><p>photo-wrapper에 적용된 animation 디테일
<img src="https://velog.velcdn.com/images/e_jink0/post/4b8619cd-3b8b-45ff-b3c2-65db8901ed16/image.png" alt=""></p>
</li>
<li><p>2장의 사진이 이동하면서 끊어지지 않아야 하기 때문에
0, 25, 50, 75, 100 으로 끊어서 translateX값을 적절히 주도록 한다.</p>
</li>
<li><p>slide애니메이션이 적용되는 모습
<img src="https://velog.velcdn.com/images/e_jink0/post/663b047a-5bb9-46df-971d-d081ae9bb56d/image.gif" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS svg transition]]></title>
            <link>https://velog.io/@e_jink0/CSS-svg-transition</link>
            <guid>https://velog.io/@e_jink0/CSS-svg-transition</guid>
            <pubDate>Wed, 27 Mar 2024 07:33:19 GMT</pubDate>
            <description><![CDATA[<h3 id="vscode에서-svg-편집-플러그인-깔기">vscode에서 svg 편집 플러그인 깔기</h3>
<p><img src="https://velog.velcdn.com/images/e_jink0/post/44427a64-cf0a-495e-b375-cc1555166ba7/image.png" alt=""></p>
<h3 id="svg이미지에-클릭-이벤트를-주는-방법">svg이미지에 클릭 이벤트를 주는 방법</h3>
<ol>
<li>svg를 img태그가 아닌 object태그로 넣기</li>
</ol>
<p>html</p>
<pre><code>&lt;object class=&quot;key&quot; data=&quot;./images/key.svg&quot; type=&quot;&quot;&gt;&lt;/object&gt;</code></pre><p>object태그로 넣으면 개발자 도구에서 svg내부 코드를 확인할 수 있음
<img src="https://velog.velcdn.com/images/e_jink0/post/3401f6bd-ba0f-4090-bb70-967c2c75c8f1/image.png" alt=""></p>
<ol start="2">
<li>svg 파일을 열어서 수정하기</li>
</ol>
<ul>
<li><p>svg 파일 기본 형태</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;svg class=&quot;key&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 392.87 486.05&quot;&gt;

&lt;defs&gt;
    &lt;style&gt;
  &lt;/style&gt;
&lt;/defs&gt;

&lt;g&gt;
    &lt;path class=&quot;key-blade cls-3&quot; d=&quot;M223.23,20.53h-22.68c-6.26,0-11.34,5.08-11.34,11.34V213.28s45.35,0,45.35,0V31.87c0-6.26-5.08-11.34-11.34-11.34Z&quot;/&gt;
  &lt;path class=&quot;cls-2&quot; d=&quot;M349.01,167.61H218.22c-30.28,0-54.82,24.54-54.82,54.82v220.05c0,11.27,9.13,20.4,20.4,20.4h130.8c30.28,0,54.82-24.54,54.82-54.82V188.02c0-11.27-9.13-20.4-20.4-20.4Z&quot;/&gt;
  &lt;circle class=&quot;key-btn cls-1&quot; cx=&quot;211.89&quot; cy=&quot;213.45&quot; r=&quot;22.68&quot;/&gt;
&lt;/g&gt;

&lt;script&gt;
&lt;/script&gt;
&lt;/svg&gt;</code></pre></li>
</ul>
<blockquote>
<p>viewBox 속성: 4개의 숫자 중에서 첫번째와 두번째 숫자는 왼쪽 위
(x,y)의 좌표를의미하고 세번째와 네번째 숫자는 오른쪽 아래의 좌표(width, height)를 의미함.</p>
</blockquote>
<blockquote>
<p>defs 태그: 스타일 정의, 그라디언트, 클리핑 등 스타일 선언 후 g태그 내부 코드들에 적용할 수 있음.</p>
</blockquote>
<blockquote>
<p>script태그: 내부에 javascript로 svg에 이벤트를 적용할 수 있음.</p>
</blockquote>
<ul>
<li>svg파일 내부에 style 코드 수정하기<pre><code>.key-blade {
  transform: rotate(-180deg);
  transform-origin: 211.89px 213.45px;
  transition: 0.3s;
}
</code></pre></li>
</ul>
<p>.key-blade-flipped {
    transform: rotate(0deg);
}</p>
<pre><code>
* javascript로 클릭 이벤트 주기</code></pre><p>const keyBtnElement = document.querySelector(&#39;.key-btn&#39;);
const bladeElement = document.querySelector(&#39;.key-blade&#39;);
keyBtnElement.addEventListener(&#39;click&#39;, e =&gt; {
    bladeElement.classList.toggle(&#39;key-blade-flipped&#39;);
});</p>
<p>```</p>
<p>보통의 트랜지션 작업하듯이 작업하면 됨.</p>
<ul>
<li>key.svg 동작 확인
<img src="https://velog.velcdn.com/images/e_jink0/post/47264db4-b2c1-4017-bb63-9a4a3bac9f27/image.gif" alt=""></li>
</ul>
<p>&lt;참고 사이트&gt;
<a href="https://inf.run/gjZFX">인프런 1분코딩-웹 애니메이션의 새로운 표준, Web Animations API</a></p>
]]></description>
        </item>
    </channel>
</rss>