<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>juice-han.log</title>
        <link>https://velog.io/</link>
        <description>강알쥐</description>
        <lastBuildDate>Fri, 17 Apr 2026 08:49:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>juice-han.log</title>
            <url>https://velog.velcdn.com/images/juice-han/profile/658cedb1-1e32-4618-b0cc-bac855f427ff/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. juice-han.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/juice-han" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Claude Code와 figma mcp server를 연결해서 활용하기]]></title>
            <link>https://velog.io/@juice-han/Claude-Code-figma-mcp-server</link>
            <guid>https://velog.io/@juice-han/Claude-Code-figma-mcp-server</guid>
            <pubDate>Fri, 17 Apr 2026 08:49:55 GMT</pubDate>
            <description><![CDATA[<p>오늘 figma mcp server를 ai agent와 연결해서 활용하는 방법에 대한 세미나를 들었다.</p>
<blockquote>
<p>&quot;이렇게나 간편하고 편하게 디자인을 코드로 구현할 수 있는 방법이 있다고?&quot;</p>
</blockquote>
<p>ai 사용에 대해 아직 좀 회의적이지만 이 기술은 너무나 인상적이어서 사용해보고 싶었다.</p>
<p>세미나가 종료된 후 바로 claude code에 figma mcp server를 연결해봤다.</p>
<p>claude cli를 활용하니 연결은 간단하게 됐다.</p>
<h2 id="claude-code와-figma-mcp-server-연결-방법">claude code와 figma mcp server 연결 방법</h2>
<p>먼저 claude code에 figma mcp server를 등록해야 한다.</p>
<h3 id="claude-code에-figma-mcp-server-등록">claude code에 figma mcp server 등록</h3>
<p>터미널을 열고 다음 명령어를 입력하면 전체 프로젝트에서 사용할 수 있는 figma mcp server 등록이 된다. (claude cli가 깔려 있어야 한다.)</p>
<pre><code class="language-shell">claude mcp add --scope user --transport http figma https://mcp.figma.com/mcp</code></pre>
<p>만약 현재 프로젝트에서만 figma mcp를 사용하고 싶다면 다음과 같이 입력하면 된다.</p>
<pre><code class="language-shell">claude mcp add --transport http figma https://mcp.figma.com/mcp</code></pre>
<p><img src="https://velog.velcdn.com/images/juice-han/post/ee54f6d7-9c1c-4e61-9be9-6d1802cbe5a5/image.jpg" alt="figma mcp 등록 명령어"></p>
<p>이렇게 명령어를 작성하면 등록이 됐다는 메세지가 나올 것이다.</p>
<h3 id="figma-mcp-server-인증">figma mcp server 인증</h3>
<p>그 다음 figma mcp server 인증을 해야 한다. 인증은 claude code 통해 진행할 수 있다.</p>
<p>claude cli를 실행하고 <code>/mcp</code> 를 입력하면 현재 연결된 mcp 서버를 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/996d5179-7055-48b8-81b5-a99e325eed99/image.jpg" alt="figma mcp 등록 결과"></p>
<p>필자는 이미 인증을 해서 connected라고 나오는데 인증을 안 했다면 아마 unauthenticated라고 나올 것이다. 그러면 figma 정보를 조회하고 인증을 진행하면 된다.</p>
<p>인증은 브라우저를 통해서 진행되기 때문에 간편했다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/d1749257-f9d6-4190-801b-b72d9095a29a/image.jpg" alt="피그마 브라우저 인증"></p>
<p>동의 및 액세스 허용을 누르면 인증이 완료된다.</p>
<h2 id="figma-mcp로-figma-디자인을-코드로-구현하기">figma mcp로 figma 디자인을 코드로 구현하기</h2>
<p>먼저 디자인을 구현하고 싶은 브라우저 figma 페이지에 접속해서 URL을 복사한다.
그리고 claude code에 그 디자인을 본인이 원하는 라이브러리 코드로 구현해달라고 말하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/39f457c0-6f67-4b07-a053-59e129e29435/image.jpg" alt="claude code figma mcp 사용 명령"></p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/24d635fa-6979-4fb5-9ea4-2af81e181872/image.jpg" alt="figma 디자인 페이지"></p>
<p>위 사진은 figma에 간단하게 만들어봤던 경제 서비스 와이어프레임이다.</p>
<p>figma mcp server 테스트를 위해 이 figma 디자인을 활용했다.</p>
<p>claude code에게 &quot;내가 보여준 figma 페이지의 디자인을 옮겨와서 next.js로 구현해줘 {figma 주소}&quot; 라고 명령을 했더니 알아서 디자인을 가져오고 Next.js 코드로 구현해줬다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/64aa4aea-4ba5-4864-ba59-cd48c8018c01/image.jpg" alt="figma 디자인 가져오기"></p>
<p>현재 figma 무료 플랜이라 총 7개의 페이지인데 4개 밖에 가져오지 못했다.</p>
<p>하지만 claude code는 똑똑해서 나머지 3개 페이지는 메타데이터를 가져와 코드로 구현해준다고 했다.</p>
<p>그 결과 완전히 똑같진 않더라도 어느정도 비슷한 UI를 구현해냈다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/juice-han/post/bb32c6da-aa34-4db5-8a16-b85f6079c194/image.jpg" alt=""></th>
<th><img src="https://velog.velcdn.com/images/juice-han/post/90c5df46-76b5-4123-bddd-f885fe4baeb3/image.jpg" alt=""></th>
<th><img src="https://velog.velcdn.com/images/juice-han/post/100baf32-3eec-4b0f-957d-b971c09798bb/image.jpg" alt=""></th>
</tr>
</thead>
</table>
<p>와우. 디자인만 괜찮은 게 아니라 사용자와의 상호작용도 잘 구현해냈다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/02dd5e34-1d98-4962-bbb1-a21ea6a00339/image.gif" alt="페이지 사용자 상호작용 구현"></p>
<p>그저 페이지 UI만 따라 구현한게 아니라 페이지 간의 연결성을 고려하고 상호작용 요소를 배치한 것이다.</p>
<p>간단하게 figma mcp server를 사용해봤는데 결과가 만족스러워서 claude code MAX 플랜을 구매하고 싶다는 충동이 마구 생겨났다. (현재 가장 싼 Pro 플랜을 사용 중이다😭)</p>
<p>ai를 잘만 사용하면 생산성을 말도 안 되게 높일 수 있을 것 같다는 걸 직접 체험한 순간이었다.
기술의 발전이 정말 무섭도록 빠른 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] URLSearchParams를 활용해 search parameter를 수정하는 방법]]></title>
            <link>https://velog.io/@juice-han/Next-useSearchParams-URLSearchParams</link>
            <guid>https://velog.io/@juice-han/Next-useSearchParams-URLSearchParams</guid>
            <pubDate>Tue, 14 Apr 2026 06:35:37 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-ts">const searchParams = useSearchParams()
const params = new URLSearchParams(searchParams.toString())</code></pre>
<p>궁금했다. 왜 searchParams를 문자열로 변환해 다시 <code>URLSearchParams()</code>에 넣는지.</p>
<p>그냥 searchParams를 바로 사용하면 안 되나 싶었다.</p>
<p>하지만 URLSearchParams를 생성한 이유는 따로 있었다.</p>
<p>바로 Next.js의 <code>useSearchParams</code> 반환값은 <strong>read-only</strong>인 <code>URLSearchParams</code> 이기 때문이다.</p>
<h2 id="nextjs의-usesearchparams">Next.js의 useSearchParams()</h2>
<p>useSearchParams()에 관한 Next.js 공식 문서: <a href="https://nextjs.org/docs/app/api-reference/functions/use-search-params">https://nextjs.org/docs/app/api-reference/functions/use-search-params</a></p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/dff7a621-ca38-4d00-8813-20b191654abe/image.jpg" alt="useSearchParams 공식 문서 사진"></p>
<p>사진에서 보이는 것처럼 <strong>useSearchParams의 반환값은 read-only 버전의 URLSearchParams</strong>라고 한다.</p>
<p>실제로 <code>console.log</code>에 <code>useSearchParams</code> 반환값과 <code>URLSearchParams</code>로 생성한 값을 순서대로 찍으면 다음과 같이 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/4041c7f1-83da-415f-a3fc-ef5aa2b0d578/image.jpg" alt="searchParams와 URLSearchParams 값 console.log 결과"></p>
<p><strong>결국 searchParams의 값을 변경해서 사용하려면 다른 방법을 찾아야한다</strong>는 뜻이다.</p>
<h2 id="urlsearchparams-생성자">URLSearchParams 생성자</h2>
<p>값을 변경해서 사용하는 방법 중 하나는 <strong>URLSearchParams 생성자에 기존 searchParams를 전달해 사용하는 것</strong>이다.</p>
<p>URLSearchParams의 생성자를 활용하는 방법은 MDN 공식문서에 다음과 같이 나와있다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/11f895e6-4e1a-4814-9f4d-3f4886024294/image.jpg" alt="URLSearchParams 생성자 관련 공식 문서"><a href="https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams/URLSearchParams">https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams/URLSearchParams</a></p>
<p>이 중 두 번째 방법인 <strong>문자열 리터럴을 전달하는 방법</strong>을 통해 기존 Next.js의 read-only인 searchParams 값을 변경 가능한 값으로 변환하여 사용할 수 있다.</p>
<p>코드를 다시 살펴보자.</p>
<pre><code class="language-ts">const searchParams = useSearchParams()
console.log(searchParams.toString()) // &#39;foo=1&amp;bar=2&#39;
const params = new URLSearchParams(searchParams.toString())</code></pre>
<p>searchParams의 <code>toString()</code> 메서드는 search parameter 값을 <strong>?를 제외한 문자열 형태로 반환</strong>한다.</p>
<p>이 값을 URLSearchParams의 초깃값으로 전달해주면 같은 URLSearchParams지만 값이 <strong>변경 가능한 객체</strong>가 생성된다.</p>
<p>다음은 useSearchParams()에 관한 Next.js 공식 문서 활용 예제이다.</p>
<p><img src="https://exp-upload.goorm.io/2026-04-14/%C3%A1/C9KERFWqPfsYcjZdOtwebp" alt="file_C9KERFWqPfsYcjZdOt"></p>
<p>이를 통해 공식 문서에서도 searchParams를 변경하는 방법으로 URLSearchParams 객체를 재생성하는 방법을 사용하고 있는 걸 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Typescript] declare module을 사용해야 하는 이유]]></title>
            <link>https://velog.io/@juice-han/Typescript-declare-module</link>
            <guid>https://velog.io/@juice-han/Typescript-declare-module</guid>
            <pubDate>Sun, 12 Apr 2026 12:31:38 GMT</pubDate>
            <description><![CDATA[<h1 id="declare-module">declare module</h1>
<p><code>declare module</code>은 타입스크립트 문법으로, <strong>특정 module에 선언된 타입을 추가하거나 수정해서 사용</strong>하기 위해 사용한다.</p>
<p>다음은 금요일에 작성했던 코드의 일부다.</p>
<pre><code class="language-typescript">declare module &#39;next-auth&#39; {
  interface Session {
    user: {
      id: string
    } &amp; DefaultSession[&#39;user&#39;]
  }
}</code></pre>
<p><code>next-auth</code>의 Session 타입을 커스텀 해야하는 일이 있었다.</p>
<p>기존 Session 타입의 <strong>user 속성에 id 속성을 추가</strong>해야 했던 것이다.</p>
<p>이를 위해 기존에 선언된 Session 타입을 중복해서 선언함으로써 원래 갖고 있던 속성들을 그대로 가져왔고, user 속성에 id 속성을 추가했다. (interface 타입의 선언 병합 개념)</p>
<p>자세히 보면 user 속성에 <code>DefaultSession[&#39;user&#39;]</code>를 확장 선언했는데, 그 이유는 user 속성을 커스텀할 경우 기존 user 속성이 전부 날아가기 때문이다.</p>
<p>이로써 원래 갖고 있던 속성을 그대로 가져오면서 id값을 추가할 수 있었다.</p>
<p>결과적으로 Session 타입은 user에 id 속성이 추가되어 다음과 같이 코드에 적용된다.</p>
<pre><code class="language-ts">interface Session {
  user?: {
    id: string
    name?: string | null
    email?: string | null
    image?: string | null
  }
  expires: ISODateString
}</code></pre>
<h2 id="declare-module을-사용해야-하는-이유">declare module을 사용해야 하는 이유</h2>
<p>문득 &#39;꼭 declare module을 써야할까? 커스텀 타입을 만들어 사용하면 안 될까?&#39;라는 궁금증이 생겼다.</p>
<p>이에 대해 gemini에게 물어보니 declare module을 쓰는 게 더 좋다는 답변을 얻었다.</p>
<h3 id="커스텀-타입을-만들어서-사용할-때">커스텀 타입을 만들어서 사용할 때</h3>
<p>커스텀 타입을 만든다고 하면 이렇게 만들 수 있을 것이다.</p>
<pre><code class="language-ts">interface MySession extends Session {
  user: {
    id: string
  } &amp; DefaultSession[&#39;user&#39;]
}</code></pre>
<p>만약 커스텀 타입을 만들어 사용할 경우, <code>useSession()</code> 훅이 반환한 값에서 user의 id 값에 접근하려고 하면 에러가 발생한다.</p>
<pre><code class="language-ts">const { data: session, status } = useSession()
session?.user.id // 에러 발생
const mySession = session as MySession
mySession?.user.id // 정상 작동</code></pre>
<p>이럴 때 <strong>타입 캐스팅</strong>을 활용해서 <code>MySession</code> 으로 타입을 바꿔줘야 하는데, 만약 <strong>session을 사용하는 컴포넌트가 늘어나면 불필요한 타입 캐스팅 코드가 반복</strong>된다.</p>
<p>이런 반복을 없애기 위해 declare module을 사용한다.</p>
<h3 id="declare-module의-장점">declare module의 장점</h3>
<p>declare module을 사용하면 <code>next-auth</code> <strong>모듈 내의 타입을 수정하기 때문에 typescript 컴파일러가 이를 인식하고, id값에 접근해도 오류를 내지 않는다.</strong></p>
<p>반복되는 코드를 줄여주고 코드 자동완성 기능까지 제공하는 편리한 기능이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리플로우(reflow)와 GPU 연산을 통한 애니메이션 최적화]]></title>
            <link>https://velog.io/@juice-han/reflow-gpu</link>
            <guid>https://velog.io/@juice-han/reflow-gpu</guid>
            <pubDate>Sat, 11 Apr 2026 04:13:39 GMT</pubDate>
            <description><![CDATA[<p>css로 애니메이션을 구현해보면서 렌더링 속도가 저하되는 문제가 발생했다.</p>
<p>이 문제를 해결하기 위해 gemini에게 해답을 물어보니 애니메이션 최적화가 필요하다고 답하며</p>
<p>리플로우 현상과 GPU 연산을 이해해야 한다고 말했다.</p>
<h2 id="리플로우reflow">리플로우(reflow)</h2>
<p>리플로우란 화면 레이아웃을 재조정하는 과정을 말한다.</p>
<p>애니메이션이나 레이아웃 조정이 필요할 때, 브라우저는 화면 요소의 크기와 위치를 다시 계산하여 렌더링하는 리플로우를 거친다.</p>
<p>이 과정에서 재조정이 필요한 요소 말고도 DOM 트리의 상위 요소들을 함께 다시 계산해버린다.</p>
<p>리플로우는 CPU 연산을 사용하기 때문에 복잡한 js 연산이 필요한 페이지일 경우 애니메이션이나 레이아웃 조정이 버벅이는 현상이 발생한다.</p>
<p>대표적으로 width, height, left, top, padding 속성을 바꿀 때 이 현상이 일어난다.</p>
<p>나는 기존 코드에서 padding 속성을 바꾸는 애니메이션을 구현했고, 그 결과 화면이 버벅이며 렌더링 속도가 느려졌다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/9efc88e1-5b5e-4f5c-bb6a-6344e3142283/image.png" alt="패팅 애니메이션 구현 코드"></p>
<p>이에 대한 해답으로 gemini는 padding를 바꾸는 대신 scale을 조정하는 방법을 알려줬다.</p>
<h2 id="gpu-연산">GPU 연산</h2>
<p><img src="https://velog.velcdn.com/images/juice-han/post/3346b80e-32fc-4c04-8301-caf2541cc5ab/image.jpg" alt="CPU와 GPU 연산의 차이점"></p>
<p>출처: <a href="https://mong-blog.tistory.com/entry/CSS-%EC%99%9C-transform-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C-with-GPU-Reflow#google_vignette">Mong dev blog</a></p>
<p>scale 속성 변경은 GPU를 사용하는 연산이기 때문에 기존 CPU 자원을 사용하지 않고 병렬적으로 처리가 가능해 렌더링 속도가 향상되는 이점이 있다고 한다.</p>
<p>(GPU를 사용해서 속도가 향상되는 건 노드 레이어 분리와 연관이 있는데 이 내용은 더 공부해서 시간이 되면 정리해보려고 한다.)</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/c9ea8e9a-888a-43e3-8fd9-cd279ad84090/image.png" alt="스케일 애니메이션 구현 코드"></p>
<p>이렇게 padding 대신 scale을 변경함으로써 리플로우 현상을 없애고 GPU 연산을 통해 애니메이션 최적화를 할 수 있었다.</p>
<p>물론 사진 자료에서 볼 수 있듯이 GPU 연산도 남용하면 성능 저하를 일으킬 수 있으니 필요에 따라 사용하는 것이 좋겠다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/3f18ad37-b442-4c59-8821-0a2f53b782fb/image.gif" alt="scale 애니메이션 동작 gif"></p>
<p>CPU 연산과 GPU 연산을 생각하며 애니메이션을 최적화한다면 멋진 애니메이션을 효율적으로 구현할 수 있을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Chakra UI의 group 클래스와 _groupHover 속성]]></title>
            <link>https://velog.io/@juice-han/react-chakra-ui-group-groupHover</link>
            <guid>https://velog.io/@juice-han/react-chakra-ui-group-groupHover</guid>
            <pubDate>Sun, 05 Apr 2026 08:51:30 GMT</pubDate>
            <description><![CDATA[<p>오늘은 유튜브 영상 썸네일 컴포넌트를 만들어봤다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/bd3beea1-795d-44a4-b68c-d5ecd1cb7e8f/image.jpg" alt="유튜브 영상 썸네일 컴포넌트"></p>
<p>기초적인 레이아웃을 잡는 건 어렵지 않았지만, 컴포넌트를 hover했을 때 배경색이 생기는 애니메이션을 구현하는 데 애를 먹었다.</p>
<p>이 과정에서 chakra ui의 새로운 <strong>group</strong>이라는 클래스를 알게되었다.</p>
<h2 id="group-클래스와-_grouphover-속성">group 클래스와 _groupHover 속성</h2>
<p>나는 이번에 썸네일 컴포넌트를 hover하면 컴포넌트 배경에 있는 투명한 박스의 배경색과 패딩을 변경하려고 했다.</p>
<p>배경 박스는 <code>z-index</code>가 전면 요소들보다 낮기 때문에 마우스를 아무리 올려봐도 배경 박스엔 hover가 적용되지 않았다.</p>
<p>대상 요소에 직접 마우스를 올려야 hover가 활성화되는 것 같다.</p>
<p>그래서 gemini에게 물어보니 <code>group</code>이라는 class를 알려주었다.</p>
<p>이 클래스를 부모 요소에 추가하면 <strong>부모 요소가 hover됐을 때, <code>_groupHover</code>속성이 활성화되어 직접 hover되지 않은 자식 요소에 hover 효과를 부여할 수 있다</strong>고 한다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/e68612b6-e750-479b-b95a-f6c79104d1c9/image.gif" alt="영상 컴포넌트 hover 애니메이션 gif"></p>
<h2 id="적용-코드-예시">적용 코드 예시</h2>
<p><img src="https://velog.velcdn.com/images/juice-han/post/21e48965-4fc4-407e-b414-32a8cf275db5/image.png" alt="group 클래스 적용 코드 예시"></p>
<p>작성한 코드의 일부다.</p>
<p>맨 위, 부모 Flex 요소에 <code>className=&#39;group&#39;</code>을 설정했다.
자식 Box 요소엔 <code>_groupHover</code> 로 패딩과 bg를 조절하는 코드가 있다.
이렇게 설정하면 결과적으로 Flex의 자식 요소 전체 중 일부를 hover 했을 때 <code>_groupHover</code>가 활성화되어 padding과 bg가 변한다.</p>
<p>이 내용은 chakra ui mcp 서버를 통해 가져온 정보라 공식 문서에서 자세한 내용을 직접 확인하고 싶었다.
하지만 chakra ui 공식문서를 찾아봐도 class에 대한 정보는 찾을 수 없었다.</p>
<p>이에 관해 gemini는 chakra ui가 <strong>내부적으로 panda css 엔진을 채택해서 사용하기 때문에 class를 통한 스타일링을 지원</strong>한다라고 알려줬다.
일단 그냥 쓰다가 더 공부가 필요하면 그때 찾아보려고 한다.</p>
<p>추가로, _groupHover 외에도 다양한 속성이 있어서 나중에 활용해보면 좋을 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/51e89362-85cc-45da-86fc-f86c528e180f/image.jpg" alt="_group 속성"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 화면 요소 사이즈 변화를 감지하고 콜백 함수를 실행해주는 ResizeObserver]]></title>
            <link>https://velog.io/@juice-han/React-ResizeObserver</link>
            <guid>https://velog.io/@juice-han/React-ResizeObserver</guid>
            <pubDate>Sat, 04 Apr 2026 04:47:00 GMT</pubDate>
            <description><![CDATA[<p>토이 프로젝트 개발 중에 특정 div의 <strong>너비 변화를 감지하고 그때마다 함수를 실행해야하는 상황</strong>이었다.</p>
<p><code>addEventListener(&#39;resize&#39;, () =&gt; {})</code>로 이벤트를 등록해야하는 줄 알았는데 gemini에게 물어보니 또다른 방법이 존재했다.</p>
<p>바로 <code>ResizeObserver</code>라는 객체다.</p>
<p>이 객체는 화면 요소의 사이즈 변화를 감지했을 때 콜벡함수를 자동으로 실행해준다.</p>
<p>리액트 내장 객체인 줄 알았는데, 그게 아니라 <strong><code>import</code> 없이도 사용할 수 있었다.</strong></p>
<h2 id="resizeobserver사용-방법">ResizeObserver사용 방법</h2>
<p>사용 방법은 간단하다.</p>
<ol>
<li>사이즈 변화 요소의 <code>ref</code>를 생성한다.</li>
<li><code>ResizeObserver</code> 인스턴스를 생성하고 콜벡함수를 전달한다.</li>
<li><code>ResizeObserver</code> 인스턴스의 <code>observe()</code> 메서드에 변화 요소 <code>ref.current</code>를 전달한다.</li>
<li>인스턴스의 <code>disconnect()</code>를 실행하여 감지를 해제한다.</li>
</ol>
<p>실제 코드로 사용법을 확인하면 이해하기 쉽다.</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {

    // resize 이벤트 발생시 실행할 콜백 함수
    const callbackFunc = () =&gt; {}

    // ResizeObserver 인스턴스 생성 &amp; 콜백 함수 등록
    const observer = new ResizeObserver(() =&gt; {
      callbackFunc()
    })

    // observer 감지 시작
    observer.observe(scrollAreaRef.current)

    // observer 감지 해제
    return () =&gt; observer.disconnect()
  }, [])</code></pre>
<p>다른 페이지로 넘어갔을 때 감지를 멈추기 위해
<code>useEffect()</code>안에서 <code>ResizeObserver</code>를 생성하고 클린업 함수에서 감지 해제 함수를 실행했다.</p>
<p>이렇게 하면 메모리 누수를 막을 수 있다.</p>
<h2 id="적용-예시">적용 예시</h2>
<p>Chakra UI로 유튜브 클론 코딩 중, 필터 버튼 리스트를 구현할 때 <code>ResizeObserver</code>를 적용했다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/43830aae-7da5-45a5-aad5-43e043cdc8a3/image.gif" alt="ResizeObserver 적용 예시 gif"></p>
<p>위 gif에서 스크롤 박스 크기가 일정 크기 이상 줄어들면 화살표 버튼이 표시된다. 
물론 크기가 커지면 그 반대로도 작동한다.</p>
<p>이는 스크롤 박스의 크기가 스크롤 width 보다 작아질 경우 버튼을 표시하는 함수를 <code>ResizeObserver</code>에 등록한 것이다.</p>
<p>이렇게 <strong>화면 요소의 사이즈 변화를 감지하고 특정 함수를 실행할 때</strong> <code>ResizeObserver</code>를 활용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS S3 버킷과 IAM 사용자 생성하기]]></title>
            <link>https://velog.io/@juice-han/aws-s3-iam</link>
            <guid>https://velog.io/@juice-han/aws-s3-iam</guid>
            <pubDate>Wed, 01 Apr 2026 10:20:43 GMT</pubDate>
            <description><![CDATA[<p>백엔드에서 이미지 파일을 받아 처리하는 로직을 구현하고 있었다.</p>
<p>기존엔 이미지를 받고 나서 buffer에 담긴 정보를 <code>toString(&#39;base64&#39;)</code>로 인코딩하여 db에 저장했었다.</p>
<pre><code class="language-javascript">  @Post()
  @UseInterceptors(FileInterceptor(&#39;book-image&#39;))
  async createBookReview(
    @UploadedFile() bookImage: Express.Multer.File,
    @Body() bookDto: BookReviewDto,
  ) {
    bookDto.bookImage = bookImage.buffer.toString(&#39;base64&#39;)
    await this.bookReviewService.createBookReview(bookDto);
  }</code></pre>
<p>이렇게 구현하니 프론트에서 이미지를 불러오고 렌더링 하기까지 5초가 걸렸다.</p>
<p>왜 이렇게 오래 걸린걸까?</p>
<p>gemini한테 물어보니, <strong>base64 인코딩은 이미지 저장 용량을 30%까지 늘리기 때문</strong>에 데이터베이스 저장이나 이미지 불러오는 시간 측면에서 단점이 많다고 한다.</p>
<p>gemini가 내놓은 해답은 이렇다.</p>
<blockquote>
<p> 이미지는 S3에 저장하고 저장된 이미지 url만 db에 저장하세요. </p>
</blockquote>
<p>S3는 한 번도 세팅해보지 않아서 gemini의 도움을 많이 받았다</p>
<p>먼저 AWS S3 버킷을 만들어야 한다.</p>
<h2 id="aws-s3-버킷-만들기">AWS S3 버킷 만들기</h2>
<h3 id="버킷-생성">버킷 생성</h3>
<p>AWS에 접속한 후 S3에 들어간다.</p>
<p>주황색 버킷 만들기를 누르고 초기 세팅을 진행한다.</p>
<ol>
<li>일반 구성: Global namespace 그대로 둔다.</li>
<li>버킷 이름: 마음대로 지정한다.(그렇다고 막 짓진 말고 서비스와 관련해서 짓자)</li>
<li>ACL 비활성화됨을 유지한다.</li>
<li><strong>모든 퍼블릭 액세스 차단 선택을 풀어주고 하단 경고창에 체크표시를 한다.(이 설정을 해야 외부에서도 파일 접근이 가능하다)</strong></li>
<li>버킷 버전 관리, 기본 암호화도 기본 설정을 유지한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/193e4d55-9c0d-4354-8284-7a708db71000/image.jpg" alt="버킷 생성 페이지"></p>
<p>이렇게 세팅을 마치고 버킷 만들기를 누르면 버킷이 생성된다.</p>
<h3 id="버킷-정책-설정">버킷 정책 설정</h3>
<p>버킷 생성이 완료됐으면 정책을 설정해야 한다.</p>
<ol>
<li>생성된 버킷을 클릭하고 상단 권한 탭에 들어와 버킷 정책을 편집하면 된다.</li>
<li>정책에 빈 json 내용이 있으면 거기에 작성하면 되고 없다면 우측에 새 문 추가를 누른다.</li>
<li><pre><code class="language-json">{
 &quot;Version&quot;: &quot;2012-10-17&quot;,
 &quot;Statement&quot;: [
     {
         &quot;Sid&quot;: &quot;PublicReadGetObject&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:::{본인 버킷 명}/*&quot;
     }
 ]
}</code></pre>
<code>{ }</code> 자리에 본인 버킷 명을 그대로 작성해주면 된다.(물론 <code>{</code>,<code>}</code>는 뺀다)</li>
</ol>
<p>이후 변경 사항 저장을 누른다.</p>
<p>이렇게 버킷 설정은 완료됐다.</p>
<h2 id="iam-생성하기">IAM 생성하기</h2>
<h3 id="사용자-생성">사용자 생성</h3>
<p>만들어놓은 버킷에 접근 권한이 있는 사용자를 생성한다.</p>
<p>여기서 가졌던 의문</p>
<blockquote>
<p>루트 계정으로 다 접근할 수 있게 해야하는 거 아닌가? 왜 굳이 사용자를 또 나눠서 관리하지?</p>
</blockquote>
<p>gemini의 대답</p>
<ol>
<li>최소 권한 원칙: <strong>루트 계정이 털리면 모든 서비스에 피해가 간다</strong>. 필요에 맞게 최소한의 권한으로 사용자를 설정해서 사용하는 게 안전하다.</li>
<li>책임 소재 파악: 루트 계정만 사용할 경우 <strong>로그 관리가 어렵다.</strong> 여러 부서가 한 계정을 사용할 경우 실수로 DB를 날려도 어떤 부서가 잘못한 건지 알 수 없다.</li>
</ol>
<p>개인적으로 aws를 사용하는 분들에겐 첫 번째 이유가 가장 중요할 것 같다.</p>
<p>어쨋든, IAM 사용자 탭에 들어와서 사용자 생성을 누른다.</p>
<ol>
<li>사용자 이름을 설정한다.</li>
<li>권한 옵션에서 직접 정책 연결을 선택한다.</li>
<li>하단 권한 정책 검색창에 s3full을 입력하면 아래에
<code>AmazonS3FullAccess</code> 옵션이 나오는데 왼쪽 체크박스를 클릭하고 다음 버튼을 누른다.</li>
<li>바로 다음 페이지에서 사용자 생성 버튼을 누른다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/dc11b998-dbfb-4e33-afbb-df25c96ebd67/image.jpg" alt="사용자 권한 설정"></p>
<h3 id="액세스-키-발급">액세스 키 발급</h3>
<p>사용자를 만들었으니 이 권한을 사용할 수 있는 액세스 키를 발급받아야 한다.</p>
<p>여기에 나오는 <strong>키는 생성된 직후만 조회할 수 있기 때문에 안전한 개인 파일에 보관하는 게 중요</strong>하다.</p>
<ol>
<li>생성된 사용자를 클릭하고 하단 액세스 키 탭의 액세스 키 만들기 버튼을 누른다.</li>
<li><code>AWS 외부에서 실행되는 애플리케이션 옵션</code>을 클릭하고 다음을 누른다.</li>
<li>태그는 생략하고 액세스 키 만들기를 누른다.</li>
<li>이후에 하단에 나온 <strong>액세스 키와 비밀 액세스 키를 꼭 저장해둔다.</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/2b479053-e0c0-4663-9aeb-94d58726257d/image.jpg" alt="액세스 키를 저장해놔야함"></p>
<p>이렇게 S3 버킷과 IAM 사용자 생성이 완료됐다.</p>
<p>이제 버킷명, IAM 사용자 액세스 키와 비밀 액세스 키를 본인 서비스 .env파일에 저장하고 S3를 사용하면 된다.</p>
<p>끝. 생각보다 쉬웠다.</p>
<p><del>gemini 말대로 설정했을 때 잘 된 경우가 많이 없었는데, 최근에 좀 똑똑해졌나보다</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안티그래비티 사용 후기]]></title>
            <link>https://velog.io/@juice-han/antigravity-review</link>
            <guid>https://velog.io/@juice-han/antigravity-review</guid>
            <pubDate>Mon, 30 Mar 2026 11:32:24 GMT</pubDate>
            <description><![CDATA[<h2 id="안티그래비티">안티그래비티</h2>
<p>오늘 처음으로 안티그래비티라는 에디터를 설치해서 사용해봤다.
이 에디터는 기존 vscode, cursor와 어떤 점이 다른 지가 궁금했다.</p>
<h3 id="좋았던-점">좋았던 점</h3>
<ol>
<li>처음 사용해보고 좋았던 건 agent manager 창이 따로 있다는 점이다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/4d5261ad-57c9-484d-af22-d1d2a2dc015f/image.jpg" alt="agent manager"></p>
<p>chatgpt와 gemini 와 비슷한 UI를 갖고 있어 사용법을 익히는 건 어렵지 않았다.
웹 브라우저에서 LLM을 사용하는 것과 크게 다르지 않다. 그래서 편했다.</p>
<ol start="2">
<li>사용가능한 모델 종류와 사용량</li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/f0de70e8-c01f-4f27-83fb-5c91931f2765/image.jpg" alt="ai models and usage"></p>
<p>gemini 3.1 pro부터 gpt까지 여러 모델을 사용할 수 있다.
평소에 코딩할 때 클로드 코드를 이용하는데, 여기서도 클로드 코드 모델(sonnet, opus)을 사용할 수 있어서 좋았다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/d1ea0b22-b558-4745-94a8-7cb5595e54a0/image.jpg" alt="model usage limits"></p>
<p>사용량 부분에서도 이점이 있었다.</p>
<p>gemini pro(High)는 plan 모드로 2~3번 사용했을 때 전체 사용량의 20%가 줄었고
gemini pro(Low)도 plan 모드로 5번 정도 사용했는데 20% 줄었다.
gemini flash 모델로는 가벼운 질문을 10번 넘게 했던 거 같은데 사용량은 20% 줄어있었다.</p>
<p>안티그래비티가 최근에 나온 에디터라서 그런지 무료 플랜인데도 사용량을 많이 주는 것 같다.</p>
<p>더구나 모델별로 사용량을 제한하기 때문에 한 모델의 사용량을 전부 써도 나머지 모델을 쓰면 돼서 가벼운 코딩에는 결제하지 않아도 될 정도로 사용량이 넉넉하다.</p>
<ol start="3">
<li>mcp 서버 연결</li>
</ol>
<p>테스트를 위해 노션 mcp 서버를 연결을 시도해봤다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/eaba6364-9dfc-42a8-85ce-01b59c83694f/image.jpg" alt="mcp server"></p>
<p>mcp store에서 검색 후 install 버튼을 누른 다음 API 키만 입력하니 연결이 됐다.
mcp 연결이라하면 과정이 되게 복잡할 것 같았는데, 익스텐션 설치해서 사용하는 것처럼 편하게 설정할 수 있어서 좋았다.</p>
<h3 id="불편했던-점">불편했던 점</h3>
<p>폴더에서 사진을 끌어다 에디터 폴더에 붙여넣기 하려고 했는데 paste to agent라고 나오면서 잘 되지 않았다.
구글링을 해봐도 그냥 cmd+c, cdm+v 하라는 말만 나왔다.</p>
<p>서비스가 배포된 지 얼마 되지 않아서 그런가. 오류가 좀 남아있는 듯하다.</p>
<h2 id="결론">결론</h2>
<p>vscode UI와 크게 다른 점이 없고, 세팅도 똑같이 사용할 수 있어 코딩할 때 이질감이 들지 않았다.
cursor를 선택하지 않은 이유가 vscode와 이질감이 들어서인데, 안티그래비티는 그러지 않았다.</p>
<p>또한, vscode를 쓸 땐, AI를 잘 안 썼는데, 안티그래비티에선 이상하게 자주 사용하게됐다.
AI agent를 사용하기 편하게 만들어놔서 그런 것 같다.</p>
<p>앞으로도 계속해서 안티그래비티를 사용해보려고 한다.
지금 클로드 코드를 결제해서 사용 중인데, 괜찮다면 나중에 안티그래비티 유료 플랜으로 넘어갈 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Django] PyCharm으로 프로젝트 생성&실행하기]]></title>
            <link>https://velog.io/@juice-han/Django-PyCharm%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@juice-han/Django-PyCharm%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 21 Jun 2025 22:09:12 GMT</pubDate>
            <description><![CDATA[<p>이번엔 PyCharm에서 장고 프로젝트를 생성해보자.
PyCharm은 <strong>가상환경(venv)을 자동으로 생성</strong>해주는 기능이 있어 편리하다.</p>
<h1 id="프로젝트-생성-및-설정하기">프로젝트 생성 및 설정하기</h1>
<h2 id="프로젝트-생성">프로젝트 생성</h2>
<p>PyCharm을 실행하면 기본 화면이 나오는데 여기서 New Project 버튼을 누른다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/8a7b8c9d-ca4e-490d-80f9-8e2c5df06dae/image.png" alt="PyCharm 실행 화면"></p>
<h2 id="프로젝트-세부-설정">프로젝트 세부 설정</h2>
<p>그 다음으로는 프로젝트 세부 설정을 해야한다.</p>
<ol>
<li><strong>프로젝트명 수정</strong>
Location을 보면 기본적인 프로젝트 명은 PythonProject로 설정되어 있을 것이다. 이것을 본인이 원하는 프로젝트 명으로 바꿔주면 된다</li>
<li><strong>본인이 원하는 Python 버전 선택(3버전 이상으로)</strong></li>
</ol>
<p>모두 완료하면 다음 화면처럼 보인다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/826bfa42-c759-4424-adad-3588cc35e1a4/image.png" alt="PyCharm 프로젝트 세부 설정 화면"></p>
<p>설정 완료 후 우측 하단 Create 버튼을 누르면 PyCharm이 프로젝트를 생성해준다.</p>
<h2 id="장고-모듈-설치">장고 모듈 설치</h2>
<p><img src="https://velog.velcdn.com/images/juice-han/post/e75aedf3-cffe-442f-a3db-bfc611e1d662/image.png" alt="파이참 터미널 창"></p>
<p>장고를 개발하려면 장고 모듈이 필요하다.
터미널을 열고 아래의 명령어를 입력하여 장고 모듈을 설치해보자.</p>
<pre><code>pip install django</code></pre><p>버전을 따로 명시해주지 않으면 최신 버전으로 설치된다.
버전을 설정하는 방법은 다음과 같다.</p>
<pre><code>pip install django==5.2.1</code></pre><p><img src="https://velog.velcdn.com/images/juice-han/post/ee10e1ee-b8cd-4ea4-bea5-79b0aa3bd87d/image.png" alt="장고 설치 성공시 터미널 창"></p>
<h2 id="장고-프로젝트-기본-구조-생성">장고 프로젝트 기본 구조 생성</h2>
<p>터미널을 열어 다음과 같이 명령어를 입력하자.</p>
<pre><code>django-admin startproject 프로젝트명 .</code></pre><p>현재 위치에 장고 프로젝트를 생성하라는 명령어다.
(프로젝트명은 python 모듈로 사용되기 때문에 &#39; &#39;(공백), -(하이픈)을 피하고 숫자로 시작하지 않는 이름을 사용해야 한다.)</p>
<p>이렇게 입력하면 다음과 같이 manage.py 파일과 프로젝트 폴더가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/1d53e958-e2cd-4d53-b5a9-5bb5a450c0f6/image.png" alt="장고 프로젝트 기본 구조 생성"></p>
<blockquote>
<p>manage.py는 말 그대로 프로젝트를 관리하는 데 사용되는 파일이다.
프로젝트 관련 설정(setting.py)을 자동으로 인식한다는 특징이 있다.</p>
</blockquote>
<h1 id="장고-실행하기">장고 실행하기</h1>
<p>이제 장고 서버를 실행해보자.</p>
<p>터미널에 다음과 같이 명령어를 입력하자.</p>
<pre><code>python manage.py runserver</code></pre><p>이는 <strong>장고 서버를 시작하라는 명령어</strong>다.
실행이 성공적으로 완료되면 터미널 창에 다음과 같이 표시가 된다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/ac2d9b2b-0ea8-4fb2-926e-45d6a595eb12/image.png" alt="장고 서버 시작시 터미널 창"></p>
<p>이제 브라우저를 열고 주소창에 127.0.0.1:8000(혹은 localhost:8000)을 입력하면 장고 웹사이트를 볼 수가 있다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/6f823c32-db1d-4ba3-ae65-939b948a0d2d/image.png" alt="장고 실행화면"></p>
<h1 id="-pycharm에서-django-편의-기능-활용하기">+ PyCharm에서 Django 편의 기능 활용하기</h1>
<p>파이참에선 장고 개발을 위한 여러 편의 기능을 제공해준다.
해당 기능을 사용하려면 따로 설정이 필요하다.</p>
<h2 id="enable-django-support-설정">Enable Django support 설정</h2>
<ol>
<li>PyCharm -&gt; Settings -&gt; Languages &amp; Framework -&gt; Django</li>
<li>Enable Django support를 체크</li>
<li>django project root에 현재 프로젝트 최상위 폴더 지정</li>
<li>Settings는 settings.py 파일을 지정</li>
</ol>
<p>(폴더나 파일을 지정할 때 각 입력칸 맨 오른쪽 끝에 있는 폴더 모양을 클릭하면 편하게 할 수 있다.)</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/0716dd8c-c2b6-430f-b260-72a59d527021/image.png" alt="Enable Django support창"></p>
<h2 id="서버-실행-및-디버깅-설정">서버 실행 및 디버깅 설정</h2>
<ol>
<li>화면 우측 상단 Current File을 누르고 Edit Configurations를 클릭</li>
<li>Add new run configuration을 누르고 Django Server를 찾아 클릭</li>
<li>Name은 본인 프로젝트 명으로 입력하고 Working directory는 프로젝트 최상위 폴더로 지정</li>
</ol>
<p><img src="https://velog.velcdn.com/images/juice-han/post/a47bb192-06de-40bb-8d5f-0d563271ea04/image.png" alt="장고 실행 환경 설정창"></p>
<p>이렇게 하면 우측 상단 시작 버튼▶️만 누르면 장고 서버를 시작할 수 있다. 장고 서버 중지 및 디버깅도 가능하다.</p>
<hr>
<p>파이참을 사용하면 개발이 편해진다.
<span style="color:green">장고를 개발할 땐 꼭 파이참을 사용하자.</span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Django] 개발 환경 설정하기(Mac 용)]]></title>
            <link>https://velog.io/@juice-han/Django-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0Mac-%EC%9A%A9</link>
            <guid>https://velog.io/@juice-han/Django-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0Mac-%EC%9A%A9</guid>
            <pubDate>Sat, 21 Jun 2025 11:12:13 GMT</pubDate>
            <description><![CDATA[<h1 id="파이썬-설치하기">파이썬 설치하기</h1>
<p>Django는 파이썬 기반 프레임워크이기 때문에 파이썬 설치가 필수다.</p>
<p>현재로서는 3.12 버전이 bugfix가 완료된 안정된 버전이기 때문에
다운로드 시점 기준 3.12 버전 중 최신 stable release 버전을 다운로드 받으면 된다.
(나는 3.12.10 버전을 다운로드 받았다.)</p>
<p><strong>파이썬 다운로드 링크</strong>: <a href="https://www.python.org/downloads/macos/">https://www.python.org/downloads/macos/</a></p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/bfd31bf6-6dba-4176-a442-ad4ab01e9a54/image.png" alt="파이썬 버전에 대한 정보"></p>
<p>(사진 출처: <a href="https://devguide.python.org/versions/">https://devguide.python.org/versions/</a>)</p>
<h1 id="가상-환경-세팅하기">가상 환경 세팅하기</h1>
<p>파이썬에서는 가상환경을 세팅해주는 라이브러리가 있다.
(사실 파이참을 사용하면 알아서 가상환경을 설정해주지만 직접 적용하는 법을 배워놔도 나쁠 건 없다.)</p>
<h2 id="가상환경이란">가상환경이란?</h2>
<p>개발하다보면 여러 프로젝트를 진행할 때가 있다. 만약 A와 B라는 프로젝트가 있다고 하자.
이 프로젝트는 C라는 라이브러리를 공통으로 사용한다.
이 때 A프로젝트에서는 2.xx버전을 사용하고 B프로젝트에서는 3.xx버전을 사용한다고 하면
<strong>라이브러리 버전을 프로젝트별로 관리할 필요성이 생긴다.</strong>
이때 사용하는 것이 <strong>가상환경</strong>이다.</p>
<p>가상환경을 사용하면 A프로젝트용 가상환경을 생성하여 2.xx버전을 다운받아 사용할 수 있고 마찬가지로 B프로젝트용 가상환경을 설정하여 3.xx버전을 다운받고 사용할 수 있는 것이다.</p>
<p>간단히 말해 <strong>라이브러리 버전 관리 시스템</strong>이다.</p>
<h2 id="가상환경-생성-방법">가상환경 생성 방법</h2>
<p>파이썬에서는 <strong>venv</strong>라는 라이브러리로 가상환경을 세팅할 수 있다.
(<strong>V</strong>irtual <strong>ENV</strong>ironment의 줄인말)</p>
<p>터미널을 열어 프로그래밍 작업 폴더에 들어가 venvs 폴더를 만들고 그곳으로 들어가보자.</p>
<pre><code class="language-shell">cd 작업폴더명
mkdir venv
cd venv</code></pre>
<p>그 후 다음 명령어를 실행해보자.</p>
<pre><code class="language-shell">python3 -m venv 가상환경이름</code></pre>
<p>(나는 가상환경을 venv1으로 설정하였다.)</p>
<p>이렇게 작성하면 내가 입력한 가상환경이름으로 가상환경 폴더 하나가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/8c8a4e10-10ef-4f83-a5df-f4901d233e76/image.png" alt="가상환경 생성"></p>
<h2 id="가상환경-활성화하기">가상환경 활성화하기</h2>
<p>생성한 가상환경 폴더로 들어간 다음 bin 폴더로 들어가자.</p>
<pre><code class="language-shell">cd venv1/bin</code></pre>
<p>그 후 다음 명령어를 실행해보자.</p>
<pre><code class="language-shell">source activate</code></pre>
<p>(source는 해당 실행파일을 실행하라는 명령어다.)</p>
<p>이렇게 하면 터미널에 다음과 같이 가상환경이 활성화되었다는 표시가 생긴다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/a6bd1137-61d5-4cf6-acbe-d5f5a98f0795/image.png" alt="가상환경 활성화"></p>
<p>(가상환경 활성화 표시는 터미널 환경마다 다를 수 있다.
나는 따로 oh my zch로 터미널 테마를 커스텀했기 때문에 위와 같이 보인다.)</p>
<h2 id="가상환경-비활성화하기">가상환경 비활성화하기</h2>
<p>활성화한 가상환경을 비활성화 하는 법은 간단하다.
터미널 창에 다음과 같이 명령어를 실행해보자.</p>
<pre><code class="language-shell">deactivate</code></pre>
<p><img src="https://velog.velcdn.com/images/juice-han/post/a7c773d8-40e7-4123-a905-840be70a583a/image.png" alt="가상환경 비활성화"></p>
<p>비활성화하면 활성화 되었다는 표시가 사라진다</p>
<h1 id="pycharm-ide-설치하기">PyCharm IDE 설치하기</h1>
<p>본격적으로 장고를 개발하기에 앞서 코딩 프로그램을 하나 설치해보자.</p>
<p>PyCharm은 JetBrains사에서 만든 파이썬 전용 통합개발환경 프로그램이다.
코드 편집, 코드 자동 완성, 데이터베이스 조회, 프로젝트 버전 관리 등 개발자가 편하게 개발할 수 있는 환경을 제공해준다.
(대학생이라면 학생 인증을 하고 무료로 Pro 버전을 사용할 수 있다. 기본 버전은 Community 버전이며 둘 중 어느 것으로 해도 상관없다.)</p>
<p><a href="https://www.jetbrains.com/pycharm/download/?section=mac">PyCharm IDE 공식 다운로드 링크 for Mac</a></p>
<p>다운로드가 완료되면 실행해보자.
아래 사진과 같이 새로운 프로젝트를 생성하거나 기존 프로젝트를 볼 수 있는 화면이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/6ae38596-420a-44b2-9787-1c40b5b726e6/image.png" alt="PyCharm 실행 사진"></p>
<p>다음 포스팅에선 장고 프로젝트를 생성하고 실행해보는 시간을 가져보겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Spring Security 인증, 인가 예외 처리 클래스 커스텀해서 사용하기]]></title>
            <link>https://velog.io/@juice-han/Spring-Boot-Spring-Security-%EC%9D%B8%EC%A6%9D-%EC%9D%B8%EA%B0%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@juice-han/Spring-Boot-Spring-Security-%EC%9D%B8%EC%A6%9D-%EC%9D%B8%EA%B0%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Jan 2025 18:50:26 GMT</pubDate>
            <description><![CDATA[<p>인증 받지 않은 유저, 인가 받지 않은 유저를 필터링 하는 좋은 방법이 있어
글로 정리해보고자 한다.</p>
<p>글에서 나오는 <code>SecurityConfig</code> 클래스는 <code>filterChain</code> 메서드를 작성하는 
<code>Spring Security</code>의 환경 설정 클래스이다. </p>
<p>클래스명은 <strong>사람마다 다를 수 있다는 점</strong>을 알아주기 바란다.</p>
<blockquote>
<p>사용 버전 정보</p>
</blockquote>
<ul>
<li>Spring Boot: 3.4.1</li>
<li>Spring Security: 6.4.2</li>
<li>Java: 17</li>
</ul>
<h1 id="인증받지-않은-유저-처리">인증받지 않은 유저 처리</h1>
<p>인증 받지 않은 유저를 처리할 수 있게 해주는 인터페이스가 존재한다.</p>
<p>바로 <code>AuthenticationEntryPoint</code>라는 인터페이스다.</p>
<p>이 인터페이스를 구현하는 클래스를 만들어 Spring Security에 등록하면
<strong>인증 받지 않은 유저에 대한 예외 처리가 가능</strong>해진다.
<em>ex) 응답 날리기, 리디렉션 하기 등</em></p>
<p>코드를 보며 예시를 살펴보자.</p>
<h2 id="authenticationentrypoint-구현-클래스-작성">AuthenticationEntryPoint 구현 클래스 작성</h2>
<pre><code class="language-java">@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType(&quot;application/json&quot;);
        response.setCharacterEncoding(&quot;UTF-8&quot;);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(&quot;{\&quot;error\&quot;: \&quot;Unauthorized\&quot;, \&quot;message\&quot;: \&quot;로그인이 필요합니다.\&quot;}&quot;);
    }
}</code></pre>
<p><code>@Component</code> 어노테이션을 사용하여 빈으로 등록했다.
이는 <code>SecurityConfig</code> 클래스에서 주입 받아 사용하려는 목적을 가진다.</p>
<p><code>AuthenticationEntryPoint</code> 인터페이스를 구현하려면 <code>commence</code>라는 메서드를 오버라이딩 해야된다.</p>
<p>나는 <code>response</code>에 Content-Type, Encoding, status 설정을 하고 메세지를 작성해주었다.</p>
<p>이렇게 메서드 내용을 작성한 후 <code>SecurityConfig</code> 클래스에 등록해주면 된다.</p>
<p>등록하는 과정은 인가 관련 클래스를 작성 후 보여주겠다.</p>
<h1 id="인가받지-않은-유저-처리">인가받지 않은 유저 처리</h1>
<p>인가받지 않은 유저를 처리할 수 있게 해주는 인터페이스가 존재한다.</p>
<p>바로 <code>AccessDeniedHandler</code> 인터페이스다.</p>
<p>이 인터페이스도 마찬가지로 구현하는 클래스를 작성 후 <code>SecurityConfig</code> 클래스에 등록해주면
<strong>인가 받지 않은 유저에 대한 예외처리</strong>를 할 수 있게 된다.</p>
<h2 id="accessdeniedhandler-구현-클래스-작성">AccessDeniedHandler 구현 클래스 작성</h2>
<pre><code class="language-java">@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType(&quot;application/json&quot;);
        response.setCharacterEncoding(&quot;UTF-8&quot;);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(&quot;{\&quot;error\&quot;: \&quot;Unauthorized\&quot;, \&quot;message\&quot;: \&quot;권한이 없습니다.\&quot;}&quot;);
    }
}</code></pre>
<p>이 클래스도 <code>@Component</code> 어노테이션을 사용해 빈으로 등록했다.</p>
<p><code>AccessDeniedHandler</code> 인터페이스에는 구현해야할 <code>handle</code> 메서드가 존재한다.</p>
<p><strong>해당 메서드 안에 인가 받지 않은 유저에 대한 처리를 작성하면 된다.</strong></p>
<p>아까와 마찬가지로 <code>response</code> 에 여러 옵션을 설정해줬다.</p>
<p>이렇게 인증과 인가 관련 예외 처리 클래스 작성이 완료됐다.</p>
<h1 id="securityconfig-클래스에-등록">SecurityConfig 클래스에 등록</h1>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // ~~생략~~

        http
                .exceptionHandling((exceptions) -&gt; exceptions
                        .authenticationEntryPoint(customAuthenticationEntryPoint)
                        .accessDeniedHandler(customAccessDeniedHandler)
                );

        // ~~생략~~

        return http.build();
    }
}</code></pre>
<p><code>SecurityConfig</code> 클래스의 <code>filterChain</code> 메서드 안에 등록하면 된다.</p>
<p><code>HttpSecurity</code> 객체가 갖는 <code>exceptionHandling</code> 메서드 안에 람다식을 작성하고
<strong>구현하는 인터페이스 명과 일치하는 메서드</strong>에 알맞은 객체를 전달해준다.</p>
<blockquote>
<p>이렇게 하면 <strong>인증되지 않은 사용자가 접근하려할 때 혹은
인가받지 않은 사용자가 특정 자원에 접근하려할 때의 예외 처리가 가능</strong>해진다.</p>
</blockquote>
<h1 id="사용-이유">사용 이유</h1>
<p>Oauth2 로그인을 구현하는 도중,
해당 클래스를 작성하기 전에는 인증 받지 않은 사용자가 접근했을 때
의도치 않게 로그인 페이지로 리다이렉션되는 어려움을 겪었다.</p>
<p>리다이렉션을 방지하고 메세지와 오류 상태코드를 반환하고자 사용하였다.</p>
<p>스프링 시큐리티의 편리함과 기능의 다양성을 느낄 수 있었던 좋은 경험이었다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://chb2005.tistory.com/177">chb2005님의 블로그글</a>
해당 자료가 2년 전 자료라 현재 deprecated된 게 존재해서 코드를 살짝 수정하였다.
SecurityConfig 클래스에 등록 방법: 메서드 체이닝 -&gt; 람다식 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] csrf 공격과 대응 방법]]></title>
            <link>https://velog.io/@juice-han/Spring-Boot-csrf-%EA%B3%B5%EA%B2%A9%EA%B3%BC-%EC%98%88%EB%B0%A9-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@juice-han/Spring-Boot-csrf-%EA%B3%B5%EA%B2%A9%EA%B3%BC-%EC%98%88%EB%B0%A9-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 12 Nov 2024 08:18:33 GMT</pubDate>
            <description><![CDATA[<h2 id="csrf-공격이란">csrf 공격이란?</h2>
<p>= Cross-Site Request Forgery</p>
<p>웹 애플리케이션 취약점 중 하나로 <strong>사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 해서</strong> 특정 웹 페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법</p>
<h2 id="공격-과정">공격 과정</h2>
<ol>
<li>공격자가 CSRF 스크립트가 담긴 게시글을 작성한다.</li>
<li>다른 사용자가 해당 게시글에 접근한다.</li>
<li>CSRF 스크립트가 사용자의 권한으로 실행된다</li>
</ol>
<h2 id="예시">예시</h2>
<p>공격자가 해당 사이트의 url 패턴을 분석하여 계정 정보를 바꾸는 url을 알아낸다.</p>
<p>그 후 자신이 원하는대로 계정을 바꾸는 링크를 게시글이나 메일에 올려서 사용자가 접속하도록 한다.</p>
<pre><code>http://example.com/user/update?user=abcd&amp;password=1234</code></pre><p>이 링크를 누른 사용자의 아이디는 공격자가 미리 지정해놓은 대로 바뀌게 되고, 이를 통해 해킹이 가능하게된다.</p>
<h2 id="예방-방법">예방 방법</h2>
<p>서버에 http 요청을 보낼 때 특정 토큰을 함께 보내도록 강제해서 사용자가 의도한 요청만 실행되도록 한다.</p>
<p>서버에서 사용자에게 웹페이지를 전달할 때 토큰을 전달하고, 사용자가 그 페이지에서 http 요청을 보내면 전달받은 토큰을 함께 받아 토큰 값을 검증하는 방식이다.</p>
<pre><code class="language-html">&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
    &lt;title&gt;Join Page&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
join page
&lt;br&gt;
&lt;form action=&quot;/joinProc&quot; method=&quot;post&quot; name=&quot;joinForm&quot;&gt;
    &lt;input id=&quot;username&quot; type=&quot;text&quot; name=&quot;username&quot; placeholder=&quot;id&quot;/&gt;
    &lt;input id=&quot;password&quot; type=&quot;text&quot; name=&quot;password&quot; placeholder=&quot;password&quot;/&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;_csrf&quot; value=&quot;{{_csrf.token}}&quot;&gt; &lt;!-- 요청할 때 토큰을 함께 보낸다. --&gt;
    &lt;input type=&quot;submit&quot; value=&quot;join&quot;/&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>회원가입 페이지의 html 코드이다.</p>
<p>서버는 해당 페이지를 사용자에게 보낼 때 토큰값을 끼워넣어 함께 보내고, 사용자가 http 요청을 하면 다시 토큰을 받아 이전에 보냈던 토큰과 일치하는 지 확인한다.</p>
<p>이를 통해 사용자는 항상 자신이 의도한 요청만 할 수 있게 된다.</p>
<h2 id="spring-security에서의-csrf-대응-방법">Spring Security에서의 csrf 대응 방법</h2>
<p>스프링 시큐리티는 기본적으로 csrf 토큰을 전달해서 공격을 방어한다.</p>
<p>그래서 처음 스프링 시큐리티를 배울 때 이 옵션을 disable하라고 한다. 왜냐하면 추가적으로 해야할 일이 생기기 때문이다.</p>
<pre><code class="language-java">http
        .csrf((auth) -&gt; auth.disable());</code></pre>
<p>(config 클래스에서 해당 코드를 작성하면 csrf 옵션이 비활성화된다.)</p>
<p>모든 폼 타입의 요청에서 csrf 토큰 코드를 작성하지 않으면 요청이 받아들여지지 않는다.</p>
<p>따라서 초보자는 처음 배울 때 해당 옵션을 비활성화 하고 나중에 숙련이 되면 코드를 추가하는 식으로 csrf 설정을 하게된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Spring Security 세션로그인 구현 방법]]></title>
            <link>https://velog.io/@juice-han/Spring-Boot-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B3%BC%EC%A0%95-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@juice-han/Spring-Boot-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B3%BC%EC%A0%95-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 31 Oct 2024 14:55:32 GMT</pubDate>
            <description><![CDATA[<p>스프링 시큐리티를 적용하며 복잡하게 느껴졌던 세션 로그인 구현 과정을 정리해보고자 글을 작성하게 되었습니다. 다음은 제가 프로젝트에 적용했던 버전 정보입니다.</p>
<blockquote>
<p>Spring Boot : 3.3.5 버전
Java : 17 버전</p>
</blockquote>
<h1 id="📕-스프링-시큐리티란">📕 스프링 시큐리티란?</h1>
<p><strong>인증&amp;인가를 편하게 구현할 수 있도록 스프링에서 제공해주는 보안 관련 프레임워크</strong>입니다.</p>
<h2 id="📗-인증과-인가authentication-authorization">📗 인증과 인가(Authentication&amp; Authorization)</h2>
<p>인증과 인가라는 단어를 자주 들어보셨을테지만 헷갈리는 개념이라 짚고 넘어가겠습니다.</p>
<blockquote>
<p>인증(Authentication) : 사용자가 <strong>누구</strong>인지 검증하는 과정
인가(Authorization) : 사용자의 <strong>역할에 따라 권한을 부여</strong>하는 과정</p>
</blockquote>
<p>서버를 회사라고 비유해보겠습니다.</p>
<p>회사 출입 카드를 사용하여 회사에 들어가는 과정 = <strong>인증</strong>
직급에 따라 출입가능한 사무실이 다르게 배치되는 것 = <strong>인가</strong></p>
<p>이렇게 생각하면 이해하기 쉬울 겁니다.</p>
<h3 id="스프링-시큐리티-인증-과정">스프링 시큐리티 인증 과정</h3>
<p>전체적인 인증 과정을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/bfc74aab-2dd8-449f-a19b-b54101450186/image.png" alt="스프링 시큐리티 인증 내부 구조"></p>
<p>스프링 시큐리티의 인증 내부 구조입니다. 간단한 흐름은 다음과 같습니다.</p>
<ol>
<li>사용자가 로그인 요청을 보냈을 때 인증 필터가 요청을 받습니다.</li>
<li>인증 매니저에게 요청과 관련된 유저 정보를 가져오도록 합니다.</li>
<li>UserDetailsService에서 유저 정보를 찾아 인증 매니저에게 전달합니다.</li>
<li>요청 정보와 찾아낸 유저 정보를 비교하여 일치한다면 인증 객체를 생성하고
SecurityContext에 저장합니다. (= 세션 저장)</li>
</ol>
<h1 id="📕-작성할-코드">📕 작성할 코드</h1>
<p>내부구조를 보면 엄청 복잡해보이지만 사실 구현할게 그리 많지 않습니다.</p>
<h2 id="📗-추가할-의존성dependency">📗 추가할 의존성(dependency)</h2>
<pre><code class="language-java">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
}</code></pre>
<p><code>spring-boot-starter-security</code> 의존성 하나만 의존성 정보에 추가해주시면 됩니다.</p>
<h2 id="📗-security-config-파일">📗 Security Config 파일</h2>
<p>가장 중요한 설정 파일입니다. 이 파일에 여러가지 보안 관련된 설정을 할 수 있습니다. </p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // 인가(Authorization) 설정 코드
        http
                .authorizeHttpRequests((auth) -&gt; auth
                        .requestMatchers(&quot;/&quot;, &quot;/login&quot;,&quot;/loginProc&quot;,&quot;/join&quot;,&quot;joinProc&quot;, &quot;loginFail&quot;).permitAll()
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // static 자원에 대한 접근 모두 허용
                        .requestMatchers(&quot;/api/admin/**&quot;,&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                        .requestMatchers(&quot;/logout&quot;).hasAnyRole(&quot;ADMIN&quot;, &quot;USER&quot;)
                        .anyRequest().authenticated()
                );

        // 특정 url에 csrf 검증 비활성화
        http
                .csrf((auth) -&gt; auth
                        .ignoringRequestMatchers(&quot;/api/**&quot;)
                );

        // 로그인 관련 설정
        http
                .formLogin((auth) -&gt; auth
                        .loginPage(&quot;/login&quot;)
                        .loginProcessingUrl(&quot;/loginProc&quot;)
                        .failureForwardUrl(&quot;/loginFail&quot;)
                        .defaultSuccessUrl(&quot;/articles&quot;, true)
                        .permitAll()
                );

        // 로그아웃 관련 설정
        http
                .logout((auth) -&gt; auth
                        .logoutUrl(&quot;/logout&quot;)
                        .logoutSuccessUrl(&quot;/&quot;)
                );

        // 중복 로그인 처리
        http
                .sessionManagement((auth) -&gt; auth
                        .maximumSessions(1)
                        .maxSessionsPreventsLogin(true)
                );

        // 세션 고정 공격을 방어하기 위한 방법 - 공격자의 세션 id로 로그인 해도 새로운 세션 id가 발급되어
        // 공격자의 세션 id는 여전히 익명 사용자 세션이 됨
        http
                .sessionManagement((auth) -&gt; auth
                        .sessionFixation().changeSessionId());

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}</code></pre>
<p>뭔가 많아보이지만 하나씩 살펴보면 간단합니다.</p>
<h3 id="어노테이션-설정">어노테이션 설정</h3>
<p>먼저 <code>@Configuration</code>과 <code>@EnableWebSecurity</code>를 클래스 상단에 추가해주세요. 스프링 시큐리티 설정을 위한 어노테이션입니다.</p>
<h3 id="인가authorization-설정">인가(Authorization) 설정</h3>
<pre><code class="language-java">http
        .authorizeHttpRequests((auth) -&gt; auth
                .requestMatchers(&quot;/&quot;, &quot;/login&quot;,&quot;/loginProc&quot;,&quot;/join&quot;,&quot;joinProc&quot;, &quot;loginFail&quot;).permitAll()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // static 자원에 대한 접근 모두 허용
                .requestMatchers(&quot;/api/admin/**&quot;,&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .requestMatchers(&quot;/logout&quot;).hasAnyRole(&quot;ADMIN&quot;, &quot;USER&quot;)
                .anyRequest().authenticated()
        );</code></pre>
<p>인가 관련 코드를 작성하는 곳입니다.</p>
<p><code>requestMatchers</code>에 특정 url을 입력하면 해당 url에 대한 접근 권한을 설정할 수 있습니다.</p>
<p>자세히 보시면 <code>requestMatchers</code> 뒤에 <code>permitAll()</code>, <code>hasRole()</code>, <code>hasAnyRole()</code>이 붙어있는 것을 볼 수 있습니다.</p>
<ul>
<li>permitAll(): <strong>인증받지 않은 유저</strong>(로그인 하지 않은 유저)에게도 접근을 허용</li>
<li>hasRole(): <strong>특정 권한을 갖고 있는 유저</strong>에게만 접근을 허용</li>
<li>hasAnyRole(): 인수에 전달한 <strong>여러 권한 중 하나라도 가진 유저</strong>에게 접근을 허용</li>
</ul>
<p>예를 들어 </p>
<pre><code class="language-java">.requestMatchers(&quot;/api/admin/**&quot;,&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)</code></pre>
<p>이 코드는 <code>/api/admin/**</code>,<code>/admin/**</code> url 관련 요청은 ADMIN 권한을 가진 유저만 접근이 가능하다는 것을 의미합니다.</p>
<pre><code class="language-java">.anyRequest().authenticated()</code></pre>
<p>마지막에 작성된 이 코드는 위에 설정한 url 이외에 다른 모든 url에 대한 요청은 인증된 유저에게 접근을 허용한다는 뜻입니다.</p>
<h3 id="csrf-설정">csrf 설정</h3>
<pre><code class="language-java">// 특정 url에 csrf 검증 비활성화
http
        .csrf((auth) -&gt; auth
                .ignoringRequestMatchers(&quot;/api/**&quot;)
        );</code></pre>
<p>이는 <code>/api/**</code> url에 전송되는 요청은 csrf 검증을 하지 않는다는 것을 의미합니다.</p>
<pre><code class="language-java">  http
          .csrf(AbstractHttpConfigurer::disable);</code></pre>
<p>위와 같이 코드를 작성하여 csrf 검증을 아예 적용하지 않게 할 수 있습니다. 이는 주로 csrf 검증이 필요없는 REST API server를 만들 때 사용합니다. 
혹은 csrf 설정이 복잡한 초보자 분들이 이를 비활성화 할 때 사용합니다.</p>
<h3 id="로그인-로그아웃-설정">로그인, 로그아웃 설정</h3>
<pre><code class="language-java">// 로그인 관련 설정
http
        .formLogin((auth) -&gt; auth
                .loginPage(&quot;/login&quot;) // 로그인 페이지
                .loginProcessingUrl(&quot;/loginProc&quot;) // 로그인 요청을 받는 url
                .failureForwardUrl(&quot;/loginFail&quot;) // 로그인이 실패했을 때 이동하는 url
                .defaultSuccessUrl(&quot;/articles&quot;, true) // 성공했을 때 이동하는 url
                .permitAll()
        );

// 로그아웃 관련 설정
http
        .logout((auth) -&gt; auth
                .logoutUrl(&quot;/logout&quot;) // 로그아웃 요청을 받는 url
                .logoutSuccessUrl(&quot;/&quot;) // 로그아웃이 성공했을 때 이동하는 url
        );</code></pre>
<p>로그인과 로그아웃을 어떤 url을 통해 진행할건지 설정합니다.
또한 해당 과정이 실패하거나 성공했을 때 어떤 url로 이동할지도 설정합니다.</p>
<p>제가 작성한 것 외에 다른 여러 옵션도 많으니 직접 찾아보시면 다양한 설정을 하실 수 있을 것입니다.</p>
<h3 id="다중-로그인-설정">다중 로그인 설정</h3>
<pre><code class="language-java">// 중복 로그인 처리
http
        .sessionManagement((auth) -&gt; auth
                .maximumSessions(1) // 최대 다중 로그인 허용자 설정
                .maxSessionsPreventsLogin(true)); // 최대 허용자를 넘어선 로그인에 대해 금지하는 설정</code></pre>
<p>다중 로그인은 말 그대로 최대 몇 명까지 로그인을 허용할지를 결정하는 설정입니다.</p>
<h3 id="세션-고정-공격-설정">세션 고정 공격 설정</h3>
<pre><code class="language-java">// 세션 고정 공격을 방어하기 위한 방법 - 공격자의 세션 id로 로그인 해도 새로운 세션 id가 발급되어
// 공격자의 세션 id는 여전히 익명 사용자 세션이 됨
http
        .sessionManagement((auth) -&gt; auth
                .sessionFixation().changeSessionId());</code></pre>
<p>세션 고정 공격을 막는 설정입니다. 세션 고정 공격에 대한 내용을 여기서 설명하지는 않겠습니다. 따로 검색해보시면 자세히 알 수 있습니다.</p>
<p>간단하게 말하면 공격자가 사용자 브라우저에 자신의 세션을 심어두고 사용자가 로그인을 하면 자신도 사용자의 인증 권한을 얻게되는 공격 방법입니다.</p>
<p>이를 방어하기 위해 로그인을 했을 때 <strong>자신이 갖고있던 세션 아이디를 변경</strong>시켜주는 설정입니다.</p>
<h3 id="bcryptpasswordencoder-비밀번호-암호화-객체">BCryptPasswordEncoder (비밀번호 암호화 객체)</h3>
<p>스프링 시큐리티에서는 기본적으로 BCrypt 암호화 객체를 제공합니다. 
이를 통해 비밀번호를 암호화할 수 있습니다.</p>
<p>사용자의 비밀번호를 그대로 db에 저장하기보단 암호화를 하고 저장하는 게
보안상 좋습니다.</p>
<p>다음과 같이 회원가입 과정에서 사용합니다.</p>
<pre><code class="language-java">private final BCryptPasswordEncoder bCryptPasswordEncoder;

User user = User.builder()
                .username(joinDTO.getUsername())
                .password(bCryptPasswordEncoder.encode(joinDTO.getPassword()))
                .role(&quot;ROLE_USER&quot;)
                .build();

userRepository.save(user);
</code></pre>
<h2 id="📗-유저-엔티티">📗 유저 엔티티</h2>
<p>스프링 시큐리티가 유저 정보를 가져오기 위해 유저 엔티티에 추가설정을 해야합니다.</p>
<p>아마 기존에 유저 엔티티를 만들어 놓으셨을텐데 거기에 추가로 UserDetails 인터페이스를 구현해주시면 됩니다. </p>
<p>UserDetails 인터페이스를 구현함으로써 스프링 시큐리티가 유저 정보를 가져올 수 있도록 합니다.</p>
<pre><code class="language-java">@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    private String role; //admin이나 user 역할 부여용 컬럼

    @OneToMany(mappedBy = &quot;user&quot;)
    private List&lt;Article&gt; articleList;

    @Builder
    public User(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    // 여기서부터 UserDetails 인터페이스 구현 메서드
    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(role));
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword(){
        return password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}</code></pre>
<p>구현해야하는 메서드는 7개입니다.(@Override가 붙은 메서드만 보세요)</p>
<p>하나씩 살펴보겠습니다.</p>
<h3 id="getauthorities-메서드">getAuthorities 메서드</h3>
<pre><code class="language-java">@Override
public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
    return List.of(new SimpleGrantedAuthority(role));
}</code></pre>
<p>Config 파일에 대해 살펴볼 때 <strong>권한</strong>에 대해 언급했습니다. <code>&quot;ADMIN&quot;</code>, <code>&quot;USER&quot;</code>가 있었는데, <code>getAuthrities</code> 함수를 통해 스프링 시큐리티는 유저의 권한 정보를 얻습니다.</p>
<p>유저 정보를 반환하도록 코드를 작성합니다.</p>
<h3 id="getusername-getpassword-메서드">getUsername, getPassword 메서드</h3>
<pre><code class="language-java">@Override
public String getUsername() {
    return username;
}

@Override
public String getPassword(){
    return password;
}</code></pre>
<p>유저의 아이디와 비밀번호를 반환하도록 작성합니다.
사용자 인증을 하기위한 유저 정보를 가져올 때 사용됩니다.</p>
<h3 id="그외-4가지-메서드">그외 4가지 메서드</h3>
<pre><code class="language-java">@Override
public boolean isAccountNonExpired() { // 계정이 만료되었는지 반환
    return true;
}

@Override
public boolean isAccountNonLocked() { // 계정이 잠금되었는지 반환
    return true;
}

@Override
public boolean isCredentialsNonExpired() { // 계정 비밀번호가 만료되었는지 반환
    return true;
}

@Override
public boolean isEnabled() { // 계정이 활성화되어있는지 반환
    return true;
}</code></pre>
<p>만약 여러분이 계정에 대한 만료 기간이나 비밀번호 만료 기간을 설정해두셨다면 해당 기간을 검증하여 결과를 반환하는 로직을 작성하시면 됩니다.</p>
<p>하지만 그렇지 않고 일반적인 로그인 과정을 구현하고 싶으시다면 <code>true</code>를 반환하도록 합니다.</p>
<h3 id="role에-관하여">role에 관하여</h3>
<p>회원가입을 할 때 유저의 role을 설정해야할 겁니다.</p>
<p>이때 그냥 <code>&quot;ADMIN&quot;</code>, <code>&quot;USER&quot;</code>로 하는 게 아니라
<code>&quot;ROLE_ADMIN&quot;</code>, <code>&quot;ROLE_USER&quot;</code>로 설정해서 저장해야합니다.</p>
<p>그렇지 않으면 오류가 나올 것입니다. 꼭 지켜주세요.</p>
<h2 id="📗-customuserdetailsservice">📗 CustomUserDetailsService</h2>
<p>스프링 시큐리티에서는 로그인 요청이 오면 해당 아이디의 유저를 찾아서 비밀번호를 비교하고 검증한다고 했습니다.</p>
<p>이때 유저를 가져오는 일을 담당하는 서비스가 필요합니다.</p>
<p><code>UserDetailsService</code> 인터페이스를 구현하는 클래스를 만들고
메서드 한 개만 간단하게 구현해주시면 됩니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        return userRepository.findByUsername(username)
                .orElseThrow(()-&gt; new IllegalArgumentException(username));
    }
}</code></pre>
<h3 id="findbyusername-메서드">findByUsername 메서드</h3>
<p>말 그대로 username을 바탕으로 유저 정보를 찾는 함수입니다.
<code>UserRepository</code>에서 <code>username</code>을 통해 유저를 반환하는 함수를 작성하시면 됩니다.</p>
<pre><code class="language-java">Optional&lt;User&gt; findByUsername(String username);</code></pre>
<p>저는 UserRepository에 위의 코드를 추가하였습니다.</p>
<h2 id="📗-로그인-시도해보기">📗 로그인 시도해보기</h2>
<p>이를 통해 스프링 시큐리티 세션 로그인 구현이 완료되었습니다.</p>
<p>직접 로그인 페이지를 만들어 <code>form login</code> 방식으로 <code>username</code>과<code>password</code>를 담아 <code>POST</code> 요청을 보낸다면 로그인이 완료될 것입니다.</p>
<ul>
<li>html 예시<pre><code class="language-html">&lt;form id=loginForm action=&quot;/loginProc&quot; method=&quot;post&quot; name=&quot;loginForm&quot;&gt;
&lt;label for=&quot;username&quot;&gt;아이디&lt;/label&gt;
&lt;input id=&quot;username&quot; type=&quot;text&quot; name=&quot;username&quot; placeholder=&quot;id&quot;/&gt;
&lt;label for=&quot;password&quot;&gt;비밀번호&lt;/label&gt;
&lt;input id=&quot;password&quot; type=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;password&quot;/&gt;
&lt;/form&gt;</code></pre>
</li>
</ul>
<h1 id="📕-참고-자료">📕 참고 자료</h1>
<ul>
<li><a href="https://www.youtube.com/@xxxjjhhh">유튜브 개발자유미 채널</a> : 스프링 시큐리티에 대해 이해하기 쉽게 설명해주십니다. 재생목록에서 spring security를 찾아 보시면 도움이 되실 겁니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Controller와 Service 제대로 구분하기]]></title>
            <link>https://velog.io/@juice-han/Spring-Boot-Controller%EC%99%80-Service-%EA%B5%AC%EB%B6%84-%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@juice-han/Spring-Boot-Controller%EC%99%80-Service-%EA%B5%AC%EB%B6%84-%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 23 Oct 2024 06:19:30 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트의 Controller와 Service를 제대로 구분하지 못했던 과거의 나를 반성하고
앞으로는 코드를 제대로 작성하고자 내용을 정리해보려고 한다.</p>
<h2 id="스프링-계층layer">스프링 계층(layer)</h2>
<p><img src="https://velog.velcdn.com/images/juice-han/post/b847bc13-65ea-4da6-b860-8d3cdd0a317f/image.png" alt="스프링 계층"></p>
<p>스프링에는 계층이 존재한다.</p>
<p><strong>Client가 전송한 요청을 Controller, Service, Repository 순으로 처리</strong>하고 
다시 역순으로 데이터를 반환하며 응답을 전송한다.
이렇게 여러개의 계층을 나눈 까닭은 <strong>MVC 패턴에 맞춰 개발하기 위함</strong>이다. </p>
<p>이를 통해 <strong>협업의 편리, 유지보수 용이, 코드간 의존성 낮춤, 코드 중복 제거 등</strong>
여러 장점이 생긴다.</p>
<p>간단하게 Controller와 Service에 대해 알아보자.</p>
<h3 id="controller란">Controller란</h3>
<p>Controller는 클라이언트의 <strong>요청에 맞게 함수를 실행</strong>해주고, <strong>응답을 전송</strong>해주는 역할을 한다.
데이터베이스에 직접 접근하지 않고, 실행한 함수의 리턴 결과로 오는 데이터만 가지고 응답을 전송한다.</p>
<h3 id="service란">Service란</h3>
<p>Service는 말 그대로 <strong>서비스의 메인 로직을 수행</strong>하는 역할을 한다. 
Controller가 Service의 함수를 실행하면 Service는 Repository를 통해 데이터베이스에 접근하여
<strong>필요한 정보를 가져오고 가공</strong>해서 Controller에게 다시 전달한다.</p>
<h2 id="구분을-제대로-해야하는-이유">구분을 제대로 해야하는 이유?</h2>
<p>제대로 구분하지 않는다면 처음에 말했던 <strong>MVC 패턴을 따랐을 때 생기는 장점을 모두 잃게</strong> 된다.</p>
<p>Controller는 Controller의 역할에 맞게 코드를 작성하고
Service는 Service의 역할에 맞게 코드를 작성해야 하는 것이다.</p>
<blockquote>
<p>계층에 맞게 코드를 작성하여 효율적으로 코딩하는 게 중요</p>
</blockquote>
<p>내가 이전에 작성했던 코드를 보며 뭐가 잘못됐는지 확인해보자.</p>
<h2 id="내가-작성했던-잘못된-코드">내가 작성했던 잘못된 코드</h2>
<pre><code class="language-java">@PostMapping(&quot;/user/signUp&quot;)
public ResponseEntity&lt;MessageDTO&gt; signUp(@Valid @RequestBody UserSignUpRequestDTO request, BindingResult bindingResult) {
    MessageDTO messageDTO = new MessageDTO();

    if (bindingResult.hasErrors()) { // 입력받은 request body 값에 정상적인 값들이 들어있는지 확인
        List&lt;FieldError&gt; list = bindingResult.getFieldErrors();
        for (FieldError error : list) {
            messageDTO.setMessage(error.getDefaultMessage());
            return new ResponseEntity&lt;&gt;(messageDTO, HttpStatus.BAD_REQUEST);
        }
    }

    boolean isDuplicated = userService.checkEmailDuplication(request.getEmail()); // 아이디 중복 체크

    if (isDuplicated) {
        messageDTO.setMessage(&quot;중복된 아이디입니다.&quot;);
        return new ResponseEntity&lt;&gt;(messageDTO, HttpStatus.BAD_REQUEST);
    }

    try {
        userService.signUp(request); // 중복되지 않았으면 계정 생성
        messageDTO.setMessage(&quot;회원가입 성공&quot;);
        return new ResponseEntity&lt;&gt;(messageDTO, HttpStatus.CREATED);
    }catch(IllegalArgumentException e){
        messageDTO.setMessage(&quot;학교 정보가 잘못되었습니다.&quot;);
        return new ResponseEntity&lt;&gt;(messageDTO, HttpStatus.BAD_REQUEST);
    }
}</code></pre>
<p>유저 회원가입 요청을 처리하는 Controller 함수이다.</p>
<p>잘못된 점은 크게 두 가지로 보인다.</p>
<ol>
<li>Service에서 boolean 값을 받아옴</li>
<li>Controller에서 Exception 처리</li>
</ol>
<p>첫 번째, <strong>계층 간 데이터는 DTO를 통해 전달되어야 한다.</strong>
그렇지 않고 바로 데이터를 전달한다면 계층 간 의존성이 높아지기 때문에
유지보수가 힘들어진다. </p>
<p>두 번째, <strong>Controller에서는 Controller 계층의 역할에 맞게 코드를 작성</strong>해야 한다.
Controller는 Client의 요청에 맞는 함수를 매핑하고 응답을 반환하는 계층이다.
여기서 오류처리를 해버린다면 코드 복잡도가 증가하고 반복되는 코드의 양이 늘어나 유지보수가 어려워진다.</p>
<p><strong>오류처리는 Service 계층에서 진행</strong>하고 <code>ExceptionHandler</code>를 사용해서 응답처리를 하자.</p>
<p>다음은 최근에 작성한 올바른 코드이다.</p>
<h2 id="최근에-작성한-코드">최근에 작성한 코드</h2>
<pre><code class="language-java">@PostMapping(&quot;/api/articles&quot;)
public ResponseEntity&lt;AddArticleResponse&gt; saveArticle(@RequestBody AddArticleRequest addArticleRequest) {
    AddArticleResponse addArticleResponse = boardService.saveArticle(addArticleRequest);
    return new ResponseEntity&lt;&gt;(addArticleResponse, HttpStatus.CREATED);
}

@GetMapping(&quot;/api/articles&quot;)
public ResponseEntity&lt;GetAllArticlesResponse&gt; findAllArticles(){
    GetAllArticlesResponse getAllArticlesResponse = boardService.findAllArticles();
    return new ResponseEntity&lt;&gt;(getAllArticlesResponse, HttpStatus.OK);
}

@GetMapping(&quot;/api/articles/{id}&quot;)
public ResponseEntity&lt;GetArticleResponse&gt; findArticleById(@PathVariable(&quot;id&quot;) Integer id){
    GetArticleResponse articleDTO = boardService.findArticleById(id);
    return new ResponseEntity&lt;&gt;(articleDTO, HttpStatus.OK);
}</code></pre>
<p>게시글과 관련된 요청을 처리하는 Controller다.
이전 코드와는 비교가 안 될 정도로 코드가 깔끔하다.</p>
<p>Service 계층의 <strong>함수를 실행하여 결과를 DTO로 받아오고 다른 서비스 로직없이 응답을 반환</strong>했기 때문에 코드가 간결해진 것이다.</p>
<p>이렇게 코드를 작성하면 Client 요청이 왔을 때 어떤 Service 함수가 실행되고, 어떤 값이 응답에 담겨 전달되는 지 알아보기 쉽다.
또한, <strong>Controller와 Service가 제대로 분리되어 유지보수가 쉬워진다</strong>.</p>
<p><em>앞으로 코드를 작성할 땐 Controller와 Service 구분을 제대로 해서 코드를 작성하자!</em></p>
<h2 id="출처">출처</h2>
<ul>
<li>계층 사진 자료 <a href="http://randikatech.blogspot.com/2019/09/get-your-hands-dirty-with-micro-services.html">http://randikatech.blogspot.com/2019/09/get-your-hands-dirty-with-micro-services.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vercel에 Github Repository 연동해서 Next.js 프로젝트 배포해보기]]></title>
            <link>https://velog.io/@juice-han/Vercel%EC%97%90-Github-Repository-%EC%97%B0%EB%8F%99%ED%95%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@juice-han/Vercel%EC%97%90-Github-Repository-%EC%97%B0%EB%8F%99%ED%95%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 17 Jun 2024 06:07:11 GMT</pubDate>
            <description><![CDATA[<p>Next.js 튜토리얼을 따라가면서 nextjs-dashboard 프로젝트를 만드는 도중 Vercel을 통해 간단하게 배포해보았다. 배포는 어렵다라는 생각을 항상 가지고 있었는데, <strong>이렇게나 간단하게 배포가 가능하다는 것</strong>을 경험한 후 경험을 공유하고 싶어 글을 쓰게 되었다.</p>
<h2 id="vercel이란">Vercel이란?</h2>
<blockquote>
</blockquote>
<p>자동으로 웹 어플리케이션을 빌드하고 배포해주는 서비스를 제공한다.
front-end 개발자들을 위한 사이트로 자동으로 빌드, 배포는 물론 <code>https</code>를 적용해서 보안성까지 제공해주는 친절한 사이트다. 👍👍👍</p>
<p>Github와 연동하면 <code>commit</code>을 감지하여 자동으로 웹사이트를 업데이트하고, <code>pull request</code>가 요청되면 preview page를 보여주기도 하는 등 github와의 연동성이 굉장히 좋다.</p>
<h2 id="배포-방법">배포 방법</h2>
<h3 id="vercel-로그인github-연동">Vercel 로그인(Github 연동)</h3>
<p><a href="https://vercel.com/signup">https://vercel.com/signup</a>
vercel의 회원가입 페이지에 들어가서 <code>hobby</code>를 클릭하고 간단한 인적사항을 입력 후 github로 로그인을 진행한다.
<img src="https://velog.velcdn.com/images/juice-han/post/e9f15060-5ca7-4458-894c-5e835fd35978/image.png" alt="vercel 회원가입"></p>
<h3 id="import-github-repository">Import Github Repository</h3>
<p>맨 처음엔 <code>Github application</code>을 <code>install</code> 해야할 것이다. 화면에 바로 보이는 install을 눌러서 Github application을 해준다.</p>
<p>그 후 내 Github repository를 검색 및 선택해준 후 <code>import</code> 해온다.
<img src="https://velog.velcdn.com/images/juice-han/post/f6f6df3c-52ed-4a17-a0a0-654946c11192/image.png" alt="import github repo"></p>
<p>그 다음 project 기본 설정을 해주면 배포 준비는 완료된다.
<img src="https://velog.velcdn.com/images/juice-han/post/29f18570-7f39-4200-8a78-c5a6276634ff/image.png" alt="project 기본 설정"></p>
<p>이렇게 하면 끝! 어때요 정말 쉽~죠? 😄</p>
<h3 id="배포된-사이트-방문해서-확인해보기">배포된 사이트 방문해서 확인해보기</h3>
<p><img src="https://velog.velcdn.com/images/juice-han/post/2163f2f0-db5f-4c6b-95ed-de26d91b2ab7/image.png" alt="project 설정 화면"></p>
<p>화면 속 Visit을 누르면 배포된 사이트 확인이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/843d934c-57c6-4239-bb7f-3bf66faa1bb4/image.png" alt="배포된 사이트 모습"></p>
<p>짜잔~</p>
<h2 id="느낀점">느낀점</h2>
<p>배포라는 것을 어렵게만 생각했던 지금까지의 생각이 완전히 바뀌는 경험이었다. <strong>기술이 발전함에 따라 이렇게 많은 이점이 생긴다는 것이 놀라웠고, 왜 개발자가 평생 배워야하는 직업인지를 깨닫게 되었다.</strong>
앞으로 배포하는 과정을 지인이나 후배들에게 가르쳐줄 때 Vercel을 활용해보라고 추천해 줘야겠다!😆</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>Next.js 공식 튜토리얼 페이지
<a href="https://nextjs.org/learn/dashboard-app/setting-up-your-database">https://nextjs.org/learn/dashboard-app/setting-up-your-database</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express.js에서 네이버 Papago API 요청해보기]]></title>
            <link>https://velog.io/@juice-han/Express%EC%99%80-Papago-API-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@juice-han/Express%EC%99%80-Papago-API-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 22 Feb 2024 09:27:17 GMT</pubDate>
            <description><![CDATA[<p>express 서버에서 papago API 요청을 보내기 위해 고군분투했던 과정을 글로 정리해보려고 한다.</p>
<h1 id="개발-과정">개발 과정</h1>
<hr>
<h2 id="nodejs용-papago-api-사용-예제">Node.js용 papago API 사용 예제</h2>
<p><img src="https://velog.velcdn.com/images/juice-han/post/a64f6094-cf3d-4b5e-8b12-768f628483d5/image.png" alt="node.js 파파고 api 사용 예제"></p>
<p>네이버가 아주 친절하게 사용 예제를 정리해놨다.
<code>request</code>라는 모듈로 API요청을 한 것 같은데, 뭔지는 알고 지나가야할 것 같다는 생각에 구글링을 해봤다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/4055e5a3-c696-42d0-91e9-8d922f3ecde4/image.png" alt="deprecated된 request 모듈"></p>
<p>이런.. 2020년부터 <code>deprecated</code> 되었다고 한다. 
&#39;더 이상 사용되지 않는, 사라지게 될&#39;이라는 뜻이다.</p>
<p>눈 앞이 깜깜해졌다.
그렇다면 나는 이 코드를 수정해서 API 요청을 해야했다.
마음을 가다듬고 API 요청하는 방법부터 차근차근 공부해보기로 했다.</p>
<h2 id="express에서-api-요청-보내기">express에서 API 요청 보내기</h2>
<p>맨 처음엔 express에서 API 요청 보내는 방법이 따로 있는 줄 알았다.
항상 프론트엔드단에서 axios로 API 요청을 보내다가 서버에서 API 요청을 보내려니 기분이 묘했다.</p>
<p>express에서 API 요청보내는 법을 구글링해보니 온통 express&#39;<strong>로</strong>&#39; API 요청 보내는 글만 나와서 당황스러웠다.</p>
<p>&#39;그래도 js 기반이니까 fetch로 하면 되겠지&#39;라는 생각에 <code>fetch API</code>를 사용해보기로 했다. (익숙한 <code>axios</code>를 사용해서 편하게 요청해도 됐지만 fetch라는 기본적인 함수를 공부하고 싶어서 fetch를 사용했다.)</p>
<p>fetch 함수 사용법을 구글링하니 잘 정리된 글들이 많았다.
덕분에 어렵지 않게 fetch를 사용할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/ad62fc57-87ae-43fd-a97f-456b815350c7/image.png" alt=" fetch 사용법"></p>
<p>fetch 함수는 axios 라이브러리와 다르게 <strong>body에 들어가는 data를 JSON 문자열로 바꿔줘야 한다</strong>는 귀찮음이 있다(<code>JSON.stringify</code> 활용).</p>
<p>일단 간단하게 fetch 함수 활용법은 알게 됐고, 이제 request 모듈 사용을 가정한 네이버 사용 예제를 fetch 함수에 맞게 바꿔야 하는 과정이 남았다.</p>
<h3 id="fetch함수를-활용한-papago-api-요청">fetch함수를 활용한 papago API 요청</h3>
<p><img src="https://velog.velcdn.com/images/juice-han/post/fb580dc8-9d64-41a0-b38b-1380d0b7b5dd/image.png" alt="네이버 참고 사항"></p>
<p>이 단계에서 살짝 뇌정지가 왔다.
헤더를 어떻게 설정해야하는지, x-www-form-urlencoded는 무엇인지 등 너무 복잡해 보였기 때문이다.</p>
<p>일단 먼저 Content-Type: application/x-www-form-urlencoded 라는게 뭘 뜻하는지 알아봤다.</p>
<h3 id="content-type-applicationx-www-form-urlencoded-란">Content-Type: application/x-www-form-urlencoded 란?</h3>
<p>간단하게 설명하자면 request 요청 속 body 데이터의 자료형이다.</p>
<p>클라이언트와 서버가 데이터를 주고 받을 땐 그냥 주고받지 않는다.
<strong>문자열 데이터로 바꿔서</strong> 주고 받는다.</p>
<p>클라이언트는 정해진 규칙에 따라 데이터를 문자열로 바꿔서 전달하고,
서버는 바뀐 자료형을 인식하고 그에 맞게 해석해서 데이터를 사용한다.</p>
<p>이때 request 요청의 <code>header</code>라는 부분에 &#39;우리가 <del>~ 형식으로 데이터 변환했으니까 알고있으세요</del>&#39;라는 메세지를 남기는데 그게 바로 Content-Type이다. 
Content-Type엔 <code>application/json</code>도 있고 <code>multipart/form-data</code> 등 여러가지가 있다. (요즘 많이 쓰이는 건 <code>application/json</code> 타입이다.)</p>
<pre><code>Content-Type: application/x-www-form-urlencoded</code></pre><p>는 <code>x-www-form-urlencoded</code>라는 형식으로 데이터를 변환했다는 것을 서버에게 알려준다.</p>
<p>x-www-form-urlencoded는 key=value 형식으로 데이터를 보내야하기 때문에 객체형 데이터를 key=value 형식으로 변환해주는 qs 라이브러리를 install해서 사용하였다. (query string의 줄인말)</p>
<p>qs의 사용법은 간단하다. JSON.stringify처럼 qs.stringify에 객체형 자료를 인수로 전달하면 된다.</p>
<pre><code>{ source: &quot;ko&quot;, target: &quot;en&quot;, text: query}

&quot;source=ko&amp;target=en&amp;text=만나서 반갑습니다. 앞으로 잘 부탁드립니다.&quot;</code></pre><p>이런 식으로 객체형 자료가 문자열로 바뀐다.</p>
<p><a href="https://www.npmjs.com/package/qs">npm qs 문서 링크</a></p>
<h3 id="fetch-함수-작성">fetch 함수 작성</h3>
<pre><code class="language-javascript">let query = &quot;만나서 반갑습니다. 앞으로 잘 부탁드립니다.&quot;;
app.get(&#39;/translate&#39;, function (req, res) {
  const api_url = &#39;https://openapi.naver.com/v1/papago/n2mt&#39;;
  const data = qs.stringify({ source: &quot;ko&quot;, target: &quot;en&quot;, text: query },{ encode: false })
  console.log(data);
  const opt = {
    method: &quot;POST&quot;,
    headers: {
      &#39;Content-Type&#39; : &#39;application/x-www-form-urlencoded; charset=UTF-8&#39;,
      &#39;X-Naver-Client-Id&#39;: client_id,
      &#39;X-Naver-Client-Secret&#39;: client_secret
    },
    body: encodeURI(data)
  }
  fetch(api_url, opt)
    .then((res) =&gt; {
      console.log(res.body);
    })
    .catch((err) =&gt; {
      console.error(err)
    })
});</code></pre>
<p>application/x-www-form-url<u>encoded</u>라서 encode 해줘야 되는줄 알고 encodeURI()를 통해 data를 encode했다.
(나중에 확인해보니 encode를 안 해도 응답이 잘 왔다. fetch 함수가 알아서 encoding을 해주는 것 같다.)</p>
<p>일단은 이렇게 fetch함수를 작성해봤다. papago로 번역된 결과를 콘솔창에서 확인해보자.</p>
<h2 id="readablestream-데이터-읽기">ReadableStream 데이터 읽기</h2>
<p>번역된 결과를 확인하려고 res.body를 출력해봤는데 콘솔창에</p>
<pre><code>ReadableStream { locked: false, state: &#39;readable&#39;, supportsBYOB: false }</code></pre><p>이런 결과가 나왔다...</p>
<p>분명 응답의 <code>body</code>를 console.log로 출력했는데, <code>ReadableStream</code>이라는 이상한 데이터가 출력된 것이다.</p>
<p>구글링한 결과 json()함수를 사용해서 ReadableStream을 파싱하면 데이터를 제대로 확인할 수 있다는것을 알게됐다.</p>
<pre><code class="language-javascript">fetch(api_url, opt)
    .then(async (res) =&gt; {
      console.log(await res.json());
    })
    .catch((err) =&gt; {
      console.error(err)
    })</code></pre>
<p>json()함수는 response를 리턴하는 비동기 함수이다. 따라서 async await으로 비동기처리를 하지 않으면 Response가 출력되는 일이 발생한다. 항상 주의하자.</p>
<p><a href="https://stackoverflow.com/questions/40385133/retrieve-data-from-a-readablestream-object">stack overflow - readablestream object 링크</a></p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/734184f4-d143-4d88-9ef2-4f08322e009d/image.png" alt="papago API 응답 확인"></p>
<p>짜잔~ 번역결과를 정상적으로 확인할 수 있게 되었다.</p>
<h2 id="content-type-applicationjson으로-해도-정상-작동">Content-Type: application/json으로 해도 정상 작동</h2>
<p>Content-Type을 application/json 바꿔도 잘 작동하는 지 궁금해서 확인해봤다. 
데이터 형식을 json으로 변환하고 요청을 보냈더니 잘 작동하는 걸 확인할 수 있었다.</p>
<pre><code class="language-javascript">let query = &quot;만나서 반갑습니다. 앞으로 잘 부탁드립니다.&quot;;
app.get(&#39;/translate&#39;, function (req, res) {
  const api_url = &#39;https://openapi.naver.com/v1/papago/n2mt&#39;;
  const data = JSON.stringify({ source: &quot;ko&quot;, target: &quot;en&quot;, text: query})
  console.log(data);
  const opt = {
    method: &quot;POST&quot;,
    headers: {
      &#39;Content-Type&#39; : &#39;application/json; charset=UTF-8&#39;,
      &#39;X-Naver-Client-Id&#39;: client_id,
      &#39;X-Naver-Client-Secret&#39;: client_secret
    },
    body: data
  }
  fetch(api_url, opt)
    .then(async (res) =&gt; {
      console.log(await res.json());
    })
    .catch((err) =&gt; {
      console.error(err)
    })
});</code></pre>
<p>데이터 형식이 요청을 보낼 때 크게 중요한 것 같지는 않다. header에 잘 명시해놓으면 서버가 알아서 변환을 하는 것 같다.</p>
<h1 id="최종코드">최종코드</h1>
<hr>
<h2 id="content-type-x-www-form-urlencoded-일때">Content-Type: x-www-form-urlencoded 일때</h2>
<p>express, qs 모듈 install은 필수이고 dotenv 모듈은 선택사항이다.</p>
<p>client_id와 client_secret를 직접 입력하려면 상관없는데, 나는 보안성을 위해 .env파일에 저장한 뒤 dotenv모듈을 사용해서 가져왔다.</p>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
const qs = require(&#39;qs&#39;);
require(&quot;dotenv&quot;).config();

app.listen(8080, () =&gt; {
  console.log(&#39;8080 포트에서 서버 실행중&#39;);
})

const client_id = process.env.PAPAGO_CLIENT_ID;
const client_secret = process.env.PAPAGO_CLIENT_SECRET
let query = &quot;만나서 반갑습니다. 앞으로 잘 부탁드립니다.&quot;;
app.get(&#39;/translate&#39;, function (req, res) {
  const api_url = &#39;https://openapi.naver.com/v1/papago/n2mt&#39;;
  const data = JSON.stringify({ source: &quot;ko&quot;, target: &quot;en&quot;, text: query})
  console.log(data);
  const opt = {
    method: &quot;POST&quot;,
    headers: {
      &#39;Content-Type&#39; : &#39;application/json; charset=UTF-8&#39;,
      &#39;X-Naver-Client-Id&#39;: client_id,
      &#39;X-Naver-Client-Secret&#39;: client_secret
    },
    body: data
  }
  fetch(api_url, opt)
    .then(async (res) =&gt; {
      console.log(await res.json());
    })
    .catch((err) =&gt; {
      console.error(err)
    })
});</code></pre>
<h2 id="content-type-applicationjson-일-때">Content-Type: application/json 일 때</h2>
<pre><code class="language-javascript">const express = require(&#39;express&#39;);
const app = express();
require(&quot;dotenv&quot;).config();
app.listen(8080, () =&gt; {
  console.log(&#39;8080 포트에서 서버 실행중&#39;);
})

const client_id = process.env.PAPAGO_CLIENT_ID;
const client_secret = process.env.PAPAGO_CLIENT_SECRET
let query = &quot;만나서 반갑습니다. 앞으로 잘 부탁드립니다.&quot;;
app.get(&#39;/translate&#39;, function (req, res) {
  const api_url = &#39;https://openapi.naver.com/v1/papago/n2mt&#39;;
  const data = JSON.stringify({ source: &quot;ko&quot;, target: &quot;en&quot;, text: query})
  console.log(data);
  const opt = {
    method: &quot;POST&quot;,
    headers: {
      &#39;Content-Type&#39; : &#39;application/json; charset=UTF-8&#39;,
      &#39;X-Naver-Client-Id&#39;: client_id,
      &#39;X-Naver-Client-Secret&#39;: client_secret
    },
    body: data
  }
  fetch(api_url, opt)
    .then(async (res) =&gt; {
      console.log(await res.json());
    })
    .catch((err) =&gt; {
      console.error(err)
    })
});</code></pre>
<p>express에서 papago API를 활용하려는 분들에게 도움이 됐으면 좋겠다.</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><p>네이버 파파고 api 문서
<a href="https://developers.naver.com/docs/papago/README.md">https://developers.naver.com/docs/papago/README.md</a></p>
</li>
<li><p>fetch 함수 사용법
<a href="https://www.daleseo.com/js-window-fetch/">https://www.daleseo.com/js-window-fetch/</a></p>
</li>
<li><p>x-www-form-urlencoded 타입
<a href="https://wildeveloperetrain.tistory.com/304">https://wildeveloperetrain.tistory.com/304</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA["__dirname is not defined in ES module scope" 오류의 원인과 해결방법]]></title>
            <link>https://velog.io/@juice-han/dirname-is-not-defined-in-ES-module-scope-%EC%98%A4%EB%A5%98%EC%9D%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@juice-han/dirname-is-not-defined-in-ES-module-scope-%EC%98%A4%EB%A5%98%EC%9D%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 06 Feb 2024 16:21:42 GMT</pubDate>
            <description><![CDATA[<h1 id="__dirname-오류">__dirname 오류</h1>
<p>vite를 통해 react앱을 만들고 로컬 환경에서 express로 서버를 구축하던 중 오류가 발생했다.</p>
<p><strong>바로</strong> <code>__dirname</code><strong>이 정의되지 않았다는 것이다.</strong></p>
<pre><code class="language-javascript">    app.use(express.static(path.join(__dirname, &#39;/dist&#39;)))</code></pre>
<p>이 코드에서 문제가 발생했는데, <code>dist</code><strong>라는 디렉터리를 express에 등록하는 코드</strong>이다.</p>
<p>vite로 build를 할 경우 루트 디렉터리 내에 <code>dist</code>라는 디렉터리가 생기고,
그 안에는 <strong>index.html, 이미지 파일과 같은 정적인(static) 파일</strong>이 저장된다.
이 디렉터리를 express가 전송해주기 위해선 먼저 등록하는 과정이 필요한데, 
나는 그 과정에서 오류가 발생한 것이다.</p>
<h2 id="원인">원인</h2>
<p>구글링 하던 중 원인과 해결방법을 찾을 수 있었다.</p>
<p><strong>ES module을 사용하면 <code>__dirname</code>을 &#39;그냥&#39; 사용할 수 없다는 것이다.</strong></p>
<pre><code class="language-javascript">&quot;target&quot;: &quot;ES2020&quot;,
&quot;useDefineForClassFields&quot;: true,
&quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
&quot;module&quot;: &quot;ESNext&quot;,
&quot;skipLibCheck&quot;: true,</code></pre>
<p>tsconfig.json파일의 내용 중 compilerOptions 일부분을 가져온 것이다.
코드를 보면 <code>ESNext</code>라는 module을 사용하고 있다는 것을 알 수 있다.</p>
<h2 id="해결방법">해결방법</h2>
<p>코드 2줄을 server.js파일 상단에 추가했다.</p>
<pre><code class="language-javascript">import path from &#39;path&#39;;
import { fileURLToPath } from &#39;url&#39;;</code></pre>
<p>파일이나 디렉터리 경로를 다루는 <code>path</code>모듈과
일반적인 파일 url을 Node.js 파일 path로 바꿔주는 <code>fileURLToPath</code> 함수를 가져온다.</p>
<h3 id="fileurltopath-함수란">fileURLToPath 함수란?</h3>
<p>다음은 fileURLToPath 함수에 관한 공식문서이다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/f2e520c1-bdb6-491d-8372-db55e554c452/image.png" alt="fileURLToPath 함수"></p>
<p>url을 입력받으면 (완전히 플랫폼 문제가 해결된) 특정한 Node.js 파일 경로를 리턴해준다고 한다.</p>
<hr>
<p>그 다음 하단에 코드 2줄을 추가한다.</p>
<pre><code class="language-javascript">const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);</code></pre>
<ol>
<li>server.js의 파일url을 가져와서 <code>fileURLToPath</code>로 경로 변환해서 <code>__filename</code>에 저장하고</li>
<li>변환된 경로의 디렉터리 경로를 <code>__dirname</code>에 저장한다.</li>
</ol>
<p>이렇게 코드를 작성하면 오류가 해결되는데, 도대체 어떤 과정을 거쳐 해결되는지 궁금해서 더 알아봤다.</p>
<h3 id="importmetaurl은-어떤-값일까">import.meta.url은 어떤 값일까?</h3>
<p><img src="https://velog.velcdn.com/images/juice-han/post/497a2c96-4a7f-4d84-b375-a8eca933d340/image.png" alt="import.meta.url값"></p>
<p>server.js의 경로를 저장하고 있는건 맞는데, 앞에 <code>file:///</code>가 붙어있다.
아마 이를 제거하기 위해 <code>fileURLToPath</code>함수를 사용하는 것 같다.</p>
<h3 id="그럼-__filename-__dirname에는-어떤-값이-저장될까">그럼 __filename, __dirname에는 어떤 값이 저장될까?</h3>
<p>console.log로 <code>__filename</code>과 <code>__dirname</code>을 출력해보면 다음과 같은 결과를 얻는다.</p>
<p><img src="https://velog.velcdn.com/images/juice-han/post/26b69914-3094-4b3b-a0cf-b02485545520/image.png" alt="출력결과"></p>
<ul>
<li><p><code>__filename</code>에 저장된 값
C:\Users\user\Desktop\Programing\nodejs-study\my-blog-app\server.js</p>
</li>
<li><p><code>__dirname</code>에 저장된 값
C:\Users\user\Desktop\Programing\nodejs-study\my-blog-app</p>
</li>
</ul>
<blockquote>
<p>결론적으로 <code>fileURLToPath(import.meta.url)</code>을 통해 서버 파일의 경로, 즉 server.js의 경로를 가져온 후, <code>path.dirname(__filename)</code>을 통해 그 파일이 포함된 디렉토리를 찾아서 경로를 반환하는 것을 알 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/juice-han/post/0f4f37fa-4221-4c22-b5f7-5d14a209448c/image.png" alt="오류 해결"></p>
<p>이를 통해 정상적으로 <code>__dirname</code>을 사용할 수 있게 됐다.</p>
<hr>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><p><code>__dirname</code> 오류 원인과 해결방법
<a href="https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/">https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/</a></p>
</li>
<li><p><code>fileUrlToPath</code>함수 관련 Node.js 공식 문서
<a href="https://nodejs.org/api/url.html#urlfileurltopathurl">https://nodejs.org/api/url.html#urlfileurltopathurl</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[6주 간의 부트캠프를 끝마치며]]></title>
            <link>https://velog.io/@juice-han/%ED%95%9C-%EB%8B%AC-%EA%B0%84%EC%9D%98-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84%EB%A5%BC-%EB%81%9D%EB%A7%88%EC%B9%98%EB%A9%B0</link>
            <guid>https://velog.io/@juice-han/%ED%95%9C-%EB%8B%AC-%EA%B0%84%EC%9D%98-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84%EB%A5%BC-%EB%81%9D%EB%A7%88%EC%B9%98%EB%A9%B0</guid>
            <pubDate>Mon, 05 Feb 2024 03:53:10 GMT</pubDate>
            <description><![CDATA[<p>길다면 길고 짧다면 짧은 6주동안 치열하게 코딩하며 느꼈던 아쉬웠던 점, 배운 점들을 적어보려 한다.</p>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<h3 id="1-적극적으로-질문을-하지-못한-것">1. 적극적으로 질문을 하지 못한 것</h3>
<p>내가 참여했던 부트캠프는 하나부터 열까지 친절하게 알려주는 부트캠프가 아니었다.
모르는 건 직접 찾아보고, 물어보고, 적용해보며 실전으로 배우는 과정이었다.
나는 원래 혼자서 고민하고 공부하는 걸 좋아해서 질문을 거의 안 했다. 질문 개수를 세자면 열 손가락으로 셀 수 있을 정도?
사람들이 내가 모르는 것에 대해 얘기할 때도 잘 물어보지 않고 구글에 검색해서 그 뜻을 알아냈다.
나는 그런 과정이 즐거웠고, 검색하면 다 나와서 물어볼 필요성을 못 느꼈다. </p>
<p>그런데 부트캠프가 끝나고 돌아보니 <strong>질문을 잘 하는 것도 개발자로서의 중요한 능력 중 하나</strong>인데 내가 그 능력을 키울 좋은 기회를 날렸다는 생각이 들었다.
6주라는 짧은 기간동안 프로젝트를 완성해야 했기에 1분 1초가 아까운 상황에서 나 혼자 즐겁다는 이유로 에러에 대해 3시간 이상 주구장창 고민했으면 안 됐다. 부트캠프에 참여하신 멘토님이 이런 말씀을 하셨다. &#39;모르는 게 있을 때 너무 많이 고민하지 말고 질문을 해라&#39; 이런 식으로 말이다. 질문을 잘 하는 법까지 친절히 알려주셨는데, 그걸 활용해보지 못한게 아쉽다. 앞으로는 의식적으로 질문을 하도록 노력해야겠다.</p>
<h3 id="2-팀원-간의-커뮤니케이션">2. 팀원 간의 커뮤니케이션</h3>
<p>내가 속한 팀은 뭔가 커뮤니케이션이 잘 안 되는 느낌이 들었다. 음 그냥 서로 얘기를 잘 안 했던 것 같다.
한 예로, 나는 프론트엔드를 맡았었는데, 백엔드 쪽에서 api 변경사항이 생겼는데도 변경사항을 전달받지 못한 경우가 있었다.
에러 로그가 떠서 나 혼자 console.log를 찍어보며 response data가 바뀌었다는 걸 파악하고 백엔드 분들에게 물어보자 그제야 변경사항이 있었는데 말 못해서 미안하다라는 말을 들을 수 있었다. 커뮤니케이션의 부재가 팀 전체의 효율성을 낮추는 결과를 낳았다.</p>
<p>그리고 팀의 프로젝트 방향성을 정할 때마다 적극적으로 의견을 냈어야 했는데, &#39;나는 잘 모르니까, 실력이 부족하니까&#39;와 같은 생각 때문에 다른 분들이 내는 의견에 그냥 동참했던 경우가 종종 있었다. &#39;<strong>내 의견을 좀 더 용기있게 말했으면 어땠을까</strong>&#39;라는 아쉬움이 남는다.</p>
<p>내가 만약 팀을 이끄는 리더가 된다면 이런 상황이 발생하지 않도록 자유로운 커뮤니케이션 분위기를 만들 것이다. 자신의 의견을 마음껏 표현할 수 있게 내가 먼저 의견을 적극적으로 낼 것이고, 의견을 잘 내지 않는 팀원에게는 직접 의견을 물어봐서 프로젝트 참여를 이끌어 낼 것이다.
또한, 커뮤니케이션 미스가 생기지 않도록 메뉴얼을 만들어 팀의 효율성을 높일 것이다. 예를 들어, &#39;변경사항이 생길 때는 먼저 보고 하고 문서화 제대로 반영하기&#39;, &#39;깃허브 풀리퀘스트 날릴 때 수정사항 상세히 작성하고, 코드리뷰 코멘트 작성해주기&#39;와 같이 말이다.</p>
<hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="1-공부-습관">1. 공부 습관</h3>
<p>매일 아침 10시부터 새벽 3~4시까지 하루종일 코딩을 하는 걸 3주 정도 했을 때쯤, 뇌의 변화가 느껴졌다. 이게 이상하게 들릴지도 모르지만 뭔가 두뇌회전이 빨라졌고, 새로운 내용을 배울 때 더 빨리 이해가 되는 느낌이 들었다. 이전까지는 이런 기분을 느껴본 적이 없었는데, 정말 신기한 경험이었다. 그 당시 &#39;몰입&#39;이라는 책을 읽었는데, 책에서 말하는 몰입상태에 들어간 듯했다. 종일 코딩 생각만 하고 코딩만 했으니 몰입 상태에 들어갈 수밖에 없었나보다. </p>
<p>아쉽게도 그 느낌은 3주차 이후로는 느낄 수 없었다. 처음의 열정이 3주차 이후까지 이어지지 못했기 때문이다. 처음엔 새로 배울 것도 많고 배운 것을 프로젝트에 적용하는 재미가 있어서 열정적으로 코딩을 했는데, 주차가 지나갈수록 새로운 걸 적용하기보다는 오류를 수정하는 작업이 많아졌기 때문이다.</p>
<p>결론적으로, 3주차때 느꼈던 기분을 가진채로 쭉 공부를 이어 나간다면 나 자신이 확실하게 변할 것 같다는 생각이 들었다. 이번 부트캠프를 통해 <strong>얼마나 공부해야 변화를 만들어 낼 수 있는지 알게됐다.</strong> 앞으로 꾸준히 부트캠프 때의 공부습관을 버리지 않고 이어나갈 생각이다.</p>
<h3 id="2-공부-방향">2. 공부 방향</h3>
<p>프로젝트의 기획부터 개발, 배포까지의 전 과정을 거쳤기 때문에, 내가 뭘 모르는 지 알게됐고, 나 혼자서도 서비스를 개발해서 배포까지 할 수 있겠다는 자신감이 생겼다.</p>
<p>일단 지금 당장 내가 해야할 공부는 백엔드 공부다. 백엔드 프레임워크와 데이터베이스를 공부하고 그 이후엔 docker 띄우기, AWS 서버 개설, CI/CD 적용 등의 devops 과정까지 공부해볼 생각이다.</p>
<p>만약 부트캠프를 하지 않았다면 이런 배포까지의 전 과정을 알 수 없었을 것이고, 개발과정의 큰 그림을 보지 못한 채로 어영부영 시간만 날렸을 것이다. 하지만 부트캠프 덕분에 공부 계획을 세밀하게 작성할 수 있게 되었다.</p>
<h3 id="3-공부-방법">3. 공부 방법</h3>
<p>이 부트캠프는 학생들이 기존의 이론중심의 개발에서 벗어나 <strong>잘 모르더라도 일단 어떻게든 프로그램을 만들어보면서 배우는 공부방법</strong>을 알려준다. 이게 정말 효과적인게, 일단 재밌다. 잘 모르더라도 코드에 적용했을 때 프로그램이 동작하는 걸 보니까 흥미가 생기고, 내가 이 내용을 적용했을 땐 프로그램이 이렇게 동작하는 구나를 눈으로 확인할 수 있어서 좋았다.</p>
<p>앞으로 새로운 개발 지식을 공부할 때, 두꺼운 책을 처음부터 끝까지 보고 나서 개발을 시작하는 게 아니라, 일단 개발을 시작하고 모르는 게 나오면 구글링을 하거나 유튜브, 책 등의 자료를 참고하는 방식으로 공부해야 겠다고 느꼈다.</p>
<hr>
<h2 id="앞으로의-다짐">앞으로의 다짐</h2>
<p>부트캠프에 참여하며 내가 얼마나 실력이 부족한지 알게되었다.
계속해서 새로운 걸 배우고 어려운 일에 도전하는 마인드를 가져야겠다.</p>
<p>개발을 하며 겪었던 일들을 기록하고 성장하기 위해 velog 계정을 만들어 첫 글을 써봤는데, 글쓰는 건 역시 어렵다. 계속 써봐야 감을 잡을 것 같다.</p>
]]></description>
        </item>
    </channel>
</rss>