<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Twinkle's devlog 🌟</title>
        <link>https://velog.io/</link>
        <description>Java, Spring 기반 풀스택 개발자의 개발 블로그입니다.</description>
        <lastBuildDate>Fri, 31 Jan 2025 07:47:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Twinkle's devlog 🌟</title>
            <url>https://velog.velcdn.com/images/developer_khj/profile/54f9baaa-e287-4f9a-b94b-ab43ad4f95d0/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Twinkle's devlog 🌟. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/developer_khj" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Kotlin] 코프링 시작하기 (Kotlin-Spring)]]></title>
            <link>https://velog.io/@developer_khj/Kotlin-Spring-Start</link>
            <guid>https://velog.io/@developer_khj/Kotlin-Spring-Start</guid>
            <pubDate>Fri, 31 Jan 2025 07:47:25 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>신규 프로젝트에서 일명 <strong>코프링(Kotlin-Spring)</strong>을 하게 되어, 이 시리즈를 시작하게 되었습니다.</p>
<p>이 시리즈를 작성하면서 코프링을 배워가며, 나아가 가장 간단한 환경 설정부터 개발하면서 자프링과의 장단점 비교, 신규 문법들을 채워나갈 것입니다. 😊</p>
<hr>
<h1 id="1-프로젝트-시작">1. 프로젝트 시작</h1>
<h2 id="11-프로젝트-셋업">1.1 프로젝트 셋업</h2>
<h3 id="새-프로젝트-생성">새 프로젝트 생성</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/2a325c99-784f-43be-ad37-83818b72b034/image.png" alt="새 프로젝트 생성"></p>
<p><strong>Kotlin-Spring</strong> 프로젝트를 할 것이기 때문에 위와 같이 선택했습니다.</p>
<ul>
<li>언어: Kotlin</li>
<li>타입: Gradle-Kotlin</li>
<li>Java Version: 17</li>
<li>Spring Boot: 3.4.2</li>
</ul>
<br>

<h3 id="라이브러리-선택">라이브러리 선택</h3>
<p>간략한 실습을 위해 Spring Web을 추가하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/2062fcdd-1b00-49d7-bfed-6b16c80682ce/image.png" alt="새 프로젝트 생성 - 라이브러리 선택"></p>
<br>

<blockquote>
<p>Lombok 이야기</p>
</blockquote>
<p>Java-Spring 을 진행할 때는 무조건 <strong>Lombok</strong>을 선택했는데, Kotlin-Spring에서는 주로 사용하지 않는다고 합니다. 그 이유는 Kotlin의 <strong>Data Class</strong>가 롬복에서 지원하는 기능들을 기본적으로 제공해주기 때문입니다.</p>
<p>Data Class를 생성하면 기본적으로 Getter, Setter, toString, HashCode, equals를 제공하고, 적절한 변수 선언(val, var) 및 생성자를 통해 @RequiredArgsConstructor, @Builder를 대체할 수 있습니다.</p>
<br>

<h3 id="intellij-플러그인">IntelliJ 플러그인</h3>
<p>IntelliJ로 Kotlin 프로젝트를 생성하면 아래의 플러그인이 자동으로 설정됩니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/88068b4d-cd22-4079-a01d-7c2a4be72dce/image.png" alt="IntelliJ 플러그인"></p>
<br>

<h3 id="자프링-코프링-뭐가-다를까">자프링, 코프링 뭐가 다를까?</h3>
<blockquote>
<p>새로운 기술을 대하는 IT인의 올바른 자세 -&gt; &quot;나를 가스라이팅한다&quot;</p>
<p>사실 자프링이나 코프링이나 같은 스프링을 쓰기 때문에 문법적 차이를 제외하고 크게 다를 바 없다고 생각합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c3df622a-ad54-4645-9c71-ec5829e204cb/image.png" alt="새 프로젝트 초기 구성"></p>
<p>초기 구성의 차이점을 짚어보자면 아래와 같이 써볼 수 있을 것 같습니다.</p>
<ul>
<li><strong>폴더명 변경</strong>: <code>src/main/java</code> -&gt; <code>src/main/kotlin</code></li>
<li><strong>확장자 변경</strong>: <code>.java</code> -&gt; <code>.kt</code></li>
<li><strong>Gradle 파일 변경</strong>: <code>build.gradle</code> -&gt; <code>build.gradle.kts</code></li>
<li><strong>문법 변경</strong>: 문법이 Java 스타일에서 Kotlin 스타일로 대거 변경됩니다.</li>
</ul>
<br>

<hr>
<h1 id="2-test-controller-작성">2. Test Controller 작성</h1>
<h2 id="21-파일-생성하기">2.1 파일 생성하기</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/bec6a7ff-b3b9-4de6-978f-3c827ea2b931/image.png" alt="파일 생성하기"></p>
<p>파일을 생성할 때, <strong>Java 클래스</strong> 대신 <strong>Kotlin 클래스</strong>를 선택해서 생성해야 합니다.</p>
<br>

<h2 id="22-hello-world">2.2 Hello World!</h2>
<h3 id="예시-화면">예시 화면</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/ac50c058-7e89-425b-b6cb-57a9cb435fa7/image.png" alt="Hello World"></p>
<p>Controller 클래스를 생성하고 간단하게 <strong>Hello World</strong>를 응답하는 API를 구성해보았습니다.</p>
<br>

<h3 id="kotlin-함수-문법">Kotlin 함수 문법</h3>
<p>Kotlin 함수의 기본 문법은 <strong>fun 키워드</strong>를 통해 정의하며 아래와 같이 쓸 수 있습니다.</p>
<pre><code class="language-kotlin">fun 함수명 (매개변수): 반환타입 {}</code></pre>
<br>

<h3 id="예시-코드">예시 코드</h3>
<p>아래의 코드는 함수를 사용하여 API를 만드는 예제 입니다.</p>
<pre><code class="language-kotlin">package com.example.demo.controller

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
class TestController {
    private val log: Logger = LoggerFactory.getLogger(TestController::class.java)

    // 간결화된 함수문
    @RequestMapping(&quot;/test&quot;)
    fun hello(): ResponseEntity&lt;String&gt; = ResponseEntity.ok(&quot;Hello World!&quot;)

    // 전형적인 함수
    @RequestMapping(&quot;/test2&quot;)
    fun hello2(): ResponseEntity&lt;String&gt; {
        return ResponseEntity.ok(&quot;Hello World!&quot;)
    }

    // 반환타입이 Any (Object 대체)
    @RequestMapping(&quot;/test3&quot;)
    fun hello3(): Any {
        return ResponseEntity.ok(&quot;Hello World!&quot;)
    }

    // 반환 타입이 Void
    @RequestMapping(&quot;/test4&quot;)
    @ResponseStatus(HttpStatus.OK)
    fun hello4() {
        log.info(&quot;Hello World!&quot;)
    }
}</code></pre>
<br>

<p>아직까지는 Java와 달라진 점이 있다면 함수 문법 정도입니다</p>
<ul>
<li>반환시 <strong>간결화된 Return 문</strong> 가능!</li>
<li>반환 타입 <strong>Void</strong> -&gt; 선언 X</li>
<li>반환 타입 <strong>Object</strong> -&gt; <strong>Any</strong></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>이번 포스팅에서는 코프링의 시작을 알리는 프로젝트 생성하기에 대해 다뤄보았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MacBook] MacBook Terminal 설정, Homebrew 설치]]></title>
            <link>https://velog.io/@developer_khj/MacBook-Terminal-Setting</link>
            <guid>https://velog.io/@developer_khj/MacBook-Terminal-Setting</guid>
            <pubDate>Tue, 17 Dec 2024 09:37:33 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<h1 id="1-terminal-설정">1. Terminal 설정</h1>
<p>초기의 Terminal은 예쁘긴 하지만, 약간의 멋과 편의성을 위해 여러가지 세팅해보려고 합니다.</p>
<br>

<h2 id="11-terminal-테마">1.1 Terminal 테마</h2>
<h3 id="초기-화면">초기 화면</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/ffb0982e-ad05-4ffc-a4d0-e8463cb9cb42/image.png" alt="Terminal 초기화면"></p>
<h3 id="터미널-기본-환경설정">터미널 기본 환경설정</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/3bb29744-bac6-4160-8aa2-b79af2a596c0/image.png" alt="터미널 환경 설정 열기"></p>
<p>터미널을 켠뒤, 우측 상단의 터미널을 클릭하여 설정을 눌러주면, 아래와 같이 프로필(테마)을 설정할 수 있습니다</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/41325821-efb7-4577-90ae-60eb0b3e4a9b/image.png" alt="터미널 환경 설정 &gt; 일반"></p>
<p>해당 프로필은 일반 옆 탭의 프로필 탭에서 보거나 변경할 수 있습니다.
<img src="https://velog.velcdn.com/images/developer_khj/post/6f781417-c1f0-4be9-90dd-8afe0b493d4c/image.png" alt="터미널 환경 설정 &gt; 프로필"></p>
<br>

<h2 id="12-neofetch">1.2 neofetch</h2>
<p><strong>neofetch</strong>는 운영체제, 소프트웨어 및 하드웨어에 대한 정보를 예쁜 시각적인 방식으로 표시해주는 도구입니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/5290189d-605b-456f-ba23-eae2928cba8a/image.png" alt="neofetch GitHub 레포지토리"></p>
<p><a href="https://github.com/dylanaraps/neofetch">neofetch GitHub 레포지토리</a>에서 상세 정보를 볼 수 있습니다.</p>
<p>brew를 이용해 neofetch를 설치합니다.</p>
<pre><code class="language-shell">brew install neofetch</code></pre>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e0886e69-2efe-4b85-84f8-1253655e383c/image.png" alt="neofetch 설치"></p>
<p>그런 다음, <code>neofetch</code> 명령을 실행시켜보면, 아래와 같이 사과를 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/af1158c2-8848-441e-a102-7df2651d197e/image.png" alt="neofetch"></p>
<p>터미널 실행 시마다 neofetch를 실행하게 하려면 터미널 설정을 변경해야 합니다.</p>
<p>저는 <strong>Z Shell</strong>을 설치해서 쓰고 있어, 아래와 같이 환경설정 파일에 반영해주었습니다.</p>
<pre><code class="language-shell">echo &quot;neofetch&quot; &gt;&gt; ~/.zshrc
source ~/.zshrc</code></pre>
<br>

<h2 id="13-iterm2">1.3 iTerm2</h2>
<h3 id="iterm2란">iTerm2란?</h3>
<p>iTerm2는 MacOS에서 공식 터미널 대신 사용할 수 있는 <strong>가상 터미널</strong>입니다. 기존 터미널에서 제공하는 기능보다 더 편리하고 가독성을 높여주는 애플리케이션입니다.</p>
<p>brew를 이용해 iterm2를 설치합니다.</p>
<pre><code class="language-shell">brew install iterm2</code></pre>
<p><strong>Spotlight</strong>를 통해 iterm을 실행시켜줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/1fe42d1c-625b-45d7-98ca-6e06c7fa7985/image.png" alt="Spotlight"></p>
<p>기본 터미널과 별개의 가상 터미널을 이용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/a4f5ad55-0555-4bbb-9af5-250bb6e978ae/image.png" alt="iterm2 터미널"></p>
<br>

<h3 id="ssh-profile-등록하기">SSH Profile 등록하기</h3>
<h4 id="프로필-생성">프로필 생성</h4>
<p>iterm2를 열고, 상단의 <code>Profiles 메뉴</code>를 클릭해줍니다. ssh 접속할 프로필을 하나 생성해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/1fa64a10-45a2-4f4a-9359-a3e0cc028d05/image.png" alt="프로필 생성"></p>
<p>주요 설정 목록</p>
<ul>
<li>Name: 프로필명</li>
<li>Tags: 태그 목록 설정</li>
<li>Subtitle: 터미널에 나타날 서브 제목</li>
</ul>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/97c93aa9-4e1f-4c68-a945-376430a1d54d/image.png" alt="터미널 빠르게 열기"></p>
<p>iterm2는 터미널을 빠르게 열기 기능을 제공합니다. 이 때 Tag를 등록하면 Tag 별로 그룹핑해주고, 여러개 Tag 등록도 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c8befc5c-bacc-415d-9adc-2119412d64f8/image.png" alt=""></p>
<h4 id="비밀번호-관리">비밀번호 관리</h4>
<p>[상단 <code>Window 메뉴</code> &gt; <code>Password Manager</code>]를 눌러 입력할 계정을 추가해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e6e93412-26b5-426d-98f4-36bf5cdb8d32/image.png" alt="비밀번호 자동입력"></p>
<h4 id="password-manager-자동-실행">Password Manager 자동 실행</h4>
<p>다시 Profiles로 돌아와서, Session 옵션 화면을 켭니다. <strong>Open password manager automatically</strong> 란을 활성화해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/cab75707-f714-4ba2-9d8a-f0d7d8b786b5/image.png" alt=""></p>
<hr>
<h1 id="2-homebrew">2. Homebrew</h1>
<h2 id="21-homebrew-란">2.1 Homebrew 란?</h2>
<p>홈브루는 오픈소스 기반의 Mac OS용 <strong>패키지 매니저</strong>입니다.</p>
<blockquote>
<p>패키지 매니저란, 컴퓨터 프로그램의 설치, 업그레이드, 구성, 제거 과정을 자동화하는 소프트웨어 도구들의 모임을 뜻합니다.</p>
</blockquote>
<br>

<h2 id="22-homebrew-설치">2.2 Homebrew 설치</h2>
<p><a href="https://brew.sh/">홈브루 홈페이지</a>에 접속해서 설치 명령어를 복사합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/679297b8-75a3-44b4-be09-cd6a894afd75/image.png" alt="홈브루 홈페이지"></p>
<blockquote>
<p>Install command</p>
</blockquote>
<pre><code class="language-shell">/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</code></pre>
<br>

<p><img src="https://velog.velcdn.com/images/developer_khj/post/17232b99-b52f-4a3d-b6eb-45819f65c180/image.png" alt=""></p>
<p>위의 명령을 실행하면, 홈브루 패키지 파일들을 다운받습니다. 비밀번호 입력 후, 다운로드가 완료되면 <strong>ENTER 키</strong>를 눌러줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/aa36f187-3515-4ea9-8c22-630e6fdff6fb/image.png" alt=""></p>
<p>후에는 자동으로 다운로드, 설치, 업데이트 과정을 거칩니다.</p>
<p>최종적으로, 터미널에서 사용하기 위해서는 환경변수로 등록해주어야합니다. <strong>Next Steps</strong> 아래 항목에 있는 명령들을 실행해줍니다.</p>
<pre><code class="language-shell">echo &gt;&gt; /Users/twinkle/.zprofile
echo &#39;eval &quot;$(/opt/homebrew/bin/brew shellenv)&quot;&#39; &gt;&gt; /Users/twinkle/.zprofile
eval &quot;$(/opt/homebrew/bin/brew shellenv)&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c290ddda-a76e-4e36-a935-e618c042d7cf/image.png" alt=""></p>
<p>이제 터미널에서 <code>brew</code> 명령어를 입력하면 사용할 . 수있습니다.</p>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring + React] 게시판 프로젝트 - (2) JWT 로그인 프로세스]]></title>
            <link>https://velog.io/@developer_khj/Spring-React-2-JWT-Login-Process</link>
            <guid>https://velog.io/@developer_khj/Spring-React-2-JWT-Login-Process</guid>
            <pubDate>Thu, 25 Jul 2024 12:55:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/developer_khj/post/0594b825-9380-4035-8879-dc19260f537c/image.png" alt="썸네일"></p>
<h1 id="💎-들어가며">💎 들어가며</h1>
<p>React에서는 JWT로 로그인 기능을 구현한다는 것은 익히 들어 알고 있었지만, 왜 JWT를 이용하여 로그인을 해야하는 지는 감이 안잡혔습니다.</p>
<p>Spring + React 스택을 사용하면서 기존에 사용하던 Spring Security의 FormLogin 기능을 구조적으로 이용할 수 없기 때문에 JWT를 사용해야 된다는 것을 깨닫게 되었습니다.</p>
<ul>
<li>기존의 Jsp 혹은 Thymeleaf 등의 Template Engine은 <strong>SSR (Server-Side-Rendering)</strong>으로, 서버 페이지이기 때문에 세션 정보를 관리할 수 있음</li>
<li>하지만 프론트를 프레임워크로 구현하게 되면 <strong>CSR (Client-Side-Rendering)</strong>으로, 서버 정보를 유지할 수 없음. ⇒ <strong>유저를 어떻게 판별할 것인가?</strong></li>
<li>정답은 JWT!</li>
</ul>
<hr>
<h1 id="1-jwt">1. JWT</h1>
<h2 id="11-definition">1.1 Definition</h2>
<p><strong>JWT</strong>는 <strong>Json Web Token</strong>의 약자로 <strong>JSON 객체로 정보를 안전하게 전송</strong>하는 방법을 정의하는 공개된 표준(RFC 7519)입니다.</p>
<p>JWT는 HMAC 알고리즘으로 비밀키를 사용하여 서명될 수 있고, RSA나 ECDSA를 사용한 공개/비공개 키 쌍으로도 서명될 수 있습니다.</p>
<br>


<h2 id="12-jwt-structure">1.2 JWT Structure</h2>
<p>JWT 토큰에는 <strong>Header</strong>, <strong>Payload</strong>, <strong>Signature</strong>로 세가지 구성요소로 나뉘어 있으며, 아래와 같은 형태를 지닙니다.</p>
<pre><code>HHHHH.PPPPP.SSSSS</code></pre><blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/4bcbde7e-f43c-4506-9e19-61c9a10d3b67/image.png" alt=""></p>
<p>출저: <a href="https://pradeepl.com/blog/jwt/">What is a JSON Web Token (JWT)?</a></p>
</blockquote>
<br>

<h3 id="header">Header</h3>
<p>토큰의 첫번째 부분인 <strong>헤더(Header)</strong>에는 서명시 사용하는 서명 암호화 알고리즘(alg), 사용할 타입(typ) 등 <strong>메타 정보</strong>가 담겨있습니다.</p>
<p>보통 헤더는 두 부분으로 구성됩니다.</p>
<ul>
<li>토큰의 종류 : JWT</li>
<li>사용되는 서명 알고리즘 : HMAC, SHA256, RSA</li>
</ul>
<pre><code class="language-js">{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}</code></pre>
<p>그리고 이 내용들은 JWT를 구성하기 위해 Base64Url로 인코딩됩니다.</p>
<br>

<h3 id="payload">Payload</h3>
<p>토큰의 두번째 부분인 <strong>페이로드(Payload)</strong>에는 <strong>토큰에 담을 정보</strong>가 들어있으며, 담는 정보의 한 조각을 <strong>클레임(claim)</strong>이라고 부릅니다.</p>
<p>클레임에는 크게 세가지 분류로 나뉘어 있습니다.</p>
<ol>
<li>등록된(registered) 클레임</li>
<li>공개(public) 클레임</li>
<li>비공개(private) 클레임</li>
</ol>
<br>

<h4 id="등록된-클레임">등록된 클레임</h4>
<p>먼저 등록된 클레임은 서비스에서 필요한 정보들이 아닌 토큰에 대한 정보를 담기 위해 이름이 이미 정해진 클레임들입니다.</p>
<ul>
<li><code>iss</code>: 토큰 발급자 (issuer)</li>
<li><code>sub</code>: 토큰 제목(subject)</li>
<li><code>aud</code>: 토큰 대상자(audience)</li>
<li><code>exp</code>: 토큰 만료시간(expiraton), 형식은 NumericDate로 되어 있어야합니다. (예: 1480849147370)</li>
<li><code>nbf</code>: Not Before를 의미하며, 토큰의 활성 날짜와 비슷한 개념입니다. 이 날짜가 지나기 전까지는 토큰이 처리되지 않습니다.</li>
<li><code>iat</code>: 토큰이 발급된 시간(issued at), 이 값을 사용하여 토큰의 <strong>age</strong>가 얼마나 되었는지 판단할 수 있습니다.</li>
<li><code>jti</code>: JWT의 고유 식별자로서, 주로 중복적인 처리 방지를 위해 사용됩니다.</li>
</ul>
<p>각 클레임들은 선택적(optional)으로 포함할 수 있습니다.</p>
<br>

<h4 id="공개-클레임">공개 클레임</h4>
<p>공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용됩니다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓습니다.</p>
<pre><code class="language-js">{
    &quot;https://localhost/jwt_claims/is_admin&quot;: true
}</code></pre>
<br>

<h4 id="비공개-클레임">비공개 클레임</h4>
<p>비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장합니다.</p>
<pre><code class="language-js">{
    &quot;username&quot;: &quot;admin&quot;
}</code></pre>
<br>

<h4 id="완성된-예시-페이로드">완성된 예시 페이로드</h4>
<pre><code class="language-js">{
  &quot;username&quot;: &quot;admin&quot;,
  &quot;auth&quot;: &quot;ROLE_USER,ROLE_ADMIN&quot;,
  &quot;https://localhost/jwt_claims/is_admin&quot;: true
  &quot;exp&quot;: 1724135496,
  &quot;iat&quot;: 1724133696
}</code></pre>
<br>

<h3 id="signature">Signature</h3>
<p>토큰의 마지막 부분인 <strong>서명(Signature)</strong>은 토큰을 <strong>인코딩하거나 유효성을 검증</strong>할 때 사용하는 고유한 암호화 코드입니다.</p>
<p>서명(Signature)을 만들기 위해서는 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱하고, 이 값을 다시 BASE64로 인코딩하여 생성합니다.</p>
<ol>
<li>헤더, 페이로드 Base64 인코딩</li>
<li>인코딩한 값을 해싱 (Header의 &#39;alg&#39; 알고리즘 + 비밀 키)</li>
<li>해싱한 값을 다시 Base64 인코딩</li>
<li>Signature 완성!</li>
</ol>
<hr>
<h1 id="2-spring--react-로그인">2. Spring + React 로그인</h1>
<p>Spring과 React는 각각 Server, Client로 볼 수 있습니다.</p>
<h2 id="process">Process</h2>
<p>로그인 프로세스는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/fbbaf8f3-62df-4699-9e14-809d97b0fb4c/image.png" alt="JWT 로그인 프로세스"></p>
<ol>
<li>클라이언트 단에서 사용자가 id, password를 이용해 로그인을 요청합니다.</li>
<li>서버에서 사용자 확인시 미리 정의된 Secret Key와 알고리즘을 통해 Access Token을 발급합니다. (+ 이 때 Refresh Token도 함께 고려될 수 있습니다)</li>
<li>클라이언트에 로그인 성공과 함께 jwt 토큰을 전달합니다</li>
<li>클라이언트는 각 서비스 요청시에 헤더에 jwt 토큰을 넣어 전달합니다.</li>
<li>서버는 요청 처리 전 JWT 서명을 체크하고, JWT 토큰을 분해하여 사용자 정보를 확인합니다.</li>
<li>서버는 클라이언트 요청에 대한 응답을 전달합니다</li>
</ol>
<br>

<h2 id="in-spring">in Spring</h2>
<p>Spring에서는 다음과 같은 항목을 구현해야 합니다.</p>
<ul>
<li>DB 연동하여 로그인 요청 프로세스 구현 (AuthenticationProvider 구현)</li>
<li>로그인 시 JWT 토큰 발급 (Token Provider 구현)</li>
<li>Request 요청시 Header에서 JWT 토큰을 통해 사용자 인증 정보 획득 (JwtRequestFilter 구현)</li>
</ul>
<br>

<h2 id="in-react">in React</h2>
<p>React에서는 다음과 같은 항목을 고려해야 합니다.</p>
<ul>
<li>로그인 요청시 받은 JWT 토큰 저장</li>
<li>매 요청시 헤더에 JWT 토큰 추가</li>
</ul>
<br>

<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://jwt.io/introduction">Introduction to JSON Web Tokens</a></li>
<li><a href="https://pradeepl.com/blog/jwt/">What is a JSON Web Token (JWT)?</a></li>
<li><a href="https://velopert.com/2389">[JWT] JSON Web Token 소개 및 구조</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>이번 포스팅에서는 다음과 같은 항목에 의해 포스팅하였습니다.😊</p>
<ul>
<li>JWT란 무엇인지</li>
<li>JWT를 이용하여 Spring과 React가 어떤 프로세스로 동작하는지</li>
<li>각 프로그램에서 어떤 것을 구현해야 되는지</li>
</ul>
<p>다음 포스팅에서는 Spring에서 구현해야될 항목에 대해 포스팅하겠습니다~🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring + React] 게시판 프로젝트 - (1) 개발 환경 세팅하기]]></title>
            <link>https://velog.io/@developer_khj/Spring-React-1-Setting</link>
            <guid>https://velog.io/@developer_khj/Spring-React-1-Setting</guid>
            <pubDate>Thu, 25 Jul 2024 08:55:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/developer_khj/post/f5919874-4fe0-4427-b928-597a1c934ed4/image.png" alt=""></p>
<h1 id="💎-들어가며">💎 들어가며</h1>
<blockquote>
<p>게시판 프로젝트를 하는 이유?!</p>
</blockquote>
<p>단순한 CRUD 성의 프로젝트지만, Spring Boot 3와 React를 연동하고 결과물을 내는데 의의가 있습니다.</p>
<p>가벼운 프로젝트지만 여러가지를 내포하고 있습니다.</p>
<ul>
<li>회사에서 개발한 프로젝트는 비공개이기 때문에 <strong>공개성 프로젝트</strong></li>
<li><strong>Java 17, Spring Boot 3</strong> 기반 프로젝트</li>
<li><strong>Spring Security 6</strong> 버전 연동</li>
<li>Spring + React 개발 환경 설정</li>
<li>Spring + React <strong>JWT 로그인</strong></li>
</ul>
<hr>
<h1 id="1-개발환경">1. 개발환경</h1>
<blockquote>
<ul>
<li>Java: 17</li>
<li>Spring Boot: 3.3.2</li>
<li>Build Tool: Gradle</li>
<li>Node: 20.15.1</li>
<li>React: 18.3.1</li>
<li>IDE: IntelliJ, WebStorm</li>
</ul>
</blockquote>
<p>위의 환경에서 개발하였습니다.
⚠️ 버전에 따라 코드가 다를 수 있습니다.</p>
<hr>
<h1 id="2-개발환경-세팅">2. 개발환경 세팅</h1>
<p>개발환경 세팅 먼저 시작해보도록 하겠습니다~!</p>
<br>

<h2 id="21-spring-프로젝트-설정">2.1 Spring 프로젝트 설정</h2>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<p>Spring Initializer를 통해 프로젝트를 생성합니다.</p>
<table>
<thead>
<tr>
<th>새 프로젝트</th>
<th>라이브러리 추가</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/2ce5e000-5d3c-4dc5-b374-6e193bde2467/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/098b31d6-a77f-4196-8311-a23f1f2b4061/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<p>Project 초기화면입니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/8778ade5-06cf-428b-9a6e-765d4554b9d8/image.png" alt="IntelliJ 초기 화면"></p>
<br>

<p>application.properties를 <strong>application.yml</strong>로 확장자를 변경하고, 아래와 같이 환경설정 정보를 넣어주었습니다.</p>
<pre><code class="language-yml">spring:
  application:
    name: board
  # DataSource Configuration
  datasource:
    url: jdbc:postgresql://127.0.0.1:5432/board
    username: postgres
    password:
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: create-drop
  devtools:
    restart:
      enabled: true
server:
  port: 8080
logging:
  level:
    org.springframework.web: info
</code></pre>
<br>

<h3 id="현재-시간-api-추가-apitime">현재 시간 API 추가: /api/time</h3>
<p>프론트 연동을 테스트 하기 위해 <strong>현재 시간을 가져오는 간단한 API</strong>를 추가하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/811a9481-2b4a-449d-91bd-b31ce7808a74/image.png" alt="패키지 구성 화면"></p>
<p><strong>ApiController 클래스를 생성</strong>하였습니다.</p>
<pre><code class="language-java">package io.github.twinklekhj.board.api;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@RestController
@Slf4j
public class ApiController {
    @GetMapping(&quot;/api/time&quot;)
    public ResponseEntity&lt;String&gt; getTime() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        return ResponseEntity.ok().body(LocalDateTime.now().format(dtf));
    }
}
</code></pre>
<br>

<h2 id="22-react-프로젝트-설정">2.2 React 프로젝트 설정</h2>
<h3 id="cra-프로젝트-생성">CRA 프로젝트 생성</h3>
<p>터미널을 열어 React 프로젝트를 생성합니다.
저는 <strong>TypeScript</strong> 기반 프로젝트를 CRA로 생성하였습니다.</p>
<pre><code class="language-shell">cd src/main
npx create-react-app frontend --template typescript</code></pre>
<pre><code>PS D:\Git\board\src\main&gt; npx create-react-app frontend

Creating a new React app in D:\Git\board\src\main\frontend.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts
with ccra-template-typescript...</code></pre><p><img src="https://velog.velcdn.com/images/developer_khj/post/b81ee905-1c5d-48d8-8d91-3db90b0d34c9/image.png" alt=""></p>
<p>Front IDE를 열어 프로젝트를 실행해줍니다.
(저는 WebStorm을 이용하였습니다.)</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/40d59857-e9a5-4739-a4a8-5069c0d3b6be/image.png" alt=""></p>
<p><a href="http://localhost:3000">http://localhost:3000</a> 로 접속할 수 있습니다.</p>
<br>

<h3 id="api-호출">API 호출</h3>
<p>fetch 메소드를 이용하여 API를 호출하는 코드를 예시로 작성해보았습니다.</p>
<pre><code class="language-tsx">// 파일 위치: src/App.tsx
import React, {useEffect} from &#39;react&#39;;

const App = () =&gt; {
    const [time, setTime] = React.useState(&quot;&quot;);
    useEffect(() =&gt; {
        fetch(&quot;http://localhost:8080/api/time&quot;, {
            method: &quot;GET&quot;
        })
            .then(res =&gt; {
                if (!res.ok) {
                    throw new Error(&#39;Network response was not ok&#39;);
                }
                return res.text();
            })
            .then(res =&gt; {
                setTime(res)
            })
            .catch(err =&gt; console.error(err));
    }, [])
    return (
        &lt;div&gt;
            Hi, Server time is {time}
        &lt;/div&gt;
    );
};

export default App;</code></pre>
<p><strong>냅다 호출코드에 url을 적으면</strong> 문제가 발생하리란 걸 아실 겁니다.</p>
<blockquote>
<p>바로 CORS 문제!!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/bf6558fe-2e58-4fe6-8a4f-0dcdc03aa1e1/image.png" alt="CORS 예시"></p>
<blockquote>
<h3 id="cors란">CORS란?</h3>
<p>CORS(Cross-Origin Resource Sharing)는 직역하면 &quot;교차 출처 리소스 공유&quot;로 URL에서 도메인만 뜻하는 게 아니라 프로토콜과 포트까지 포함하는 개념입니다.</p>
<p>출처를 구성하는 세 요소는 프로토콜·도메인(호스트 이름)·포트로, 이 중 하나라도 다르면 CORS 에러를 만나게 됩니다.
<img src="https://velog.velcdn.com/images/developer_khj/post/d5d116da-ad09-4626-818d-aa8d344199ff/image.png" alt="CORS 란?"></p>
</blockquote>
<br>

<h3 id="proxy-설정">Proxy 설정</h3>
<p>React에서 다른 애플리케이션을 호출할 때 생기는 CORS를 해결하기 위해서 <strong>Proxy 설정</strong>이 꼭 필요합니다.</p>
<p>React에서는 두가지 방법을 제공합니다.</p>
<ol>
<li>package.json에 proxy 프로퍼티에 url 추가</li>
<li>http-proxy-middle 라이브러리 설치 및 설정 스크립트 추가</li>
</ol>
<br>

<h4 id="packagejson">package.json</h4>
<p>package.json에 프로퍼티를 추가하는 방법은 간단합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/d3e248fd-9eb6-4cac-b424-8bddcac528e9/image.png" alt="package.json"></p>
<br>

<h4 id="http-proxy-middleware">http-proxy-middleware</h4>
<p>아래 명령어를 통해 http-proxy-middleware 모듈을 설치해줍니다.</p>
<pre><code class="language-shell">npm install http-proxy-middleware --save
npm install @types/http-proxy-middleware</code></pre>
<p>아래와 같이 설정합니다.</p>
<ul>
<li>React 프로젝트 아래에 <code>.env</code> 파일 생성</li>
<li>src 폴더 아래에 <code>setupProxy.js</code> 파일 생성</li>
</ul>
<p>파일을 넣기만 하면 자동으로 설정합니다.</p>
<blockquote>
<p>주의! ts 파일로 생성하면 자동으로 생성되지 않습니다.</p>
</blockquote>
<pre><code class="language-ts">// 파일 위치: frontend/.env
REACT_APP_API_URL=http://localhost:8080/api</code></pre>
<pre><code class="language-ts">// 파일위치: frontend/src/setupProxy.js

import { createProxyMiddleware } from &#39;http-proxy-middleware&#39;;

const { REACT_APP_API_URL } = process.env;
module.exports = function (app: any) {
    app.use(
        &#39;/api&#39;,
        createProxyMiddleware({
            target: REACT_APP_API_URL || &#39;http://localhost:8080/api&#39;,
            changeOrigin: true,
        })
    );
};</code></pre>
<p>이제 fetch 코드에서 api url을 빼줍니다.</p>
<pre><code class="language-ts">fetch(&quot;/api/time&quot;, {
  method: &quot;GET&quot;,
})
.then(res =&gt; {
  if (!res.ok) {
    throw new Error(&#39;Network response was not ok&#39;);
  }
  return res.text();
})
.then(res =&gt; {
  setTime(res)
})
.catch(err =&gt; console.error(err));</code></pre>
<h3 id="결과-화면">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e9a1820a-2856-49a8-bf2d-919165d1f5c5/image.png" alt="React 결과 화면"></p>
<br>

<h2 id="23-gradle-빌드-설정">2.3 Gradle 빌드 설정</h2>
<p>Spring Boot 빌드 배포시 자동으로 react 파일을 import 하도록 <strong>배포 자동화를 설정</strong>할 수 있습니다. build.gradle 파일을 열어 하단에 아래 내용을 추가해줍니다.</p>
<h3 id="빌드-스크립트-작성">빌드 스크립트 작성</h3>
<pre><code>processResources {
    dependsOn &quot;copyReactBuildFiles&quot;
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
}

// react 설치
tasks.register(&#39;installReact&#39;, Exec) {
    workingDir(&quot;$frontendDir&quot;)
    inputs.dir(&quot;$frontendDir&quot;)
    group = BasePlugin.BUILD_GROUP
    if (System.getProperty(&#39;os.name&#39;).toLowerCase(Locale.ROOT).contains(&#39;windows&#39;)) {
        commandLine &quot;npm.cmd&quot;, &quot;audit&quot;, &quot;fix&quot;
        commandLine &#39;npm.cmd&#39;, &#39;install&#39;
    } else {
        commandLine &quot;npm&quot;, &quot;audit&quot;, &quot;fix&quot; commandLine &#39;npm&#39;, &#39;install&#39;
    }
}

// react 빌드
tasks.register(&#39;buildReact&#39;, Exec) {
    dependsOn &quot;installReact&quot;
    workingDir &quot;$frontendDir&quot;
    inputs.dir &quot;$frontendDir&quot;
    group = BasePlugin.BUILD_GROUP
    if (System.getProperty(&#39;os.name&#39;).toLowerCase(Locale.ROOT).contains(&#39;windows&#39;)) {
        commandLine &quot;npm.cmd&quot;, &quot;run-script&quot;, &quot;build&quot;
    } else {
        commandLine &quot;npm&quot;, &quot;run-script&quot;, &quot;build&quot;
    }
}

// react build 파일 static 폴더로 복사
tasks.register(&#39;copyReactBuildFiles&#39;, Copy) {
    dependsOn &quot;buildReact&quot;
    from &quot;$frontendDir/build&quot;
    into &quot;$project.projectDir/src/main/resources/static&quot;
}</code></pre><p>Spring Boot 프로젝트가 build 될 때 실행시키는 스크립트 입니다.</p>
<ul>
<li>React 프로젝트가 먼저 build</li>
<li>결과물을 SpringBoot 프로젝트 build 결과물에 포함</li>
</ul>
<br>

<h3 id="결과-화면-1">결과 화면</h3>
<p>Spring 프로젝트를 실행만 하더라도 아래와 같이 화면이 구성된 것을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f87e388c-2e87-465c-a32c-6c1a273ad2b1/image.png" alt="Spring Boot 프로젝트 화면"></p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://velog.io/@u-nij/Spring-Boot-React.js-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85#%EC%82%AC%EC%9A%A9%ED%95%9C-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD">Spring Boot + React.js 개발환경 연동하기</a></li>
<li><a href="https://docs.tosspayments.com/resources/glossary/cors">CORS(교차 출저 리소스 공유)</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>이것으로 환경 설정이 마무리 되었습니다!! 다음 포스팅에서는 JWT를 이용한 Spring + React 로그인에 대해 포스팅하겠습니다🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백수일기] 치과 가다 - 사랑니 발치, 스케일링, 신경치료]]></title>
            <link>https://velog.io/@developer_khj/unemployee-diary-go-to-dentist</link>
            <guid>https://velog.io/@developer_khj/unemployee-diary-go-to-dentist</guid>
            <pubDate>Wed, 17 Jul 2024 10:18:06 GMT</pubDate>
            <description><![CDATA[<h1 id="1-치과에-가다">1. 치과에 가다</h1>
<p>마야흐로 1년전 백수가 먼저 된 친구와 이야기하면서 사랑니 치료에 대해 알게되었습니다. 백수 시절에 사랑니를 발치했다고... 일 안다닐때 가는게 좋다더군요!!🤗</p>
<br>

<h2 id="화이트덴트-치과">화이트덴트 치과</h2>
<p>약 5개월 전, 그 말이 떠올라 그렇게 무작정 집 앞에 <strong>화이트덴트 치과</strong>에 갔습니다. ⭐</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/44c7a62c-6e0f-442a-8a40-18a7b170c62f/image.png" alt="화이트덴트 치과"></p>
<p><a href="https://naver.me/xrPKu2U5">화이트 덴트 치과 바로가기</a></p>
<p>개업한 지도 얼마 안됐고, 치과들은 대부분 인테리어가 화이트 톤에 예쁘더라구요~</p>
<br>

<h2 id="시작된-검사">시작된 검사</h2>
<blockquote>
<p>&quot;사랑니 발치하고 싶어서 왔어요. 치과는 처음이니 종합검진 해주세요&quot;</p>
</blockquote>
<p>종합 검진을 받고 관리가 전혀 안된다는 말을 들었어요. 제 입장에선 당연한 결과라 그리 충격적이진 않았습니다. 치과에 간게 처음이니까요.🤣</p>
<p>하지만 그 말을 듣고, 치과도 <strong>건강 검진</strong>처럼 주기적으로 가서 검진을 해봐야 되겠다는 생각이 들었습니다.</p>
<p>종합검진 요약해보자면</p>
<ul>
<li>주기적으로 스케일링할 것</li>
<li>레진 치료 2개</li>
<li>사랑니 발치 4개</li>
<li>크라운을 뜯고 검사해볼 것</li>
</ul>
<br>

<h2 id="스케일링">스케일링</h2>
<p>종합 검진 그 후, 난생 처음으로 스케일링을 받았습니다. 28살이 되도록 내 발로 직접 치과에 간건 처음이라, 너무 너무 무서웠어요😢</p>
<p>하지만...!! 그 막연한 두려움이 <strong>스케일링</strong>을 받고 <strong>어마 무시한 두려움</strong>으로 바꼈습니다. 체감상 약 5-10분 정도 한 거같은데 세상이 안끝나는 거 같더라구요. 아직도 스케일링을 생각하면 끔찍해요🤢🤢</p>
<p>치료하면서 간호사님이 너무 긴장한거 같다구 춘식이 인형을 갖다 주시더라구요 ㅎㅎ</p>
<p>스케일링이 1년에 한번 건강보험 적용 혜택이 있다는 건 알았지만, 정확히 어떻게 적용된다는 건지도 이해를 못했었는데, 그냥 일반 병원가듯이 가면 되는 거였어요..!!</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b0f0f0a9-847a-4b78-8f7f-1d2c332ba471/image.png" alt="스케일링 가격"></p>
<p> 건강보험 가입자라면 1년에 1회에 한에 <strong>약 1만 5천원 정도</strong>로 스케일링을 받을 수 있습니다</p>
<br>

<h2 id="본격적인-사랑니-발치-계획">본격적인 사랑니 발치 계획</h2>
<h3 id="엑스레이-검사">엑스레이 검사</h3>
<p>본격적인 사랑니 발치 계획을 세우기 위해 <strong>치아 전체의 엑스레이</strong>를 찍었습니다. 기구에 치아를 악물면서 엑스레이를 찍었습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/74fa7817-dd2a-47c6-9b4a-5d8569d2113a/image.png" alt="엑스레이 결과"></p>
<p>&quot;사랑니가 4개 있으시군요. 위에 2개는 이미 썩었습니다.🤢
뽑는 건 선택이지만, 뽑는 게 좋을 거 같네요.
우선 급한 위에 사랑니 1개를 발치하겠습니다. 한개씩 밖에 안뽑아요~&quot;</p>
<p>사실 스케일링으로 너무 지쳐있던 나머지, 더 남았다는 사실이 무서워서 2개를 뽑을 엄두도 안났어요.🤣 모두 친절하게 잘 해주셔서 왕추천..!!!</p>
<br>

<h3 id="발치-계획">발치 계획</h3>
<ul>
<li>3/12 (당일) - 왼쪽 위 사랑니 발치. 스케일링 및 엑스레이 포함 약 5.5</li>
<li>3/26 (그후 일주일) - 오른쪽 위 사랑니 발치. 약 2.8</li>
</ul>
<p>일반 사랑니 빼는 건 얼마 안하더라구요</p>
<br>

<h3 id="발치한-치아">발치한 치아</h3>
<p>두근 두근..!!</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/65adaf05-cdfb-4358-8da4-c4c945ea6416/image.png" alt="발치한 치아"></p>
<p>제 발치한 치아입니다... 너무 더럽네요...😣 변명을 해보자면 입안 구조가 작은 탓에 가장 깊숙히 숨어 있던 놈입니다.. ㅎㅎ 사랑니가 이렇게 관리가 안되요~</p>
<p>그렇게 처음 사랑니를 빼고 집에 가면서, 가고서 <strong>며칠 동안 하나도 안아팠어요</strong>!!! 화이트덴트 치과 여기 사랑니 맛집이에요!!! 나머지 3개의 사랑니를 빨리 빼고 싶더라구요. 그 후 일주일 뒤에는 일주일 씩 빼는게 너~무 느리고 아프지도 않아서 사랑니 두개를 빼달라고 요청했더니..</p>
<blockquote>
<p>Me: 사랑니 두 개도 발치 가능한가요?
간호사: 환자분, 용기가 너무 가득해지셨는데요???</p>
</blockquote>
<p>춘식이 주신 간호사님의 용기가 가득하단 말을 듣고 너무 웃겨서 한참을 웃었습니다. 🤣🤣</p>
<p>But... <strong>결국 한개를 발치</strong>하게 됐어요. 우선, 일반 치과 병원에는 <strong>매복 사랑니</strong>를 발치해주지 않는다고 해요.. 그리고 <strong>어려운 치아</strong>(신경과 가까운 치아)도 발치해주지 않아요. (찾아보니 치료 거절 사례가 상당하더라구요)</p>
<p>치아는 신경이 예민해서 조금만 건드려도 얼굴 하관에 마비가 올 수 있다고 하더라구요. 그래서 일반병원에서는 못하고 전문병원을 가야된다고 합니다. 또, 그런 이유로 사랑니를 발치할 때마다 <strong>서약서</strong>를 작성합니다.</p>
<br>

<h1 id="2-사랑니-전문-병원에-가다">2. 사랑니 전문 병원에 가다</h1>
<p>기존 치과에서의 거절 끝에 <strong>사랑니 전문 병원</strong>을 탐색했어요. 역시 장소 검색은 Naver..!! 가장 가까운 병원인 <strong>퍼스트 사랑니 치과</strong>를 보면서 꽉 차있는 예약과 후기가 좋아서 선택했어요!💕</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/3c9eb4fe-9530-42bc-8f33-b2040fc40c98/image.png" alt="네이버 플레이스 검색결과"></p>
<p>네이버 플레이스 1등이라 더 믿음직 스러웠답니다.!</p>
<br>

<h2 id="퍼스트-사랑니-치과">퍼스트 사랑니 치과</h2>
<blockquote>
<p>2024.07.17 기준 예약 목록</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/cf759da3-942d-47d0-b209-fff3fc09a61d/image.png" alt=""></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/422d2abd-ff7e-484e-970c-ecc4e586d66d/image.png" alt=""></p>
<p><a href="https://naver.me/FABk6GSa">퍼스트 사랑니 치과 바로가기</a></p>
<p>가자마자 문진표 작성 후, 바로 엑스레이를 찍었습니다! 치과는 병원 기록이 공유가 되지 않아서 치과를 옮길 때마다 엑스레이같은 경우도 다시 찍어야됩니다.</p>
<p>처음부터 여기를 왔더라면 엑스레이 비용을 두번 내지 않았을 거 같다는 생각이.... 들었지만, 여기는 사랑니 전문 병원이라 사랑니 발치 외엔 거의 진료를 보지 않습니다..!! 어찌됐든 다른 치과는 필요했던 것이었죠. 🤗</p>
<p>진료 받으려고 누워있는데, 한번에 거의 세명씩 누워 있더라구요.</p>
<br>

<h2 id="사랑니-발치">사랑니 발치</h2>
<p>이번에는 아래 사랑니 두개를 발치해버렸습니다. 이 치과에서도 사랑니를 두개 발치하는 것을 권장하지는 병원을 또 가야되는게 너무 귀찮은 나머지, 두개를 한번에 발치해버렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b905ea7f-5fae-4955-9bf6-af11823ceb6d/image.png" alt=""></p>
<p>한개 뽑을 때랑은 달리 아래 사랑니 두개를 뽑아버려서 <strong>첫, 둘째 날은 죽만</strong> 먹었어요 😢😢 게다가 더 심각한건 매복 사랑니를 뺀 덕분에 치통이 발생하기 시작했습니다...</p>
<p>사랑니를 뺄 때도 <strong>옆에 치아를 건드리는 매복 사랑니</strong>는 옆에 치아를 계속 건드려서 썩기 때문에, 발치하게 되면 <strong>그 옆 치아는 신경치료</strong>를 해야될 거라고 하더라구요</p>
<p>가격은 사악했습니다.. 두개 빼서, 약 20만원 정도 깨졌어요</p>
<br>

<h1 id="3-신경-치료-시작">3. 신경 치료 시작</h1>
<p>시리고 아프고 말로 형용할 수 없는 치통으로 <strong>진통제로 겨우 버티며</strong> 4일을 고생하다 먼저 갔던 치과로 달려갔어요.</p>
<p>너무 아픈 치통으로 바로 신경치료를 시작하고 싶었지만, 사랑니 발치를 얼마 하지 않은 상태여서 치아가 자리 잡지 않았다고 하더군요.. 약 5일을 기다려 신경 치료를 시작했습니다.</p>
<blockquote>
<p>Me: 신경 치료, 많이 아픈가요..?
의사: 사랑니는 발치하면 끝이지만 신경치료는 오래걸리고 계속 아파요</p>
</blockquote>
<p>치료하다 보니 너무너무 아프더라구요... 한 번은 밥먹다가 갈비탕을 먹었는데 잘게 안잘랐다가 잘못해서 해당 이빨을 건드려서 순간 너무 아파서 엉엉 눈물 흘려버리고 밥을 안먹은 적이 있었네요...😂</p>
<p>그래도 진통제로 버틸 때보단 <strong>나을 거라는 기약</strong> 때문인지 참을 만 했어요. 🤦‍♀️</p>
<br>

<h2 id="신경치료-일정">신경치료 일정</h2>
<p>기억은 잘 안나지만 본뜨고 한 일주일 정도 기다려서, <strong>치료 만으로 약 3주</strong>, <strong>검진부터는 약 한달</strong>이 소요됐어요.</p>
<ul>
<li>사랑니 발치: 5/6</li>
<li>검진: 5/11</li>
<li>1차: 5/16 신경 긁어내기</li>
<li>2차: 5/21 신경 긁어내기</li>
<li>3차: 5/24 신경 채우기</li>
<li>4차: 5/31 본뜨기</li>
<li>5차: ?.? 크라운 씌우기</li>
</ul>
<br>

<h2 id="신경치료-가격">신경치료 가격</h2>
<p>신경치료 후 반드시 덧씌워야(크라운)되기 때문에 <strong>약 60만원 정도</strong> 깨집니다.</p>
<ul>
<li>신경 치료는 약 9만원</li>
<li>크라운은 약 50만원</li>
</ul>
<p>치과 치료를 받은 지 오래되셨다면 무조건 보험부터 들고 계획을 세우고 가세요.... 보험 들 생각을 안했던 저는 생돈을 전부 냈답니다..😢</p>
<br>

<h2 id="신경치료-과정">신경치료 과정</h2>
<p>신경 치료는 신경을 죽이고 긁어내서 채우고 크라운을 씌우는 과정이에요.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f07b7734-7873-4a26-83c3-1491ad46c53a/image.png" alt=""></p>
<p>두-세번 방문하여 신경을 긁어내고, 신경을 채운 다음, 이빨을 본을 뜨고 크라운을 제작한 다음 이빨 위에 씌워요.</p>
<br>

<h1 id="💎-마치며">💎 마치며</h1>
<p>길고 길었던 치과 치료... 약 90만원 정도 깨진거 같네요 🤦‍♀️ 그치만 적어도 지금은 치아 관리에 대해 조금은 알게 되었습니다. 주기적으로 치아관리를 위해 노력할 예정입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] ActiveMQ (2) - Spring Boot에서 ActiveMQ 사용하기]]></title>
            <link>https://velog.io/@developer_khj/Spring-Boot-ActiveMQ-Integration</link>
            <guid>https://velog.io/@developer_khj/Spring-Boot-ActiveMQ-Integration</guid>
            <pubDate>Sat, 06 Jul 2024 17:45:50 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 Spring에서 ActiveMQ를 세팅하고 구현하는 과정에 대해 기술하였습니다.</p>
<hr>
<h1 id="1-구현에-앞서">1. 구현에 앞서</h1>
<p>Spring에서 <strong>ActiveMQ 클라이언트를 구현</strong>하기 앞서 필요한 JMS 주요 개념을 먼저 살펴보겠습니다.</p>
<br>

<h2 id="11-jms">1.1 JMS</h2>
<p>ActiveMQ는 <strong>Java로 개발</strong>된 MQ 미들웨어로, <strong>JMS를 기반으로 통신을 제어</strong>합니다.
즉, ActiveMQ는 JMS의 구현체입니다.</p>
<blockquote>
<h3 id="jms란">JMS란?</h3>
<p>MOM (메시지 지향 미들웨어)를 구현한 자바 API</p>
</blockquote>
<br>

<h3 id="jms의-주요-구성요소">JMS의 주요 구성요소</h3>
<blockquote>
<p>공통 인터페이스</p>
</blockquote>
<ul>
<li><strong>ConnectionFactory</strong>: Connection 연결 설정 정보 관리 인터페이스.</li>
<li><strong>Session</strong>: 연결 세션 관리 인터페이스. Producer, Consumer, Listener, Message 등 관리 기능 수행.</li>
<li><strong>Destination</strong>: 목적지 인터페이스. 메세지를 수신받을 목적지 (Consumer 의 주소)</li>
<li><strong>Message</strong>: 전송 할 메시지 인터페이스.</li>
</ul>
<blockquote>
<p>메세지 방식 인터페이스</p>
</blockquote>
<ul>
<li><strong>Queue</strong>: Queue는 특정 Consumer에게 메시지 전송</li>
<li><strong>Topic</strong>: Topic은 발행(Publish)-구독(Subscribe) 모델로, Topic 구독자에게 메시지 전송</li>
</ul>
<blockquote>
<p>메시지 송수신 인터페이스</p>
</blockquote>
<ul>
<li><strong>MessageProducer</strong>: 메시지 전송 인터페이스</li>
<li><strong>MessageConsumer</strong>: 메시지 수신 처리 인터페이스</li>
<li><strong>MessageListener</strong>: 메시지 수신 처리 콜백 인터페이스</li>
</ul>
<br>

<h3 id="jms-연결-순서">JMS 연결 순서</h3>
<ol>
<li>메시지 브로커 연결을 위한 <strong>Connection Factory</strong> 생성</li>
<li>큐, 토픽 중 방식을 선택하여 <strong>Destination</strong> 생성</li>
<li>브로커 서버 <strong>연결 ⇒ 세션</strong> 취득</li>
<li>메시지 <strong>Producer</strong>나 <strong>Consumer</strong>를 생성하여 메시지 송수신</li>
<li><strong>Exception</strong> 처리</li>
<li>세션 및 연결 닫기</li>
</ol>
<br>

<h3 id="java-예시-코드">Java 예시 코드</h3>
<pre><code class="language-java">@RequiredArgsConstructor
@Slf4j
class MessageService {
  public boolean send(String message) {
      // 1. ActiveMQ ConnectionFactory 생성
      ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(&quot;tcp://localhost:61616&quot;);
      Connection connection = connectionFactory.createConnection();

      // 2. 목적지(Destination) 생성
      // 2.1 큐(Queue) 생성
      Queue queue = new ActiveMQQueue(&quot;Queue 이름&quot;);

      // 2.2 토픽(Topic) 생성
      Topic topic = new ActiveMQTopic(&quot;Topic 이름&quot;);

      // 6. Try 문에서 객체를 생성하여 Auto-close
      try (ActiveMQConnection connection = (ActiveMQConnection) connectionFactory.createConnection();
           ActiveMQSession session = (ActiveMQSession) connection.createSession(false,  ActiveMQSession.AUTO_ACKNOWLEDGE)) {

          // 3. 세션 취득
          connection.start();

          // 4. 목적지 전송
          // 4.1 Producer 생성
          MessageProducer producer = session.createProducer(queue);
          producer.setDeliveryMode(DeliveryMode.PERSISTENT);

          // 4.2 Message 생성
          Message m = session.createTextMessage(message);

          // 4.3 Message 전송
          producer.send(m);

          log.info(&quot;ActiveMQ 메세지 전송. 메세지: {}&quot;, message);

          return true;
      }
      // 5. Exception 처리
      catch (JMSException e) {
          log.error(&quot;JMS 에러 발생. 메시지: {}&quot;, e.getMessage());
          return false;
      }
  }
}</code></pre>
<br>

<h3 id="jmstemplate">JmsTemplate</h3>
<p>자바에서 제공하는 기본 API인 <strong>JMS를 이용하면</strong> JDBC와 유사하게 연결부터 데이터 송수신까지 <strong>수많은 보일러플레이트 코드를 작성</strong>해야합니다.</p>
<p>스프링은 템플릿 기반으로 JMS 코드를 단순화하는 솔루션을 제공합니다. JMS 템플릿 클래스 JmsTemplate을 이용하면 소량의 코드만으로도 JMS 메시지를 주고받을 수 있습니다.</p>
<hr>
<h1 id="2-spring-boot-설정">2. Spring Boot 설정</h1>
<h2 id="21-project-setting">2.1 Project Setting</h2>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<p>우선 테스트 프로그램을 만들기 위해 Spring Initializr를 이용해 프로젝트를 생성하였습니다.</p>
<blockquote>
<ul>
<li>프로젝트명: mqclient</li>
<li>Java: 17</li>
<li>Spring Boot: 3.3.1</li>
</ul>
</blockquote>
<table>
<thead>
<tr>
<th>프로젝트 환경 설정</th>
<th>종속성 추가</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/75296cc4-4044-44d0-946b-2b2e2dfb2bb0/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/cc3901ae-8be7-49f2-8887-467c19ddc0f6/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<p>이번 포스팅에서 다루지는 않을 예정이지만, Spring Web + Thymeleaf + WebSocket을 추가하여 Front에서 기능을 구현하면 실시간 채팅 애플리케이션을 구현할 수 있습니다</p>
<br>

<h3 id="라이브러리-추가">라이브러리 추가</h3>
<p>추가적으로 Lombok 및 Json 관련 라이브러리를 추가해주었습니다</p>
<pre><code class="language-xml">&lt;properties&gt;
  &lt;java.version&gt;17&lt;/java.version&gt;
  &lt;jackson.version&gt;2.17.2&lt;/jackson.version&gt;
&lt;/properties&gt;

&lt;!-- Lombok --&gt;
&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
    &lt;artifactId&gt;lombok&lt;/artifactId&gt;
  &lt;/dependency&gt;

  &lt;!-- JSON --&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.json&lt;/groupId&gt;
    &lt;artifactId&gt;json&lt;/artifactId&gt;
    &lt;version&gt;20231013&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-core&lt;/artifactId&gt;
    &lt;version&gt;${jackson.version}&lt;/version&gt;
    &lt;scope&gt;provided&lt;/scope&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
    &lt;version&gt;${jackson.version}&lt;/version&gt;
    &lt;scope&gt;provided&lt;/scope&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.datatype&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-datatype-jsr310&lt;/artifactId&gt;
    &lt;version&gt;${jackson.version}&lt;/version&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;</code></pre>
<p>롬복 플러그인 설정</p>
<pre><code class="language-xml">&lt;plugin&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
  &lt;configuration&gt;
    &lt;excludes&gt;
      &lt;exclude&gt;
        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
      &lt;/exclude&gt;
    &lt;/excludes&gt;
  &lt;/configuration&gt;
&lt;/plugin&gt;</code></pre>
<br>

<h3 id="activemq-5184-다운로드">ActiveMQ 5.18.4 다운로드</h3>
<p>최신버전은 6.x 지만 라이브러리가 <strong>Spring for Apache ActiveMQ 5</strong> 이므로 5.x를 다운받았습니다.
 <img src="https://velog.velcdn.com/images/developer_khj/post/9d9b8d87-fde9-4897-b704-e9ba7a06ff9d/image.png" alt="ActiveMQ 5.18.4 버전"></p>
<br>

<h2 id="22-configuration">2.2 Configuration</h2>
<h3 id="applicationyml">application.yml</h3>
<p><code>application.yml</code> 파일에 사용할 브로커의 설정 값을 입력합니다.</p>
<pre><code class="language-yml">spring:
  activemq:
    broker-url: tcp://localhost:61616 # ActiveMQ Broker URL
    user: admin       # ActiveMQ 웹 관리 콘솔 아이디
    password: admin   # ActiveMQ 웹 관리 콘솔 비밀번호</code></pre>
<br>

<p>이 설정 값들은 <strong>ActiveMQProperties 객체</strong>로 활용이 가능합니다.</p>
<pre><code class="language-java">// ActiveMQ Class 내부
@ConfigurationProperties(
    prefix = &quot;spring.activemq&quot;
)
public class ActiveMQProperties {
    private static final String DEFAULT_NETWORK_BROKER_URL
                    = &quot;tcp://localhost:61616&quot;;
    private String brokerUrl;
    private String user;
    private String password;
    private Duration closeTimeout = Duration.ofSeconds(15L);
    private boolean nonBlockingRedelivery = false;
    private Duration sendTimeout = Duration.ofMillis(0L);
    @NestedConfigurationProperty
    private final JmsPoolConnectionFactoryProperties pool = 
                    new JmsPoolConnectionFactoryProperties();
    private final Packages packages = new Packages();
}</code></pre>
<br>

<h3 id="config-클래스-생성">Config 클래스 생성</h3>
<p>ActiveMQ에서 사용할 <strong>Bean들을 관리하기 위한 클래스</strong>를 생성하고, <strong>@Configuration</strong>을 이용하여 설정합니다.</p>
<p>ActiveMQ에서 사용할 Bean 들에는 ConnectionFactory, JmsTemplate, Queue, Topic, MessageConverter, JmsListenerContainerFactory 등이 있습니다.</p>
<br>

<p>초기 설정에는 <strong>ConnectionFactory</strong> 및 <strong>JmsTemplate</strong>을 추가해주었습니다.</p>
<pre><code class="language-java">import org.apache.activemq.ActiveMQConnectionFactory;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;

@Configuration
@EnableConfigurationProperties(ActiveMQProperties.class)
public class ActiveMQConfig {
    @Bean
    public ActiveMQConnectionFactory connectionFactory(ActiveMQProperties properties) {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(properties.getBrokerUrl());
        factory.setUserName(properties.getUser());
        factory.setPassword(properties.getPassword());
        factory.setCloseTimeout(properties.getCloseTimeout().toMillisPart());

        return factory;
    }

    @Bean
    public JmsTemplate jmsTemplate(ActiveMQConnectionFactory factory) {
        JmsTemplate template = new JmsTemplate(factory);
        template.setExplicitQosEnabled(true);    // 메시지 전송 시 QOS을 설정
        template.setDeliveryPersistent(false);   // 메시지의 영속성을 설정
        template.setReceiveTimeout(1000 * 3);    // 메시지를 수신하는 동안의 대기 시간을 설정(3초)
        template.setTimeToLive(1000 * 60 * 30);  // 메시지의 유효 기간을 설정(30분)

        return template;
    }
}</code></pre>
<br>

<p><strong>@EnableConfigurationProperties</strong> 어노테이션을 활용하여 <strong>ActiveMQProperties 객체</strong>를 바인딩해주었습니다</p>
<br>

<h3 id="messageconverter-등록">MessageConverter 등록</h3>
<p><strong>Java 객체</strong>를 <strong>JSON 형태</strong>로 변환하기 위해서는 <strong>Converter를 등록</strong>해야합니다.
ActiveMQConfig 클래스에 다음과 같이 Converter를 추가해줍니다.</p>
<pre><code class="language-java">@Bean
public MappingJackson2MessageConverter messageConverter() {
    MappingJackson2MessageConverter messageConverter =
            new MappingJackson2MessageConverter();
    messageConverter.setTypeIdPropertyName(&quot;_typeId&quot;);

    // VO 타입 추가하기
    Map&lt;String, Class&lt;?&gt;&gt; typeIdMappings = new HashMap&lt;&gt;();
    typeIdMappings.put(&quot;chat&quot;, ChatMessage.class);

     essageConverter.setTypeIdMappings(typeIdMappings);

    return messageConverter;
}</code></pre>
<p>추가한 Converter를 <strong>JmsTemplate에 추가</strong>해줍니다.</p>
<pre><code class="language-java">@Bean
public JmsTemplate jmsTemplate(ActiveMQConnectionFactory factory, 
                                MappingJackson2MessageConverter messageConverter) {
    JmsTemplate template = new JmsTemplate(factory);
    template.setMessageConverter(messageConverter); // 메시지 Converter 설정

    ... (중략)

    return template;
}</code></pre>
<hr>
<h1 id="3-message-송수신">3. Message 송수신</h1>
<p>이제 간단한 Message 송수신을 구현해볼 차례입니다!</p>
<h2 id="31-vo-구현">3.1 VO 구현</h2>
<p>우선, 메시지를 VO 객체로 구현합니다.
저는 Chatting Message를 구현했습니다.</p>
<pre><code class="language-java">@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    private String userId;
    private String msg;

    @JsonSerialize(using = CustomLocalDateTimeSerializer.class)
    @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class)
    private LocalDateTime date;
}</code></pre>
<br>

<h3 id="localdatatime-처리">LocalDataTime 처리</h3>
<p>직렬화, 역직렬화 예시를 보여주기 위해 VO 객체 필드에 많이 사용하는 LocalDateTime 객체를 추가했습니다.</p>
<p>LocalDateTime 객체는 json 형태로 자동 변환을 지원하지 않아서 <strong>직렬화 및 역직렬화를 직접 구현</strong>해야합니다.</p>
<pre><code class="language-java">// 직렬화 코드
@JsonComponent
public class CustomLocalDateTimeSerializer extends StdSerializer&lt;LocalDateTime&gt; {
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&quot;);

    public CustomLocalDateTimeSerializer() {
        this(null);
    }

    protected CustomLocalDateTimeSerializer(Class&lt;LocalDateTime&gt; t) {
        super(t);
    }

    @Override
    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(localDateTime.format(DATE_TIME_FORMATTER));
    }
}

// 역직렬화 코드
@JsonComponent
public class CustomLocalDateTimeDeserializer extends StdDeserializer&lt;LocalDateTime&gt; {
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&quot;);

    public CustomLocalDateTimeDeserializer() {
        this(null);
    }

    public CustomLocalDateTimeDeserializer(Class&lt;?&gt; vc) {
        super(vc);
    }

    @Override
    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        String dateString = node.asText();
        return LocalDateTime.parse(dateString, DATE_TIME_FORMATTER);
    }
}</code></pre>
<br>

<h2 id="32-destination-관리">3.2 Destination 관리</h2>
<p>현업에서는 메세지 프로토콜이 이미 정의되어 있기 때문에 Destination (Queue, Topic) 등을 String으로 보낼 필요가 없습니다.</p>
<h3 id="properties-설정">properties 설정</h3>
<p>application.yml 파일에 사용할 Queue와 Topic의 이름을 관리합니다.</p>
<pre><code class="language-yml"># 파일명: application.yml

activemq:
  queue:
    chat: chat
  topic:
    chat: chat</code></pre>
<br>

<h3 id="환경설정-클래스-수정">환경설정 클래스 수정</h3>
<p>환경설정 클래스(ActiveMQConfig)에 <strong>Bean을 추가</strong>하여 해당 Destination을 <strong>싱글톤 객체로 관리</strong>해줍니다</p>
<pre><code class="language-java">@Bean
public ActiveMQQueue chatQueue(@Value(&quot;${activemq.queue.chat}&quot;) String queueName) {
    return new ActiveMQQueue(queueName);
}

@Bean
public Topic chatTopic(@Value(&quot;${activemq.topic.chat}&quot;) String topicName) {
    return new ActiveMQTopic(topicName);
}</code></pre>
<br>

<h2 id="33-메세지-보내기">3.3 메세지 보내기</h2>
<h3 id="인터페이스">인터페이스</h3>
<p>Topic과 Queue를 보내는 간단한 인터페이스입니다.</p>
<pre><code class="language-java">public interface ChattingService {
    boolean send(Destination destination, ChatMessage message);
    boolean sendQueue(ChatMessage message);
    boolean sendTopic(ChatMessage message);
}</code></pre>
<br>

<h3 id="구현">구현</h3>
<p><strong>JmsTemplate</strong>을 이용하여 간편하게 구현이 가능합니다.
이미 정의한 <strong>Queue, Topic</strong>을 <strong>Autowiring</strong> 하여 메시지를 전송합니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class ChatServiceImpl implements ChatService {
    private final JmsTemplate template;
    private final Queue chatQueue;
    private final Topic chatTopic;

    @Override
    public boolean send(Destination destination, ChatMessage message) {
        try {
            template.convertAndSend(destination, message);

            return true;
        } catch (JmsException exception) {
            log.error(&quot;JMS Exception 발생: {}&quot;, exception.getMessage());
            exception.getStackTrace();
            return false;
        }
    }

    @Override
    public boolean sendQueue(ChatMessage message) {
        log.info(&quot;[Queue] send message: {}&quot;, message);
        return send(chatQueue, message);
    }

    @Override
    public boolean sendTopic(ChatMessage message) {
        log.info(&quot;[Topic] send message: {}&quot;, message);
        return send(chatTopic, message);
    }
}</code></pre>
<br>

<h2 id="34-메시지-받기">3.4 메시지 받기</h2>
<h3 id="메시지-수신-컨테이너-등록">메시지 수신 컨테이너 등록</h3>
<p>메시지를 수신받기 위해서는 <strong>메시지 수신 컨테이너</strong>를 등록해야합니다. 환경설정 클래스에서 JmsListenerContainerFactory 빈을 등록해줍니다.</p>
<pre><code class="language-java">@Bean
public JmsListenerContainerFactory&lt;?&gt; queueContainerFactory(
        ActiveMQConnectionFactory connectionFactory, MappingJackson2MessageConverter messageConverter) {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    return factory;
}

@Bean
public JmsListenerContainerFactory&lt;?&gt; topicContainerFactory(
       ActiveMQConnectionFactory connectionFactory,
       MappingJackson2MessageConverter messageConverter) {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();

    // Topic 구조로 설정
    factory.setPubSubDomain(true);

    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    return factory;
}</code></pre>
<br>


<h3 id="메시지-리스너-구현">메시지 리스너 구현</h3>
<p>메시지 리스너를 구현하는 방법에는 두가지가 있습니다</p>
<ol>
<li><strong>@JmsListener</strong>를 이용하여 <strong>메소드 단에서 구현</strong></li>
<li><strong>MessageListener 인터페이스를 구현</strong>하여 DefaultMessageListenerContainer에 등록하여 <strong>클래스 단에서 구현</strong></li>
</ol>
<br>

<blockquote>
<h4 id="메소드-단에서-구현">메소드 단에서 구현</h4>
</blockquote>
<p><strong>@JmsListener</strong> 어노테이션을 이용하면 리스너를 손쉽게 구현할 수 있습니다</p>
<pre><code class="language-java">@Component
@Slf4j
public class ChatListener {
    @JmsListener(destination = &quot;${activemq.queue.chat}&quot;, containerFactory = &quot;queueContainerFactory&quot;)
    public void receiveQueue(ChatMessage message) {
        log.info(&quot;[Queue] receive message: {}&quot;, message);
    }

    @JmsListener(destination = &quot;${activemq.topic.chat}&quot;, containerFactory = &quot;topicContainerFactory&quot;)
    public void receiveTopic(ChatMessage message) {
        log.info(&quot;[Topic] receive message: {}&quot;, message);
    }
}</code></pre>
<br>

<blockquote>
<h4 id="클래스-단에서-구현">클래스 단에서 구현</h4>
</blockquote>
<p>또 다른 방법으로는 MessageListener를 구현하여 DefaultMessageListenerContainer에 등록하는 방법입니다.</p>
<p>먼저 <strong>리스너를 구현할 클래스를 생성</strong>하여 MessageListener 인터페이스를 implements 합니다.</p>
<pre><code class="language-java">@Component
@Slf4j
public class TopicListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        log.info(&quot;[Topic Object] receive message: {}&quot;, message);
    }
}</code></pre>
<p>그런 다음 <strong>DefaultMessageListenerContainer 빈을 추가</strong>하여 구현한 MessageListener를 등록합니다. </p>
<pre><code class="language-java">@Bean
public DefaultMessageListenerContainer topicContainerFactory2(
        ActiveMQConnectionFactory factory, Topic topic, TopicListener listener) {
    DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
    container.setConnectionFactory(factory);
    container.setDestination(topic);

    // 1. 구현한 클래스 등록
    container.setMessageListener(listener);

    // 2. 람다식 구현
    container.setMessageListener((MessageListener) message -&gt; {
        log.info(&quot;[Topic - Lambda] receive message: {}&quot;, message);
    });

    return container;
}</code></pre>
<p>꼭 클래스 파일을 생성하지 않아도 <strong>람다 익명 객체</strong>를 이용하여 메시지 리스너를 구현할 수도 있습니다.</p>
<p>메시지 리스너를 클래스로 구현할 때 한가지 아쉬운 점은 MessageListener가 Message 객체를 처리할 것을 요구하기 때문에 <strong>VO로 바로 받기 어렵다</strong>는 점입니다</p>
<p>Message 객체를 로깅해보면 아주 아주 많은 정보를 담고 있는 것을 볼 수 있습니다.</p>
<pre><code>ActiveMQBytesMessage {
commandId = 5, responseRequired = false, 
messageId = ID:DESKTOP-05O71ON-63104-1720286743523-1:5:1:1:1, 
originalDestination = null, originalTransactionId = null, 
producerId = ID:DESKTOP-05O71ON-63104-1720286743523-1:5:1:1, 
destination = topic://TOPIC_CHAT, 
transactionId = null, 
deliveryTime = 0, expiration = 1720288545781, timestamp = 1720286745781, arrival = 0, 
brokerInTime = 1720286745782, brokerOutTime = 1720286745794,
correlationId = null, replyTo = null, persistent = false, 
type = null, priority = 4, groupID = null, groupSequence = 0, 
targetConsumerId = null, compressed = false, userID = null, 
content = org.apache.activemq.util.ByteSequence@656d5cea, 
marshalledProperties = org.apache.activemq.util.ByteSequence@38811fe, 
dataStructure = null, redeliveryCounter = 0, size = 0, 
properties = {_typeId=chat}, readOnlyProperties = true, 
readOnlyBody = true, droppable = false, jmsXGroupFirstForConsumer = false} 
ActiveMQBytesMessage{ bytesOut = null, dataOut = null, dataIn = null 
}</code></pre><blockquote>
<h4 id="message-to-object">Message To Object</h4>
</blockquote>
<p>이 Message를 parsing하기 위해서는 아래와 같이 구현이 가능합니다.</p>
<pre><code class="language-java">@Component
@Slf4j
public class TopicListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        log.info(&quot;[Topic Object] receive message: {}&quot;, message);
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            ChatMessage body = null;
            try {
                body = textMessage.getBody(ChatMessage.class);
            } catch (JMSException e) {
                throw new RuntimeException(e);
            }
            log.info(&quot;[Topic Object] receive body: {}&quot;, body);
        }
    }
}</code></pre>
<hr>
<h1 id="4-test-코드-작성">4. Test 코드 작성</h1>
<p>메시지 송수신이 잘 이루어지고 있는지 테스트 코드를 작성하여 확인하였습니다.</p>
<pre><code class="language-java">@SpringBootTest
class MqclientApplicationTests {
    private final ChatService chatService;

    @Autowired
    MqclientApplicationTests(ChatService chatService) {
        this.chatService = chatService;
    }

    @Test
    @DisplayName(&quot;Queue 테스트&quot;)
    void topicQueue() throws JMSException {
        ChatMessage message = ChatMessage.builder()
                .userId(&quot;twinklekhj&quot;)
                .msg(&quot;안녕하세요!&quot;)
                .date(LocalDateTime.now())
                .build();

        Assertions.assertTrue(chatService.sendQueue(message), &quot;Queue 보내기 실패&quot;);
    }
    @Test
    @DisplayName(&quot;Topic 테스트&quot;)
    void topicTopic() throws JMSException {
        ChatMessage message = ChatMessage.builder()
                .userId(&quot;twinklekhj&quot;)
                .msg(&quot;좋은 하루 보내세요~&quot;)
                .date(LocalDateTime.now())
                .build();

        Assertions.assertTrue(chatService.sendTopic(message), &quot;Topic 보내기 실패&quot;);
    }
}</code></pre>
<p>테스트 로그는 다음과 같습니다!</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f1c58f9e-7316-4eab-9d3e-312539d4be59/image.png" alt="테스트 화면"></p>
<p>안녕하세요~ 좋은 하루보내세요😊🙌</p>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>이미 ActiveMQ 클라이언트 프로그램을 완벽하게 구현했다고 생각했지만, 내용을 정리하면서 기존 코드가 얼마나 깔끔하지 못했는지 다시 한번 회고하게 되는 시간이었습니다😢</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Artillery로 부하 테스트하기]]></title>
            <link>https://velog.io/@developer_khj/what-is-artillery</link>
            <guid>https://velog.io/@developer_khj/what-is-artillery</guid>
            <pubDate>Thu, 13 Jun 2024 09:31:50 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>&quot;포트폴리오에 유의미한 성과에 대해 적으려면 성능에 관해 적어야되는데 성능은 어떻게 체크할까?&quot; 에 대해 고민하다가 <strong>Artillery</strong>를 알게 되었습니다.</p>
<p>이번 포스팅에서는 Artillery의 간단한 사용법에 대해 적어볼까 합니다.🤗</p>
<hr>
<h1 id="1artilley란">1.Artilley란?</h1>
<p>Artilley는 <strong>Node.js 기반의 부하테스트 도구</strong>입니다. HTTP 혹은 websocket request를 원하는 시나리오대로 요청할 수 있습니다.</p>
<p>이를테면 아래 같은 상황을 재현할 수 있습니다.</p>
<blockquote>
<p>&quot;10분동안 초당 5개의 POST /api/info 와 같은 URL 요청을 쏜다.&quot;</p>
</blockquote>
<hr>
<h1 id="2-설치">2. 설치</h1>
<p>우선 Artillery는 <strong>Node 기반의 프로그램</strong>이기 때문에 Node가 설치되어있어야합니다. 터미널에 아래와 같은 명령어를 통해서 Artillery를 설치해줍니다.</p>
<pre><code class="language-bash">npm install -g artillery@latest</code></pre>
<br>

<p>이렇게 설치하고 아래 명령어를 통해서 버전이 잘 확인되면 잘 설치된 것입니다.</p>
<pre><code class="language-bash">&gt; npx artillery version

        ___         __  _ ____
  _____/   |  _____/ /_(_) / /__  _______  __ ___
 /____/ /| | / ___/ __/ / / / _ \/ ___/ / / /____/
/____/ ___ |/ /  / /_/ / / /  __/ /  / /_/ /____/
    /_/  |_/_/   \__/_/_/_/\___/_/   \__  /
                                    /____/


VERSION INFO:

Artillery: 2.0.15
Node.js:   v20.10.0
OS:        win32</code></pre>
<p>자세한 내용은 <a href="https://www.artillery.io/docs/get-started/get-artillery">공식 Docs</a>에서 볼수 있습니다.</p>
<hr>
<h1 id="3-test-들어가기">3. Test 들어가기</h1>
<p>concept에 대해 정리하기 전에 Artillery란 무엇인지 파악하기 위해 우선 test 코드를 예제대로 짜서 따라해보았습니다.</p>
<h2 id="31-quick-명령어">3.1 quick 명령어</h2>
<pre><code class="language-cmd">&gt; artillery quick --count 1000 -n 100 http://localhost:8080</code></pre>
<p>artillery를 빠르게 실행해보는 방법은 quick 명령어를 사용하는 것입니다. 테스트 스크립트 없이 API를 테스트 해볼 수 있습니다.</p>
<pre><code class="language-cmd">artillery quick [TARGET] [-c &lt;value&gt;] [-n &lt;value&gt;] [-o &lt;value&gt;] [-k] [-q]</code></pre>
<p>주요 옵션</p>
<ul>
<li><code>-c</code>, <code>--count value</code>: 생성할 가상 사용자 수 (기본 값: 10)</li>
<li><code>-n</code>, <code>--num</code>: 가상사용자가 보샐 요청 수 (기본 값: 10)</li>
<li><code>-o</code>, <code>--output</code>: JSON 리포트 파일명</li>
<li><code>-q</code>, <code>--quiet</code>: Quiet 모드</li>
</ul>
<br>

<h2 id="32-run-명령어">3.2 run 명령어</h2>
<h3 id="script-작성">Script 작성</h3>
<pre><code class="language-yml">config:
  target: &quot;http://localhost:8080&quot;
  phases:
    - duration: 60        # 지속시간
      arrivalRate: 5     # 초당 요청수
scenarios:
  - name: Books
    flow:
      - post:
          url: &quot;/api/books&quot;</code></pre>
<p>60초 동안 초당 5명의 사용자가 동시접속을 시나리오로 도서목록 요청을 보내는 간단한 테스트 스크립트 입니다.</p>
<br>

<h3 id="실행">실행</h3>
<pre><code class="language-bash">&gt; artillery run test.yml --output test.json</code></pre>
<p>run 명령어를 통해 작성한 스크립트를 수행합니다.</p>
<ul>
<li>--output 옵션을 통해 JSON 형태의 리포트 파일을 받을 수 있습니다.</li>
</ul>
<br>

<p>그 밖의 명령어에 대한 정보는 --help 옵션을 통해 얻을 수 있습니다.</p>
<pre><code class="language-bash">&gt; artillery run --help</code></pre>
<br>

<h3 id="report">Report</h3>
<pre><code class="language-bash">&gt; artillery report test.json</code></pre>
<br>

<p>report 명령을 날리면 <strong>DEPRECATION 경고</strong>와 함께 html을 생성해줍니다.</p>
<pre><code class="language-cmd">&gt; artillery report test.json

┌───────────────────────────────────────────────────────────────────────┐
│                          DEPRECATION NOTICE                           │
├───────────────────────────────────────────────────────────────────────┤
│ The &quot;report&quot; command is deprecated and will be removed in a future    │
│ release of Artillery.                                                 │
│                                                                       │
│ Artillery Cloud is now the recommended way to visualize test results. │
│ It provides more comprehensive reporting, advanced visualizations,    │
│ and includes a free tier.                                             │
│                                                                       │
│ Sign up on https://app.artillery.io                                 │
└───────────────────────────────────────────────────────────────────────┘

Report generated: test.json.html</code></pre>
<br>

<p><code>test.json.html</code>을 클릭해서 들어가보면 test.json 의 성능 지표를 표와 그래프로 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/1792e4e3-ebbf-444f-875a-f94f8d63557a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/ce3758fd-8aa5-4f38-873f-e6f77f73e51f/image.png" alt=""></p>
<br>

<h2 id="33-cloud-dashboard">3.3 Cloud Dashboard</h2>
<p>저는 로컬 Report 보다 Cloud Dashboard 를 어떻게 이용하는 것일까가 더 궁금했는데요! (Cloud Dashboard의 통계가 더 이해하기 쉽게 느껴졌습니다)</p>
<p>아래 순서대로 진행하면 됩니다.</p>
<br>

<h3 id="api-key-발급">API Key 발급</h3>
<ul>
<li><a href="https://app.artillery.io/">https://app.artillery.io/</a> 에 접속합니다.</li>
<li>Dashboard에 로그인합니다.</li>
<li>Settings 메뉴 → API Keys 클릭한뒤, 새로운 API key를 만들어줍니다.</li>
</ul>
<br>

<h3 id="run-시-key-추가">Run 시 key 추가</h3>
<p>대시보드에 테스트 수행을 기록하려면 run 명령어 수행시 --record 옵션과 --key 옵션을 사용해야합니다.</p>
<p>아래는 --record 옵션과 --key 옵션을 사용한 예제입니다.</p>
<pre><code class="language-cmd">&gt; artillery run my-service-test.yml --record --key 0x12345_my_api_key</code></pre>
<br>

<h3 id="결과-추적">결과 추적</h3>
<p>대시보드의 메인화면에서 테스트 목록을 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/44a82565-f43e-4e5a-b7cd-16045c385339/image.png" alt=""></p>
<p>하나의 테스트를 클릭하면 상세 결과를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/developer_khj/post/ea098fb2-a095-472d-9222-4bd74cf50301/image.png" alt=""></p>
<br>

<h2 id="34-결과-분석">3.4 결과 분석</h2>
<h3 id="그래픽-보는-법">그래픽 보는 법</h3>
<ul>
<li>max: 가장 오래 걸린 요청 -&gt; 응답 시간</li>
<li>p95: 전체 HTTP 트랜잭션 중 가장 빠른 것부터 95%까지 (대부분의 트래픽)</li>
<li>p50: 전체 HTTP 트랜잭션 중 가장 빠른 것부터 50%까지 (절반의 트래픽)</li>
<li>min: 가장 빠르게 온 요청 -&gt; 응답 시간</li>
<li>p99: 이외에도 많이 사용하는데, 거의 모든 트래픽을 의미하기 때문</li>
</ul>
<br>

<h3 id="tps-측정">TPS 측정</h3>
<p><strong>TPS(Transaction Per Second)</strong>는 <strong>arrivalRate</strong>로 측정할 수 있습니다.</p>
<p>대부분의 경우 스트레스 테스트는 목표로 하는 TPS가 고정되어 있고 그 TPS를 맞춰야 하기 때문에 TPS를 고정 시킨 상태에서 코드나 인프라를 수정하면서 목표로 하는 TPS가 안정적인지 보면 됩니다.</p>
<hr>
<h1 id="4-concepts">4. Concepts</h1>
<h2 id="41-scripts">4.1 Scripts</h2>
<p>Artillery의 테스트 스크립트는 <strong>YAML 파일</strong>로, 두가지 주요 section이 있습니다.</p>
<ul>
<li>config: URI, Protocol 등 테스트를 위한 <strong>실행 환경</strong> 설정</li>
<li>senarios: <strong>가상유저(VU, Virutal User)</strong>의 <strong>행동(behavior)</strong>을 정의</li>
</ul>
<br>

<h3 id="config">config</h3>
<h4 id="target">target</h4>
<p>테스트할 <strong>엔드포인트(endpoint)</strong> 지정합니다.</p>
<pre><code class="language-yml">config:
  target: &#39;http://localhost:8080&#39;</code></pre>
<br>

<h4 id="phases">phases</h4>
<p>phases는 Artillery가 구체적으로 <strong>가상 유저(VU, 이하 VU)를 어떻게 생성할 지에 대해 정의</strong>합니다.</p>
<p>phases는 리스트로 여러가지 단계(phase)가 포함 될 수 있습니다.</p>
<pre><code class="language-yml">phases:
  - name: ramp up
      duration: 30m
    arrivalRate: 1
    rampTo: 100

  - name: pause
      duration: 1h

  - name: sustain
      duration: 3h
    arrivalRate: 100</code></pre>
<br>

<blockquote>
<p>name 옵션</p>
</blockquote>
<p>CLI 로그 및 Artillery Cloud 대시보드에서 더 쉽게 식별할 수 있도록 단계에 이름을 지정할 수 있습니다.</p>
<pre><code class="language-bash">&gt; artillery run artillery.yml

# 이름 지정시
Phase completed: test (index: 0, duration: 10s) 12:05:34(+0900)

# 이름 미지정시
Phase started: unnamed (index: 0, duration: 10s) 12:05:51(+0900)</code></pre>
<br>

<blockquote>
<p>duration 옵션</p>
</blockquote>
<p>초(s)단위의 단계 시간 (VU가 생성되는 도착 시간)을 의미합니다.</p>
<blockquote>
<p>arrivalRate 옵션</p>
</blockquote>
<p>초당 생성할 VU를 정의합니다. 아래 예시에서는 5분(300초) 동안 매초 50명의 VU를 생성합니다.</p>
<pre><code class="language-yml">phases:
  - duration: 300
    arrivalRate: 50</code></pre>
<blockquote>
<p>arrivalCount 옵션</p>
</blockquote>
<p>한 단계에 VU를 지정할 수 있습니다. 아래 예시에서는 60초 안에 20명의 가상 사용자를 만듭니다. (대략 3초마다 한 명의 가상 사용자)</p>
<pre><code class="language-yml">phases:
  - duration: 60
    arrivalCount: 20</code></pre>
<blockquote>
<p>maxUsers 옵션</p>
</blockquote>
<p>최대 VU를 지정할 수 있습니다. 아래 예시에서는 5분 동안 매초 10명의 VU를 생성하며, 특정 시간에 최대 50명의 동시 VU를 생성합니다.</p>
<pre><code class="language-yml">phases:
  - duration: 300
    arrivalRate: 10
    maxVusers: 50</code></pre>
<blockquote>
<p>rampTo 옵션</p>
</blockquote>
<p>duration 동안 VU를 rampTo 옵션 만큼 늘립니다. 아래 예시에서는 2분 동안 가상 사용자의 도착률을 10명에서 50명으로 늘립니다.</p>
<pre><code class="language-yml">phases:
  - duration: 120
    arrivalRate: 10
    rampTo: 50</code></pre>
<blockquote>
<p>pause 옵션</p>
</blockquote>
<p>아무것도 하지 않는 단계를 구성할 수 있습니다.</p>
<pre><code class="language-yml">config:
  target: &#39;https://staging.example.com&#39;
  phases:
    - pause: 60</code></pre>
<blockquote>
<p>tls 옵션</p>
</blockquote>
<p>Artillery로 https 연결을 하게 되면 <strong>자체 서명된 인증서</strong>에 한해 아래와 같은 에러가 발생할 수 있습니다.
⇒ errors.UNABLE_TO_VERIFY_LEAF_SIGNATURE</p>
<p>Postman으로 해당 URL을 날려보면 아래와 같은 에러를 볼 수 있죠
⇒ SSL Error: Unable to verify the first certificate</p>
<pre><code class="language-yml">config:
  tls:
    rejectUnauthorized: false</code></pre>
<p>이 때 TLS 옵션으로 Artillery가 자체 서명된 TLS 인증서를 수락하도록 지시할 수 있습니다. (보안이 중요하다면 권장하지는 않습니다.)</p>
<br>

<h4 id="environments">environments</h4>
<p>하나의 테스트 스크립트에 다양한 환경을 정의할 수 있습니다.</p>
<p>예를 들어 로컬, 테스트서버, 프로덕션에서 동일한 성능 테스트를 실행하고자 할때, 각 환경에 대해 테스트 정의 파일을 복제하는 대신 <code>config.environments</code> 설정을 통해 환경 별로 구성을 정의할 수 있습니다.</p>
<pre><code class="language-yml">config:
  target: &#39;http://service1.acme.corp:3003&#39;
  phases:
    - duration: 10
      arrivalRate: 1
  environments:
    production:
      target: &#39;http://service1.prod.acme.corp:44321&#39;
      phases:
        - duration: 1200
          arrivalRate: 10
    local:
      target: &#39;http://127.0.0.1:3003&#39;
      phases:
        - duration: 1200
          arrivalRate: 20</code></pre>
<p>테스트를 실행할 때 <code>-e</code> 옵션을 사용하여 명령줄에서 환경을 지정할 수 있습니다.</p>
<pre><code class="language-bash">&gt; artillery run my-script.yml
&gt; artillery run -e local my-script.yml</code></pre>
<br>

<p>특정 환경에서 테스트를 실행할 때 <code>$environment</code> 변수를 사용하여 현재 환경의 이름에 액세스할 수 있습니다.</p>
<pre><code class="language-yml">scenarios:
  - flow:
      - log: &#39;Current environment is set to: {{ $environment }}&#39;</code></pre>
<br>

<h3 id="scenarios">scenarios</h3>
<p>scenarios 섹션에서는 Artillery가 생성할 가상 사용자(VU)에 대한 하나 이상의 시나리오에 대한 정의가 포함되어 있습니다.</p>
<p>각 시나리오는 애플리케이션 사용자가 보낸 일반적인 요청 또는 메시지 순서를 나타내는 일련의 단계입니다.</p>
<br>

<h4 id="시나리오별-옵션">시나리오별 옵션</h4>
<blockquote>
<p>flow (필수)</p>
</blockquote>
<p>가상 사용자가 수행하는 작업 배열입니다. HTTP 기반의 애플리케이션의 경우 GET 및 HTTP 요청을 실행하거나 Socket.io 테스트에 대한 이벤트를 내보낼 수 있습니다.</p>
<blockquote>
<p>name (선택)</p>
</blockquote>
<p>보고에 도움이 되는 설명이 포함된 이름을 시나리오에 할당합니다.</p>
<blockquote>
<p>weight (선택)</p>
</blockquote>
<p>새로운 VU가 시나리오를 선택할 확률을 다른 시나리오와 비교하여 가중치를 설정할 수 있습니다. 가중치가 높을 경우 높은 확률로 해당 시나리오가 선택됩니다.</p>
<br>

<h4 id="before-after">before, after</h4>
<p>시나리오가 시작되기 전에 사용할 수 있습니다.</p>
<p>다음 예시에서는 before 섹션에서 인증 요청을 호출하고 가상 사용자가 도착하기 전에 인증 토큰을 캡처합니다. 시나리오가 실행된 후 after섹션에서는 토큰을 무효화합니다.</p>
<pre><code class="language-yml">config:
  target: &#39;http://app01.local.dev&#39;
  phases:
    - duration: 300
      arrivalRate: 25

before:
  flow:
    - log: &#39;Get auth token&#39;
    - post:
        url: &#39;/auth&#39;
        json:
          username: &#39;myUsername&#39;
          password: &#39;myPassword&#39;
        capture:
          - json: $.id_token
            as: token
scenarios:
  - flow:
      - get:
          url: &#39;/data&#39;
          headers:
            authorization: &#39;Bearer {{ token }}&#39;
after:
  flow:
    - log: &#39;Invalidate token&#39;
    - post:
        url: &#39;/logout&#39;
        json:
          token: &#39;{{ token }}&#39;</code></pre>
<br>

<h2 id="42-commands">4.2 Commands</h2>
<blockquote>
<p>이 포스트에서는 기본적인 Command인 <strong>run</strong>과 <strong>quick</strong>에 대해서만 다룹니다</p>
</blockquote>
<p>기본적인 Command</p>
<ul>
<li><code>run</code> - Artillery 테스트 스크립트를 실행합니다.</li>
<li><code>quick</code> - 테스트 스크립트 없이 하나의 HTTP 엔드포인트를 테스트합니다.</li>
</ul>
<p>AWS 관련 Command</p>
<ul>
<li><code>run-lambda</code> - AWS Lamda에서 분산 테스트를 수행합니다.</li>
<li><code>run-fargate</code> - AWS Fargate에서 분산 테스트를 수행합니다.</li>
</ul>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://www.artillery.io/docs">Artillery 공식 Docs</a></li>
<li><a href="https://velog.io/@kwaktaemin_/Artillery-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">Artillery 부하 테스트 사용해보기 - 곽태민 블로그</a></li>
<li><a href="https://yoonsoo-space.tistory.com/97">artillery를 이용한 서버 부하 테스트 - YS 개발 블로그</a></li>
<li><a href="https://hyerin6.github.io/2021-11-05/stress-test/">어떤 부분을 테스트하고 분석해야 할까?</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>Artillery를 사용해보면서 아직 추가 보완해야될 내용이 있지만, 생각보다 사용하기 쉽다고 느껴졌습니다!🤗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트] 2016년 a월 b일 요일 구하기]]></title>
            <link>https://velog.io/@developer_khj/coding-test-python-get-day</link>
            <guid>https://velog.io/@developer_khj/coding-test-python-get-day</guid>
            <pubDate>Tue, 04 Jun 2024 12:50:33 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 요일 구하는 문제를 풀어볼까합니다~!
(사실 한달이 며칠까지 있는지 매번 까먹어서 기록용으로 포스팅합니다🤣)</p>
<br>

<h1 id="1-한달세기">1. 한달세기</h1>
<h2 id="날짜-구하기">날짜 구하기</h2>
<p>한달은 며칠까지 있을까?</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/bc0d3773-b817-4b15-abe7-49cb60c70eec/image.png" alt=""></p>
<p>사진 출저 - <a href="https://news.sbs.co.kr/news/endPage.do?news_id=N1005867324">스브스뉴스</a></p>
<table>
<thead>
<tr>
<th>1월</th>
<th>2월</th>
<th>3월</th>
<th>4월</th>
<th>5월</th>
<th>6월</th>
<th>7월</th>
<th>8월</th>
<th>9월</th>
<th>10월</th>
<th>11월</th>
<th>12월</th>
</tr>
</thead>
<tbody><tr>
<td>31일</td>
<td>28일<br>(윤년 29일)</td>
<td>31일</td>
<td>30일</td>
<td>31일</td>
<td>30일</td>
<td>31일</td>
<td>31일</td>
<td>30일</td>
<td>31일</td>
<td>30일</td>
<td>31일</td>
</tr>
</tbody></table>
<p>2월을 제외한 모든 달은 30일 또는 31일을 갖습니다.</p>
<br>

<h2 id="윤년-구하기">윤년 구하기</h2>
<p>윤년에는 법칙이 있습니다.</p>
<ul>
<li>윤년은 4년마다 돌아옵니다. (2016년, 2020년, 2024년 등등)</li>
<li>윤년의 의미는 1년이 366일까지 있다는 뜻이며, 그 중에서도 2월이 29일까지 있습니다. (1900년, 2100년, 2200년)</li>
<li>100으로 나누어 떨어지면 평년인데 400으로 나누어 떨어지면 윤년입니다. (2000년, 2400년)</li>
</ul>
<br>

<p>파이썬 코드로 구현하면 다음과 같습니다.</p>
<pre><code class="language-python">def get_day(year):
    if year % 100 == 0 and year % 400 != 0:
        return 28
    elif year % 4 == 0:
        return 29
    return 0</code></pre>
<br>

<h1 id="2-문제">2. 문제</h1>
<h2 id="21-2016년">2.1 2016년</h2>
<blockquote>
<h3 id="문제-설명">문제 설명</h3>
<p>2016년 1월 1일은 금요일입니다. 2016년 a월 b일은 무슨 요일일까요? 두 수 a ,b를 입력받아 2016년 a월 b일이 무슨 요일인지 리턴하는 함수, solution을 완성하세요.</p>
<p>요일의 이름은 일요일부터 토요일까지 각각 SUN,MON,TUE,WED,THU,FRI,SAT입니다. 예를 들어 a=5, b=24라면 5월 24일은 화요일이므로 문자열 &quot;TUE&quot;를 반환하세요.</p>
<h3 id="제한-조건">제한 조건</h3>
<ul>
<li>2016년은 윤년입니다.</li>
<li>2016년 a월 b일은 실제로 있는 날입니다. (13월 26일이나 2월 45일같은 날짜는 주어지지 않습니다)</li>
</ul>
</blockquote>
<h2 id="22-코드">2.2 코드</h2>
<pre><code class="language-python">def solution(a, b):
    day_list = [&#39;SUN&#39;,&#39;MON&#39;,&#39;TUE&#39;,&#39;WED&#39;,&#39;THU&#39;,&#39;FRI&#39;,&#39;SAT&#39;]
    day = 5

    month_list = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    for i in range(1, a):
        day += month_list[i-1]
    day += b - 1

    return day_list[day % 7]</code></pre>
<ul>
<li>day: 우선, 1월 1일이 금요일이라고 했으니, 시작 값 5로 시작합니다.</li>
<li>month 별로 날짜를 더해줍니다</li>
<li>마지막 날짜를 더해줍니다. 여기서 1월 값인 <code>month_list[0]</code>에 1월 1일이 포함되어 있으므로 1을 빼줍니다.</li>
<li>모듈러 연산을 통해 요일을 구해줍니다.</li>
</ul>
<br>

<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://m.blog.naver.com/design_woo_/221343774239">2월은 왜 짧을까?</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트] 소인수 분해]]></title>
            <link>https://velog.io/@developer_khj/Coding-Test-prime-factorization</link>
            <guid>https://velog.io/@developer_khj/Coding-Test-prime-factorization</guid>
            <pubDate>Tue, 04 Jun 2024 07:04:02 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가며">들어가며</h1>
<p>이번 포스팅에서는 소인수 분해에 대해 다뤄볼까 합니다. 😉</p>
<p>소인수 분해는 중학생 때 배울 정도로 쉬운 개념이지만, 알고리즘으르 짜는 것(코딩으로 구현하는 것)은 다른 영역이기 때문에 포스팅을 통해 남겨두고자 합니다.</p>
<br>


<h1 id="1-소인수-분해">1. 소인수 분해</h1>
<h2 id="소인수-분해">소인수 분해</h2>
<blockquote>
<p>소인수 분해 (Prime Factorization)란</p>
</blockquote>
<p>소인수 분해란 어떠한 숫자를 1보다 큰 자연수인 소인수(소수인 인수)들만의 곱으로 표현하는 것을 말합니다.</p>
<br>

<h2 id="소인수">소인수</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/385655ee-4946-4c39-88f6-8a62c858cfa7/image.png" alt="소인수란?"></p>
<p>인수 : 어떤수를 곱하기로 표현했을 때 곱해지는 각각의 수들
소인수 : 인수 중에서 소수인 것
 </p>
<ul>
<li>6의 인수 : 1, 2, 3, 6</li>
<li>6의 소인수 : 2, 3</li>
</ul>
<br>

<h2 id="소수">소수</h2>
<p>여기서 소수는 1과 자기 자신 만의 곱으로만 이루어진 수입니다. 다르게 말하면 1과 자기 자신 외에 약수를 갖지 않습니다.</p>
<p>예를 들어, 2는 <code>1 * 2</code>의 곱으로 표현할 수 있습니다</p>
<ul>
<li><code>5 = 1 * 5</code></li>
<li><code>7 = 1 * 7</code></li>
<li><code>17 = 1 * 17</code></li>
</ul>
<p>숫자 12의 약수는 {1, 2, 3, 4, 6, 12}으로 정리되며, <code>2 * 6</code>, <code>3 * 4</code> 등으로 표현될 수 있어 소수가 아닙니다</p>
<br>

<h2 id="소인수-분해하는-방법">소인수 분해하는 방법</h2>
<p>소인수분해를 하는 가장 간단한 방법은 다음과 같습니다.</p>
<ol>
<li>해당 합성수를 소인수로 몫이 소수가 될 때까지 나눗셈을 계속합니다.</li>
<li>사용된 모든 소인수와 마지막 몫을 지수의 형태로 곱합니다.</li>
</ol>
<p>예제) 72를 소인수분해하시오.
<img src="https://velog.velcdn.com/images/developer_khj/post/e8550b30-9e95-4632-90a4-fea0fd11b83b/image.png" alt="72 소인수 분해"></p>
<h2 id="코드">코드</h2>
<p>소인수는 2부터 시작하여 2로 나누지 못할 경우 2에서 +1를 해주면서 나누어지는지 체크하면 됩니다.</p>
<p>왜 2부터 시작하느냐? 하면 아래 코드를 통해 알 수 있습니다.</p>
<pre><code class="language-python">def factorization(n):
    result = []
    i = 2
    for i &lt;= n:
        if n % i == 0:
            n //= i
            # 인수 목록만 구할 경우
            # if i in result:
            result.append(i)
        else:
            i += 1
    return result</code></pre>
<br>

<h1 id="2-실전-문제">2. 실전 문제</h1>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/120852">https://school.programmers.co.kr/learn/courses/30/lessons/120852</a></p>
<br>


<h1 id="references">References</h1>
<ul>
<li><a href="https://jwj4519.com/entry/%EC%86%8C%EC%9D%B8%EC%88%98%EB%B6%84%ED%95%B4-%ED%95%98%EB%8A%94-%EB%B2%95-%EC%9D%B8%EC%88%98%EC%99%80-%EC%86%8C%EC%9D%B8%EC%88%98%EC%9D%98-%EB%9C%BB-%EC%A4%91-1-%EC%88%98%ED%95%99">소인수분해 하는 법, 인수와 소인수의 뜻</a></li>
<li><a href="https://www.tcpschool.com/codingmath/odd">소수와 소인수분해</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트] Python 문자열 - 정규식]]></title>
            <link>https://velog.io/@developer_khj/Coding-Test-Python-String-RE</link>
            <guid>https://velog.io/@developer_khj/Coding-Test-Python-String-RE</guid>
            <pubDate>Wed, 29 May 2024 14:04:12 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>정규식은 모든 프로그래밍에서 어려운 영역 중에 하나로 손 꼽히는데요, 문자열 관련 문제를 풀다가 정규 표현식을 이용하여 깔끔하게 푼 문제가 있어 이번 기회에 정리해볼까 합니다.</p>
<p>간략하게 포스팅 내용을 정리해보자면 다음과 같습니다.</p>
<ul>
<li>re 모듈 이용</li>
<li>메타 문자: <code>. ^ $ * + ? { } [ ] \ | ( )</code></li>
<li>컴파일: re.compile(&quot;expression&quot;)</li>
<li>검색 메소드: match, search, findall, finditer</li>
<li>pattern 객체 메소드: group, start, end, span</li>
<li>옵션: re.DOTALL, re.IGNORECASE, re.MULTILINE, re.VERBOSE</li>
</ul>
<hr>
<h1 id="1-정규식">1. 정규식</h1>
<blockquote>
<p><strong>정규 표현식(regular expressions)</strong> 은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용하는 일종의 형식 언어이다.</p>
</blockquote>
<p>정규 표현식은 우리가 흔히 알고 있는 <strong>정규식</strong>으로도 불립니다. 정규식은 파이썬 뿐만 아니라 많은 영역에서 사용되는데, 주된 목적은 정규표현식의 패턴을 이용하여 <strong>해당하는 문자열을 골라내고 치환하는 역할</strong>을 합니다.</p>
<hr>
<h2 id="11-메타-문자">1.1 메타 문자</h2>
<p>메타 문자는 원래 그 문자가 가진 뜻이 아니라 <strong>특별한 의미를 가진 문자</strong>를 의미합니다. 정규 표현식에서 다음과 같은 메타 문자를 사용하면 특별한 의미를 갖게 됩니다.</p>
<pre><code class="language-python">. ^ $ * + ? { } [ ] \ | ( )</code></pre>
<br>

<h3 id="--문자---문자-클래스">[ ] 문자 - 문자 클래스</h3>
<p>문자 클래스로 만들어진 정규식은 &#39;[&#39;와 &#39;]&#39; 사이의 문자들의 매치라는 의미를 가집니다. 예를 들어, 정규 표현식이 <code>[abc]</code>라면, 이 표현식의 의미는 &#39;a, b, c 중 한 개의 문자와 매치&#39;를 뜻합니다.</p>
<br>

<h4 id="하이픈-">하이픈(-)</h4>
<p>[] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위를 의미합니다.</p>
<ul>
<li><code>[a-zA-Z]</code>: 모든 알파벳</li>
<li><code>[0-9]</code>: 모든 숫자</li>
</ul>
<br>

<h4 id="">[^]</h4>
<p>문자 클래스 안에 어떤 문자나 메타 문자도 사용할 수 있지만, 주의해야할 문자가 1가지 있습니다. 그것은 바로 ^인데, 문자 클래스 안에 ^ 메타문자를 사용할 경우에는 반대(not)라는 의미를 갖습니다.</p>
<p>예를 들어, <code>[^0-9]</code>라는 정규 표현식은 숫자가 아닌 문자만 매치됩니다.</p>
<br>

<h4 id="자주-사용되는-문자-클래스">자주 사용되는 문자 클래스</h4>
<blockquote>
<p><code>[0-9]</code> 또는 <code>[a-zA-Z]</code> 등은 무척 자주 사용하는 정규 표현식이다. 이렇게 자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다. 다음을 기억해 두자.</p>
<ul>
<li><code>\d</code> - <strong>숫자</strong>와 매치된다. <code>[0-9]</code>와 동일한 표현식이다.</li>
<li><code>\D</code> - <strong>숫자가 아닌 것</strong>과 매치된다. <code>[^0-9]</code>와 동일한 표현식이다.</li>
<li><code>\s</code> - <strong>화이트스페이스(whitespace)</strong> 문자와 매치된다. <code>[\t\n\r\f\v]</code>와 동일한 표현식이다. 맨 앞의 빈칸은 공백 문자(space)를 의미한다.</li>
<li><code>\S</code> - <strong>화이트스페이스 문자가 아닌 것</strong>과 매치된다. <code>[^ \t\n\r\f\v]</code>와 동일한 표현식이다.</li>
<li><code>\w</code> - <strong>문자+숫자(alphanumeric)</strong>와 매치된다. <code>[a-zA-Z0-9_]</code>와 동일한 표현식이다.</li>
<li><code>\W</code> - <strong>문자+숫자(alphanumeric)가 아닌 문자</strong>와 매치된다. <code>[^a-zA-Z0-9_]</code>와 동일한 표현식이다.</li>
</ul>
</blockquote>
<br>

<h3 id="-문자---그룹핑">() 문자 - 그룹핑</h3>
<p>특정 문자열이 계속해서 반복되는지 조사하는 정규식을 작성하고 싶을 때 사용할 수 있는 것이 바로 <strong>그룹핑(grouping)</strong> 입니다.</p>
<pre><code class="language-python">(ABC)+</code></pre>
<p>그룹을 만들어 주는 메타 문자가 바로 <code>()</code> 입니다.</p>
<pre><code class="language-python">p = re.compile(&#39;(ABC)+&#39;)
m = p.search(&#39;ABCABCABC OK?&#39;)
print(m)
&lt;re.Match object; span=(0, 9), match=&#39;ABCABCABC&#39;&gt;

print(m.group())
ABCABCABC</code></pre>
<br>

<h4 id="예시---전화번호">예시 - 전화번호</h4>
<p>다음 예시로는 <code>이름 + &quot; &quot; + 전화번호</code> 형태의 문자열을 찾는 정규식을 작성해보면, 다음과 같이 작성할 수 있습니다.</p>
<pre><code class="language-python">p = re.compile(&quot;(\w+)\s+(\d+[-]\d+[-]\d+)&quot;)
m = p.search(&quot;park 010-1234-1234&quot;)

print(m.group(0))
# 출력: park 010-1234-1234

print(m.group(1))
# 출력: park

print(m.group(2))
# 출력: 010-1234-1234</code></pre>
<br>

<h4 id="예시---이름-지정">예시 - 이름 지정</h4>
<p>아래 예시 처럼 그룹에 이름을 지정해 줄 수도 있습니다.</p>
<pre><code>(?P&lt;그룹명&gt;...)
(\w+) → (?P&lt;name&gt;\w+)

p = re.compile(r&quot;(?P&lt;name&gt;\w+)\s+(?P&lt;phone&gt;\d+[-]\d+[-]\d+)&quot;)
m = p.search(&quot;park 010-1234-1234&quot;)

print(m.group(&quot;name&quot;))
print(m.group(&quot;phone&quot;))
</code></pre><br>

<h3 id="dot-문자---n을-제외한-모든-문자">.(dot) 문자 - <code>\n</code>을 제외한 모든 문자</h3>
<p><code>.(dot)</code> 메타 문자는 줄바꿈 문자인 <code>\n</code>을 제외한 모든 문자와 매치됩니다.</p>
<blockquote>
<p>정규식을 작성할 때 re.DOTALL 옵션을 주면 <code>.(dot)</code> 문자와 <code>\n</code> 문자도 매치된다.</p>
</blockquote>
<pre><code class="language-python">import re
p = re.compile(&quot;a.b&quot;)</code></pre>
<p>해석은 &quot;a + 모든 문자 + b&quot;입니다. 즉, a와 b 사이에 어떤 문자가 들어가도 모두 매치된다는 의미입니다.</p>
<br>

<h3 id="-문자---반복-0--">* 문자 - 반복 (0, -)</h3>
<pre><code class="language-python">import re
p = re.compile(&quot;ca*t&quot;)</code></pre>
<p><code>*</code> 바로 앞에 있는 문자 a가 0부터 무한대까지 반복될 수 있다는 의미입니다.</p>
<ul>
<li><code>ct</code> - Yes</li>
<li><code>cat</code> - Yes</li>
<li><code>caat</code> - Yes</li>
</ul>
<br>

<h3 id="-문자---반복-1--">+ 문자 - 반복 (1, -)</h3>
<p>반복을 나타내는 또 다른 메타 문자로 <code>+</code>가 있는데, 최소 1번 이상 반복될 때 사용합니다.</p>
<pre><code class="language-python">import re
p = re.compile(&quot;ca+t&quot;)</code></pre>
<ul>
<li><code>ct</code> - No</li>
<li><code>cat</code> - Yes</li>
<li><code>caat</code> - Yes</li>
</ul>
<br>


<h3 id="-문자---횟수-지정-반복">{} 문자 - 횟수 지정 반복</h3>
<p><code>{}</code> 메타 문자를 사용하면 반복 횟수를 지정할 수 있습니다.</p>
<pre><code class="language-python">import re
p = re.compile(&quot;ca{2}t&quot;)</code></pre>
<p>이 정규 표현식의 의미는 &#39;c + a를 반드시 2번 반복 + t&#39; 으로 caat가 매치됩니다.</p>
<p>범위를 지정하고 싶을 때는 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-python">import re

# 2-5회 반복
p = re.compile(&quot;ca{2, 5}t&quot;)

# 3회 이하 반복
p = re.compile(&quot;ca{,3}t&quot;)

# 3회 이상 반복
p = re.compile(&quot;ca{3,}t&quot;)</code></pre>
<br>


<h3 id="-문자---유사-반복">? 문자 - 유사 반복</h3>
<p>반복은 아니지만 그와 유사한 기능을 하는 <code>?</code> 메타 문자도 있습니다. <code>?</code> 메타 문자가 의미하는 것은 <code>{0, 1}</code>입니다.</p>
<pre><code class="language-python">import re
p = re.compile(&quot;ab?c&quot;)</code></pre>
<p>a와 c 사이에 b가 있어도 되고 없어도 되는 경우입니다.</p>
<br>

<h3 id="-메타-문자---or">| 메타 문자 - or</h3>
<p><code>|</code> 메타 문자는 or과 동일한 의미로 사용됩니다. <code>A|B</code>라는 정규식이 있다면 A또는 B라는 의미가 됩니다.</p>
<pre><code class="language-python">import re
p = re.compile(&#39;Crow|Servo&#39;)
m = p.match(&#39;CrowHello&#39;)
print(m)

# 결과
&lt;re.Match object; span=(0, 4), match=&#39;Crow&#39;&gt;</code></pre>
<br>

<h3 id="-메타-문자---문자열-맨-처음">^ 메타 문자 - 문자열 맨 처음</h3>
<p><code>^</code> 메타 문자는 문자열로 시작하는 것을 의미합니다. <code>str.startsWith 메소드</code>와 유사합니다.</p>
<pre><code class="language-python">&gt;&gt;&gt; re.search(&#39;^Life&#39;, &#39;Life is too short&#39;)
&lt;re.Match object; span=(0, 4), match=&#39;Life&#39;&gt;

&gt;&gt;&gt; re.search(&#39;^Life&#39;, &#39;My Life&#39;)
None</code></pre>
<br>

<h3 id="-메타-문자---문자열-마지막">$ 메타 문자 - 문자열 마지막</h3>
<p><code>$</code> 메타 문자는 <code>^</code> 메타 문자와 반대로 문자열로 끝나는 것을 의미합니다. <code>str.endsWith 메소드</code>와 유사합니다.</p>
<pre><code class="language-python">&gt;&gt;&gt; re.search(&#39;short$&#39;, &#39;Life is too short&#39;)
&lt;re.Match object; span=(12, 17), match=&#39;short&#39;&gt;

&gt;&gt;&gt; re.search(&#39;short$&#39;, &#39;Life is too short, you need python&#39;)
None</code></pre>
<hr>
<h2 id="12-re-모듈">1.2 re 모듈</h2>
<p>파이썬에서는 정규 표현식을 지원하기 위해 <strong>re(regular expression) 모듈</strong>을 제공합니다. re 모듈은 파이썬을 설치할 때 자동으로 설치되는 <strong>표준 라이브러리</strong>입니다.</p>
<p>다음과 같이 사용할 수 있습니다.</p>
<pre><code class="language-python">import re
p = re.compile(&#39;[a-z]+&#39;)
m = p.match(&#39;python&#39;)
if m:
    print(&#39;Match found: &#39;, m.group())
else:
    print(&#39;No match&#39;)</code></pre>
<ol>
<li>re.compile 함수를 이용하여 정규 표현식을 컴파일합니다.</li>
<li>re.compile의 리턴 값을 객체 p(컴파일된 패턴 객체)에 할당해 그 이후의 작업을 수행합니다.</li>
</ol>
<hr>
<h2 id="13-문자열-검색">1.3 문자열 검색</h2>
<p><code>re.compile</code> 메소드를 통해 컴파일된 패턴 객체를 사용하여 문자열을 검색할 수 있습니다. 패턴 객체는 다음과 같은 4가지 메소드를 제공합니다.</p>
<ul>
<li>match() - 문자열의 처음부터 정규식과 매치되는지 조사</li>
<li>search() - 문자열 전체를 검색하여 정규식과 매치되는지 조사</li>
<li>findall() - 정규식과 매치되는 모든 문자열을 리스트로 리턴</li>
<li>finditer() - 정규식과 매치되는 모든 문자열을 반복 가능한 객체로 리턴</li>
</ul>
<br>

<h3 id="match">match()</h3>
<p>match() 메소드는 문자열의 처음부터 정규식과 매치되는지 조사합니다.</p>
<pre><code class="language-python">p = re.compile(&quot;[a-z]+&quot;)

m = p.match(&quot;python&quot;)
# 출력: &lt;re.Match object; span=(0, 6), match=&#39;python&#39;&gt;

m = p.match(&quot;3 python&quot;)
# 출력: None</code></pre>
<p><code>python</code> 문자열은 <code>[a-z]+</code> 정규식에 부합하므로 match 객체가 리턴되지만 <code>3 python</code> 문자열은 처음에 나오는 문자 3이 정규식 <code>[a-z]+</code>에 부합하지 않으므로 None이 리턴됩니다.</p>
<br>

<h3 id="search">search()</h3>
<p>search() 메소드는 match() 메소드와 동일하지만 전체 문자열을 검색합니다.</p>
<pre><code class="language-python">p = re.compile(&quot;[a-z]+&quot;)

m = p.search(&quot;python&quot;)
# 출력: &lt;re.Match object; span=(0, 6), match=&#39;python&#39;&gt;

m = p.search(&quot;3 python&quot;)
# 출력: &lt;re.Match object; span=(2, 8), match=&#39;python&#39;&gt;</code></pre>
<p>이렇듯 match 메서드와 search 메서드는 문자열의 처음부터 검색할지의 여부에 따라 다르게 사용해야 합니다.</p>
<br>

<h3 id="findall">findall()</h3>
<p>findall은 패턴과 매치되는 모든 값을 찾아 리스트로 리턴합니다.</p>
<pre><code class="language-python">p = re.complie(&#39;[a-z]+&#39;)
result = p.findall(&#39;life is too short&#39;)
# 출력: [&#39;life&#39;, &#39;is&#39;, &#39;too&#39;, &#39;short&#39;]</code></pre>
<br>

<h3 id="finditer">finditer()</h3>
<p>finditer 메소드는 findall과 동일하지만, 그 결과를 반복 가능한 객체로 리턴합니다. 반복 가능한 객체는 match 객체입니다.</p>
<pre><code class="language-python">&gt;&gt;&gt; result = p.finditer(&quot;life is too short&quot;)
&gt;&gt;&gt; print(result)
&lt;callable_iterator object at 0x01F5E390&gt;

&gt;&gt;&gt; for r in result: print(r)
&lt;re.Match object; span=(0, 4), match=&#39;life&#39;&gt;
&lt;re.Match object; span=(5, 7), match=&#39;is&#39;&gt;
&lt;re.Match object; span=(8, 11), match=&#39;too&#39;&gt;
&lt;re.Match object; span=(12, 17), match=&#39;short&#39;&gt;</code></pre>
<hr>
<h2 id="14-match-객체">1.4 match 객체</h2>
<p><strong>match 객체</strong>란 앞서 살펴본 <code>p.match</code>, <code>p.search</code>, <code>p.finditer</code> 메소드에 의해 리턴된 매치 객체를 의미합니다.</p>
<p>match 객체에서 사용할 수 있는 메소드는 다음과 같습니다.</p>
<ul>
<li>group(): 매치된 문자열 리턴</li>
<li>start(): 매치된 문자열의 시작 위치 리턴</li>
<li>end(): 매치된 문자열의 끝 위치 리턴</li>
<li>span(): 매치된 문자열의 위치(시작, 끝)에 해당하는 튜플 리턴</li>
</ul>
<pre><code class="language-python">import re
p = re.compile(&#39;[a-z]+&#39;)
m = p.match(&#39;python&#39;)

&gt;&gt;&gt; print(m)
&lt;re.Match object; span=(0, 6), match=&#39;python&#39;&gt;

&gt;&gt;&gt; print(m.group())
&#39;python&#39;

&gt;&gt;&gt; print(m.start())
0

&gt;&gt;&gt; print(m.end())
6

&gt;&gt;&gt; print(m.span())
(0, 6)</code></pre>
<br>

<hr>
<h2 id="15-컴파일-옵션">1.5 컴파일 옵션</h2>
<p>정규식을 컴파일할 때 다음 옵션을 사용할 수 있습니다.</p>
<ul>
<li>DOTALL(S) - <code>.</code>(dot)이 줄바꿈 문자를 포함해 모든 문자와 매치될 수 있게 한다</li>
<li>IGNORECASE(I) - 대소문자에 관계없이 매치될 수 있게 한다</li>
<li>MULTILINE(M) - 여러 줄과 매치될 수 있게 한다. <code>^</code>, <code>$</code> 메타 문자와 관련 있는 옵션</li>
<li>VERBOSE(X) - verbose 모드를 사용할 수 있게 한다. 정규 표현식을 보기 편하게 만들 수 있고 주석 등을 사용할 수 있게 된다.</li>
</ul>
<p>옵션을 사용할 때는 <code>re.DOTALL</code> 처럼 전체 옵션 이름을 써도 되고 <code>re.S</code>로 사용해도 됩니다.</p>
<br>

<h3 id="dotall-s">DOTALL, S</h3>
<p><code>.</code> 메타 문자는 줄바꿈 문자(<code>\n</code>)을 제외한 모든 문자와 매치되는 규칙이 있습니다. 하지만 <code>re.DOTALL</code> 또는 <code>re.S</code> 옵션을 사용해 정규식을 컴파일하면 모두 매치할 수 있습니다.</p>
<pre><code class="language-python">import re
p = re.comile(&#39;a.b&#39;, re.DOTALL)
m = p.match(&#39;a\nb&#39;)

&gt;&gt;&gt; print(m)
&lt;re.Match object; span=(0, 3), match=&#39;a\nb&#39;&gt;</code></pre>
<br>

<h3 id="ignorecase-i">IGNORECASE, I</h3>
<p><code>re.IGNORECASE</code> 또는 <code>re.I</code> 옵션은 대소문자 구별 없이 매치를 수행할 때 사용하는 옵션입니다.</p>
<pre><code class="language-python">import re
p = re.compile(&quot;[a-z]+&quot;, re.I)

&gt;&gt;&gt; p.match(&#39;python&#39;)
&lt;re.Match object; span=(0, 6), match=&#39;python&#39;&gt;
&gt;&gt;&gt; p.match(&#39;Python&#39;)
&lt;re.Match object; span=(0, 6), match=&#39;Python&#39;&gt;
&gt;&gt;&gt; p.match(&#39;PYTHON&#39;)
&lt;re.Match object; span=(0, 6), match=&#39;PYTHON&#39;&gt;</code></pre>
<br>

<h3 id="multiline-m">MULTILINE, M</h3>
<p><code>re.MULTILINE</code> 또는 <code>re.M</code> 옵션은 문자열 전체의 처음이 아니라 각 라인의 처음으로 인식시킬 때 사용합니다.</p>
<pre><code class="language-python">import re

data = &quot;&quot;&quot;python one
life is too short
python two
you need python
python three&quot;&quot;&quot;

&gt;&gt;&gt; p = re.compile(&quot;^python\s\w+&quot;)
&gt;&gt;&gt; p.findall(data)
[&#39;python one&#39;]

&gt;&gt;&gt; p = re.compile(&quot;^python\s\w+&quot;, re.MULTILINE)
&gt;&gt;&gt; p.findall(data)
[&#39;python one&#39;, &#39;python two&#39;, &#39;python three&#39;]</code></pre>
<p>즉, <code>re.MULTILINE</code> 옵션은 ^, $ 메타 문자를 문자열의 각 줄마다 적용해 주는 것입니다.</p>
<h3 id="verbose-x">VERBOSE, X</h3>
<p>이해하기 어려운 정규식을 주석 또는 줄 단위로 구분할 수 있다면 얼마나 보기 좋고 이해하기 쉬울까요?</p>
<p>이 경우에 <code>re.VERBOSE</code> 또는 <code>re.X</code> 옵션을 사용하면 됩니다.</p>
<pre><code class="language-python">re.compile(r&quot;&quot;&quot;
    (?P&lt;name&gt;\w+)\s+     # 이름
    (?P&lt;phone&gt;            # 전화번호
        \d+[-]\d+[-]\d+
    )
&quot;&quot;&quot;, re.X)</code></pre>
<hr>
<h1 id="2-문제-풀이">2. 문제 풀이</h1>
<h2 id="문제">문제</h2>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/133499">https://school.programmers.co.kr/learn/courses/30/lessons/133499</a></p>
<blockquote>
<p>머쓱이는 태어난 지 11개월 된 조카를 돌보고 있습니다. 조카는 아직 &quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot; 네 가지 발음과 네 가지 발음을 조합해서 만들 수 있는 발음밖에 하지 못하고 연속해서 같은 발음을 하는 것을 어려워합니다. 문자열 배열 babbling이 매개변수로 주어질 때, 머쓱이의 조카가 발음할 수 있는 단어의 개수를 return하도록 solution 함수를 완성해주세요.</p>
</blockquote>
<br>

<h2 id="해결책">해결책</h2>
<pre><code class="language-python">import re
def solution(babbling):
    count = 0
    words = [&quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot;]
    pattern = re.compile(&#39;^(aya|ye|woo|ma)+$&#39;)

    for word in babbling:
        if pattern.match(word) and not any(word.count(w*2) for w in words):
            count += 1

    return count</code></pre>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://wikidocs.net/4308">파이썬 Docs - 8장 정규 표현식</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백수일기] 실업 급여 (3) - 실업인정 3차 with 워크넷 직업 심리 검사]]></title>
            <link>https://velog.io/@developer_khj/unemployment-benefits-3</link>
            <guid>https://velog.io/@developer_khj/unemployment-benefits-3</guid>
            <pubDate>Tue, 14 May 2024 10:05:27 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅은 3차 실업인정으로 워크넷 심리검사 중에서도 IT직무 역량검사를 실시하고, 실업인정을 신청하는 절차에 대해 기술하였습니다!</p>
<br>

<h1 id="1-직업심리검사">1. 직업심리검사</h1>
<h2 id="🔍-직업심리검사란">🔍 직업심리검사란?</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/18ee4de1-4b74-4ba8-9fcc-6f251df51ce2/image.png" alt=""></p>
<p>개인의 능력과 흥미, 성격 등 다양한 심리적 특성을 객관적으로 측정하여 자신에 대한 이해를 돕고 개인의 특성에 보다 적합한 진로분야를 선택할 수 있도록 분석해주는 검사입니다.</p>
<br>

<h2 id="⚡-워크넷-사이트-접속">⚡ 워크넷 사이트 접속</h2>
<p>우선, 워크넷 사이트에 접속합니다.</p>
<p><a href="https://www.work.go.kr/seekWantedMain.do">https://www.work.go.kr/seekWantedMain.do</a></p>
<br>

<p>워크넷에 접속하면 <strong>고용24</strong> 사이트로 들어가서 고용24를 사용할건지 워크넷을 사용할건지 물어보는데요, 아직 심리검사는 서비스가 넘어오지 않은 건지 이용할 수 없었습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/a1385f1e-545c-4b3b-866e-e8ba2bd108b3/image.png" alt="고용24 사이트"></p>
<p><code>워크넷으로 이동하기</code>를 클릭합니다</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/157551fd-ffaf-44c1-8d5b-9935e4cdb6f3/image.png" alt="워크넷 사이트"></p>
<p>메인 사이트에서 <code>직업심리검사</code> 버튼을 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/d568a343-de2d-4932-a073-488a7b806027/image.png" alt=""></p>
<p><code>성인 심리검사 바로가기</code> 버튼을 클릭해주세요</p>
<br>

<h2 id="💕-심리검사-목록">💕 심리검사 목록</h2>
<p>성인 대상 심리검사 목록입니다. 생각보다 다양한 검사들이 있었습니다</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c9bc613d-fe20-4204-adf3-7fcd5c69e6fd/image.png" alt="직업심리검사"></p>
<br>

<table>
<thead>
<tr>
<th>심리검사명</th>
<th>검사시간</th>
<th>실시가능</th>
<th>검사안내</th>
</tr>
</thead>
<tbody><tr>
<td>직업선호도검사 S형</td>
<td>25분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduPreSNewDetail.do">안내</a></td>
</tr>
<tr>
<td>직업선호도검사 L형</td>
<td>60분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduPreLNewDetail.do">안내</a></td>
</tr>
<tr>
<td>구직준비도검사</td>
<td>20분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduEquipDetail.do">안내</a></td>
</tr>
<tr>
<td>창업적성검사</td>
<td>20분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduFoundAptdDetail.do">안내</a></td>
</tr>
<tr>
<td>직업가치관검사(개정)</td>
<td>20분</td>
<td>인터넷</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExamNew/adltOccpOsvDetail.do">안내</a></td>
</tr>
<tr>
<td>영업직무 기본역량검사</td>
<td>50분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduCapaDetail.do">안내</a></td>
</tr>
<tr>
<td>IT직무 기본역량검사</td>
<td>95분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduItCapaDetail.do">안내</a></td>
</tr>
<tr>
<td>준고령자 직업선호도검사</td>
<td>20분</td>
<td>인터넷</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduOldDetail.do">안내</a></td>
</tr>
<tr>
<td>대학생 진로준비도검사</td>
<td>20분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/univJobPreDetail.do">안내</a></td>
</tr>
<tr>
<td>이주민 취업준비도 검사</td>
<td>60분</td>
<td>인터넷</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/immiEmpAblExamDetail.do">안내</a></td>
</tr>
<tr>
<td>중장년 직업역량검사</td>
<td>25분</td>
<td>인터넷</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/midExpEmpAblExamDetail.do">안내</a></td>
</tr>
<tr>
<td>성인용 직업적성검사</td>
<td>80분</td>
<td>인터넷, 지필</td>
<td><a href="https://www.work.go.kr/consltJobCarpa/jobPsyExam/aduAptNewDetail.do">안내</a></td>
</tr>
</tbody></table>
<br>

<h2 id="⭐-it-직무-기본역량검사">⭐ IT 직무 기본역량검사</h2>
<p>기본 직업선호도 검사는 의미가 없을 거같아 IT직무 기본역량검사를 실시했습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/3a91d2ed-24b3-4ac8-bbae-1c4b67711546/image.png" alt="IT직무 기본역량검사"></p>
<p>그러고는 역량검사를 선택한 것이 급 후회...
진짜 90분 가량이 소요</p>
<p>거의 NCS 수준으로 검사했습니다🤣</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c5a8ca12-9d7f-48c4-8d81-3ec4d9f2177f/image.png" alt="집중력 검사"></p>
<p>대망의 회로도 찾기..
이것이 집중력 검사입니까.. 집중력 교란 검사입니까😢</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/5f953669-60c9-413d-a491-f002097650ad/image.png" alt="검사결과"></p>
<p>그래도 검사결과 대부분 높은 편에 속해서 만족스러웠습니다🤗</p>
<br>

<h1 id="2-실업인정-신청">2. 실업인정 신청</h1>
<h2 id="검사결과-저장">검사결과 저장</h2>
<p>검사결과를 PDF로 저장합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/7885a712-189f-424a-89e2-1912b622ca1a/image.png" alt=""></p>
<h2 id="실업인정-인터넷-신청">실업인정 인터넷 신청</h2>
<p>고용24 홈페이지에 접속하여 실업인정 신청 페이지로 이동합니다</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/334bb0e6-bcb2-4b3b-b5aa-09a4ed2f4ac6/image.png" alt=""></p>
<p>구직활동내역에 직업심리검사를 선택하고 세부내역을 작성합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/98a05a02-de64-441c-b0a6-3afa9914c661/image.png" alt=""></p>
<p>이때 저장한 검사결과를 첨부합니다!</p>
<p>완료-⭐</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 크롤링(Crawling) - BeautifulSoup, Selenium]]></title>
            <link>https://velog.io/@developer_khj/Python-Crawling-BeautifulSoup-Selenium</link>
            <guid>https://velog.io/@developer_khj/Python-Crawling-BeautifulSoup-Selenium</guid>
            <pubDate>Thu, 04 Apr 2024 12:47:15 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 크롤링과 <strong>BeautifulSoup</strong>와 <strong>Selenium</strong> 모듈에 대해 공부하면서 포스팅을 작성해보고자 합니다.</p>
<hr>
<h1 id="1-crawling">1. Crawling</h1>
<h2 id="11-crawling-definition">1.1 Crawling Definition</h2>
<p><strong>크롤링(Crawling)</strong>이란 <strong>웹 상의 정보들을 탐색하고 수집하는 작업</strong>을 의미합니다.</p>
<p>흔히들 크롤링이 불법이라고 알려져 있지만, 완전히 맞는 말은 아닙니다. 그 이유는 <strong>Google의 검색 방식 또한 크롤링</strong>을 기반으로 만들어졌기 때문입니다.</p>
<p>하지만, 허가 없이 데이터를 <strong>탐색</strong>하여 <strong>상업적으로 활용</strong>하는 것이 불법입니다.</p>
<br>

<h3 id="사례-잡코리아와-사람인">사례. 잡코리아와 사람인</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/8dff175f-7e99-4482-b23f-e1e6eb75a9b1/image.png" alt=""></p>
<ul>
<li>잡코리아와 사람인HR: 구인·구직 플랫폼을 운영하는 회사</li>
<li>사람인HR이 잡코리아 웹사이트에 게재된 채용정보를 웹크롤링해 무단으로 수집한 후 그대로 자신의 웹사이트에 활용</li>
<li>사람인HR 측은 웹크롤링이 보편적인 IT기술이며 인터넷 관련 업계에서 널리 통용되는 영업 수단, 잡코리아의 권리를 침해한 사실이 없다고 주장</li>
</ul>
<p>이에 법원은 아래와 같이 판결했습니다.</p>
<ul>
<li>잡코리아의 웹사이트가 저작권법상 데이터베이스에 해당(저작권법 제2조 제19호)</li>
<li>이를 제작하고 유지하는 데 상당한 투자를 한 잡코리아는 데이터베이스 제작자의 권리(저작권법 제93조)</li>
</ul>
<p>사람인HR 주장과 달리, 공개된 정보에 대한 웹크롤링이 영업 수단으로 제한 없이 허용되는 것은 아니라는 것입니다.</p>
<p>웹사이트에서 누구나 확인할 수 있더라도 웹사이트 운영자가 상당한 시간과 노력을 들여 데이터베이스화한 정보를 특히나 경쟁업체에서 무단으로 수집·이용하는 행위는 허용되지 않는다는 취지로 풀이됩니다.</p>
<p>출저: <a href="https://www.donga.com/news/It/article/all/20230321/118450467/1">웹크롤링 판례 (1) 사람인HR의 잡코리아 채용정보 무단복제 사건, 동아닷컴</a></p>
</blockquote>
<br>

<h2 id="12-robotstxt">1.2 Robots.txt</h2>
<p>그렇다면 어떻게 허가된 데이터를 알 수 있을까요? 바로 <strong>robots.txt</strong>를 통해 알수 있습니다.</p>
<p>robots.txt 파일은 크롤러가 사이트에서 액세스할 수 있는 URL을 검색엔진 크롤러에게 알려줍니다.</p>
<p>웹사이트에서 url 뒤에 robots.txt를 넣으면 사이트에서 수집 가능한 정보를 알 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/32dda6f5-6f76-4d9e-8be2-807ae8ae69ba/image.png" alt="Robots.txt"></p>
<br>

<h2 id="13-scraping-vs-crawling">1.3 Scraping VS Crawling</h2>
<p>웹 스크래핑과 크롤링은 흔히 크롤링으로 혼용되어 사용되는 용어입니다. 두 용어의 공통점은 특정 웹사이트에서 데이터를 추출하는 것입니다.</p>
<p>주요 차이점은 URL로 해결할 수 없는 동적 데이터를 크롤링을 통해 추출할 수 있다는 점입니다. URL을 통해 HTML을 한번에 다운받느냐, </p>
<br>

<h2 id="14-언제-막힐-지-모르는-crawling">1.4 언제 막힐 지 모르는 Crawling</h2>
<p>크롤링은 사실 언제 동작이 안될지 모릅니다. 그 이유는 HTML을 기반으로 소스가 조금씩만 수정되도, 크롤링에서 지원하지 않을 지도 모르기 때문이죠.</p>
<hr>
<h1 id="2-python-module">2. Python Module</h1>
<h2 id="21-beautifulsoup">2.1 BeautifulSoup</h2>
<blockquote>
<h3 id="beautiful-soup">Beautiful Soup</h3>
<p>웹페이지에서 정보를 쉽게 스크랩할 수 있게 해주는 모듈입니다.</p>
<p>이는 HTML 또는 XML 파서 위에 위치하며 구문 분석 트리를 반복, 검색 및 수정하기 위한 Python 관용어를 제공합니다.</p>
<p>출저: <a href="https://pypi.org/project/beautifulsoup4/">beautifulsoup4 4.12.3</a></p>
</blockquote>
<p><strong>BeautifulSoup</strong>는 스크래핑 도구입니다. URL을 통해 받아온 HTML 코드를 분석하는 도구입니다.</p>
<p>크롤링 시에도 사용되는데, 엄밀히 말하면 직접 크롤링을 하는 주체가 아니라 크롤링해온 데이터를 HTML 객체로 파싱해주는 <strong>파싱 도구</strong>입니다.</p>
<br>

<h2 id="22-selenium">2.2 Selenium</h2>
<blockquote>
<h3 id="selenium">Selenium</h3>
<p>Selenium WebDriver를 Python 언어로 제작된 모듈로, 웹 브라우저와의 상호작용을 자동화하기 위해 사용됩니다.</p>
<p>Chrome, Firefox, Internet Explorer 등 여러 브라우저 및 드라이버를 지원합니다.
출저: <a href="https://pypi.org/project/selenium/">selenium 4.22.0</a></p>
</blockquote>
<br>

<h2 id="23-requirementstxt">2.3 Requirements.txt</h2>
<p>프로젝트 내에 <strong>requirements.txt 파일</strong>을 추가해주고, 라이브러리 종속성을 넣어줍니다.</p>
<pre><code class="language-python">requests
beautifulsoup4
selenium
webdriver_manager</code></pre>
<br>

<p>이미 프로젝트가 생성되어 라이브러리가 설치된 후라면 <strong>freeze, list 명령어</strong>를 통해 requirements.txt를 생성할 수 있습니다.</p>
<pre><code class="language-bash"># freeze 명령어 사용 시
pip freeze &gt; requirements.txt

# list 명령어 사용시
pip list --format=freeze &gt; requirements.txt</code></pre>
<br>

<p>혹은 이미 requirements.txt가 있는 경우에, IDE가 지원해주지 않는다면 <strong>install 명령어</strong>를 통해 라이브러리를 설치할 수 있습니다.</p>
<pre><code class="language-python">pip install -r requirements.txt</code></pre>
<br>

<p><strong>PyCharm</strong>에서 requirements.txt를 추가해주면, 아래와 같이 설치 여부를 물어봅니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c6a4e5a9-1533-4d11-8c4e-9a9fd22bec43/image.png" alt="Pycharm requirement 설치"></p>
<hr>
<h1 id="3-beautifulsoup">3. BeautifulSoup</h1>
<h2 id="31-기본-구조">3.1 기본 구조</h2>
<p>beautifulsoup의 사용 방법은 아래와 같습니다.</p>
<ol>
<li>스크랩핑할 웹사이트 URL 지정</li>
<li>URL을 통해 html을 받아와서 BeautifulSoup 객체로 변환</li>
<li>bs 객체를 통해 데이터 파싱</li>
</ol>
<pre><code class="language-python">import requests
from bs4 import BeautifulSoup, Tag

# 1. URL 지정
url = &#39;https://news.naver.com/section/105&#39;

# 2.1 데이터 받아오기
response = requests.get(url)

if response.status_code == 200:
    html = response.text

    # 2.2 BS 객체로 변환
    soup = BeautifulSoup(html, &#39;html.parser&#39;)
    print(soup)

    # 3. 파싱
else:
    print(response.status_code)</code></pre>
<br>

<p>BeutifulSoup는 <strong>find()</strong> 및 <strong>findAll()</strong> 메소드를 통해 간편하게 HTML의 정보를 가져올 수 있습니다.</p>
<br>

<h2 id="32-네이버-뉴스-스크랩핑">3.2 네이버 뉴스 스크랩핑</h2>
<p>위의 코드를 확장하여 네이버 뉴스를 스크랩핑 해보고자 합니다.</p>
<br>

<h3 id="문서-구조-파악">문서 구조 파악</h3>
<p>우선, 데이터를 파싱하기 위해서는 문서의 구조를 잘 알고 있어야합니다. </p>
<p>웹 사이트에서 <strong>수집할 데이터를 분석</strong>하고, <strong>F12</strong>를 통해 개발자 모드를 켜서 해당 데이터를 가져올 부분의 구조를 파악하여 합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/3a277d0e-5174-42aa-a845-27d3766e40a4/image.png" alt="네이버 뉴스"></p>
<p>저는 <strong>뉴스 기사 목록</strong>을 뽑아내볼 생각입니다.</p>
<br>

<h3 id="객체로-파싱하기">객체로 파싱하기</h3>
<p>우선, 기사를 담을 <strong>Article 객체를 정의</strong>하였습니다.</p>
<pre><code class="language-python">class Article:
    def __init__(self, title, desc, url, image_url):
        self.title = title
        self.description = desc
        self.url = url
        self.image_url = image_url

    def __str__(self):
        return f&quot;Article [ \n\t Title: {self.title} 
            \n\t Description: {self.description}
            \n\t URL: {self.url}
            \n\t Image URL: {self.image_url}\n]&quot;</code></pre>
<br>

<p>그런 다음, 데이터를 파싱하여 <strong>Article 객체로 변환</strong>하였습니다.</p>
<pre><code class="language-python">def parse(element: Tag):
    try:
        a_obj = element.find(&#39;a&#39;)
        img_obj = element.find(&#39;img&#39;)
        desc_obj = element.find(&#39;div&#39;, &#39;sa_text_lede&#39;)

        return Article(img_obj.attrs[&#39;alt&#39;], 
                    desc_obj.text,
                    a_obj.attrs[&#39;href&#39;],
                    img_obj.attrs[&#39;data-src&#39;])
    except AttributeError:
        return None</code></pre>
<br>

<p>위의 기본 구조에서 파싱 단계를 추가하였습니다.</p>
<pre><code class="language-python">if response.status_code == 200:
    html = response.text
    soup = BeautifulSoup(html, &#39;html.parser&#39;)

    arr = list(map(lambda x: parse(x),
                soup.find_all(&quot;li&quot;, {&quot;class&quot;: &quot;sa_item&quot;})))
    for article in arr:
        print(article)
else:
    print(response.status_code)</code></pre>
<br>

<h3 id="결과-화면">결과 화면</h3>
<p>기사 목록이 객체로 아주 예쁘게 뽑힌 것을 볼 수 있습니다.😊</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f04d9ad1-24a2-4cc6-b4f0-7abd78c1b094/image.png" alt="결과 화면"></p>
<hr>
<h1 id="4-selenium">4. Selenium</h1>
<p>일반적인 정적 웹페이지는 https 모듈로 데이터를 뽑을 수 있지만, 동적인 기능이 들어가면 (ex. 로그인, 검색, 버튼 클릭 등) 동작이 불가능합니다.</p>
<br>

<h2 id="41-기본-구조">4.1 기본 구조</h2>
<p>우선 Selenium을 사용하는 방법은 다음과 같습니다.</p>
<ol>
<li>WebDriver 로딩</li>
<li>WebDriver로 동적 행위 (로그인, 검색 등)</li>
<li>현재 html을 받아와서 파싱해서 처리</li>
</ol>
<pre><code class="language-python">import time
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.ie.webdriver import WebDriver
from webdriver_manager.chrome import ChromeDriverManager

def crawler(url, seconds=1) -&gt; WebDriver:
    service = Service(ChromeDriverManager().install())
    chrome_options = Options()

    chrome_options.add_experimental_option(&quot;detach&quot;, True)
    driver = webdriver.Chrome(service=service, options=chrome_options)
    driver.get(url)

    time.sleep(seconds)
    return driver


def get_bs(html) -&gt; BeautifulSoup:
    return BeautifulSoup(html, &#39;html.parser&#39;)

if __name__ == &#39;__main__&#39;:
    url = &#39;https://www.google.com&#39;

    # 1. 드라이버 로딩
    driver = crawler(url)</code></pre>
<br>

<p>드라이버를 통해 url을 로딩하면 아래와 같이 자동으로 웹페이지가 뜹니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/50b4f67e-1a87-4de3-89b2-0e600207f6e1/image.png" alt=""></p>
<br>

<h2 id="42-구글링-구현하기">4.2 구글링 구현하기</h2>
<p>구글 홈페이지를 띄워두고, 키워드를 넣어 검색한 페이지의 목록을 가져오는 코드를 작성해보려고 합니다. 위의 코드를 확장하여 구글링을 구현해보도록 하겠습니다.</p>
<h3 id="동적-행위">동적 행위</h3>
<p>우선, 구글 홈페이지를 켜고 검색 영역을 분석합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/9d1bf0c6-df64-4819-963c-e8c75529758c/image.png" alt="구글 홈페이지"></p>
<pre><code class="language-python">class Post:
    def __init__(self, platform, url, title, content):
        self.platform = platform
        self.url = url
        self.title = title
        self.content = content

    def __str__(self):
        return f&quot;&quot;&quot;Post [\n\tplatform: {self.platform}
        \n\t url: {self.url}
        \n\t title: {self.title}
        \n\t content: {self.content}\n]&quot;&quot;&quot;


def parse(element: Tag):
    try:
        platform = element.find(&#39;span&#39;, {&#39;class&#39;: &#39;VuuXrf&#39;}).text
        url = element.find(&#39;a&#39;).attrs[&#39;href&#39;]
        title = element.find(&#39;h3&#39;).text
        content = element.find(&#39;div&#39;, {&#39;class&#39;: &#39;VwiC3b&#39;}).text
        return Post(platform, url, title, content)
    except AttributeError:
        return None


if __name__ == &#39;__main__&#39;:
    url = &#39;https://www.google.com&#39;
    keyword = &#39;Python Selenium&#39;
    driver = crawler(url)

    # 1. 검색 영역을 찾아 키워드를 넣고 검색
    # By.ID, By.TAG_NAME, By.NAME, By.CSS_SELECTOR, By.CLASS_NAME, By.XPATH
    input_box = driver.find_element(By.CSS_SELECTOR, &quot;textarea.gLFyf&quot;)
    input_box.send_keys(keyword)
    input_box.submit()
    driver.implicitly_wait(10)

    # 2. 검색한 데이터를 Post 객체로 파싱
    soup = get_bs(driver.page_source)
    container = soup.find_all(&#39;div&#39;, class_=&#39;MjjYud&#39;)
    arr = list(map(lambda x: parse(x), container))
    for i in arr:
        print(i)
    driver.close()</code></pre>
<p>input 영역을 찾아 키워드를 넣고 제출합니다. 검색 버튼이 따로 없기 때문에 submit 메소드로 검색합니다.</p>
<br>

<h2 id="43-결과-화면">4.3 결과 화면</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/0a76a3b7-d79c-4e9f-b682-f1248ad915a3/image.gif" alt="결과 화면"></p>
<pre><code>Post [
    platform: 위키독스
    url: https://wikidocs.net/91474
    title: 2.8 사이트 자동화하기 - selenium 사용법(1)
    content: 코딩 실력 향상 100% 보장, 실전 파이썬 데이터 수집 강의 (도움 안되면 환불가능) ... Selenium 라이브러리를 사용하는 이유는 다음과 같습니다. 1. 자바스크립트 ...
]
Post [
    platform: 티스토리
    url: https://spectrum20.tistory.com/entry/python-Selenium-%ED%81%AC%EB%A1%AC-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80
    title: [python] Selenium chrome에서 시작하기 (+ 크롬 브라우저 ...
    content: 2023. 10. 15. — [python] Selenium chrome에서 시작하기 (+ 크롬 브라우저, element, driver, alert 다루기) · 크롬드라이버 설치 · 코드. 파이썬 환경에 필요한 ...
]
Post [
    platform: 티스토리
    url: https://thecho7.tistory.com/entry/Python%EA%B3%BC-Selenium%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81-%EC%8B%A4%EC%8A%B5-1
    title: Python과 Selenium을 활용한 웹 크롤링 실습 - 1 1 - 조연 블로그
    content: 2023. 8. 28. — Python과 Selenium을 활용한 웹 크롤링 실습 - 1 1 · Selenium 설치 · 브라우저 시작 · 웹사이트 탐험하는 방법 · Selenium 기본 기능 · 관련글 · 댓글.
]</code></pre><hr>
<h1 id="5-image-다운로드">5. Image 다운로드</h1>
<h2 id="51-urlretrieve">5.1 urlretrieve</h2>
<pre><code class="language-python"># Test URL
url = &quot;https://www.residentadvisor.net/images/events/flyer/2017/7/no-0713-986042-front.jpg&quot;

def download_image(url, filename):
    urlretrieve(url, filename)

if __name__ == &quot;__main__&quot;:
    download_image(url, &quot;testPhytonImg.jpg&quot;);</code></pre>
<br>

<h2 id="52-403-forbidden">5.2 403 Forbidden</h2>
<blockquote>
<h3 id="403-forbidden">403 Forbidden</h3>
<p>작동중인 서버에 클라이언트의 요청이 도달했으나, 서버가 클라이언트의 접근을 거부할 때 반환하는 HTTP 응답 코드이자 오류 코드다.</p>
<p>출저 - <a href="https://namu.wiki/w/403%20Forbidden">Wiki</a></p>
</blockquote>
<p>Image 다운로드하는데 403 에러가 뜬다.
이유는 <code>User-Agent</code> 정보가 없어서 그렇습니다.</p>
<blockquote>
<h3 id="user-agent">User-Agent</h3>
<p>user agent는 HTTP 요청을 보내는 디바이스와 브라우저 등 사용자 소프트웨어의 식별 정보를 담고 있는 request header의 한 종류이다.</p>
<ul>
<li>형태: Mozilla 정보/버전 + 운영체제 정보 + 렌더링 엔진 정보 + 브라우저</li>
</ul>
<p>출저 - <a href="https://velog.io/@ggong/User-agent-%EC%A0%95%ED%99%95%ED%95%98%EA%B2%8C-%ED%95%B4%EC%84%9D%ED%95%98%EA%B8%B0">User-agent 정확하게 해석하기</a></p>
</blockquote>
<p><strong>User-Agent</strong>는 쉽게 말해 접속하는 장치(브라우저, 디바이스)의 정보인데, request 라이브러리를 통해 요청할 경우 로봇(크롤러)라 판단하여 접속을 막는 것입니다. 쉽게 말해, 보안 조치라고 생각하면 됩니다.</p>
<br>

<p>코드를 조금 수정하여 header에 User Agent를 추가하여 요청해면 데이터를 다운받을 수 있습니다. (이렇게 수집된 데이터를 활용해서는 안됩니다)</p>
<pre><code class="language-python">import urllib.request

# Test URL
url = &quot;https://www.residentadvisor.net/images/events/flyer/2017/7/no-0713-986042-front.jpg&quot;

def download_image(url, filename):
    headers = {
        &#39;User-Agent&#39;: &#39;Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
                        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537&#39;,
    }
    req = urllib.request.Request(url, None, headers)
    res = urllib.request.urlopen(req)

    f = open(filename, &#39;wb&#39;)
    f.write(res.read())
    f.close()

 if __name__ == &quot;__main__&quot;:
    download_image(url, &quot;testPhytonImg.jpg&quot;);</code></pre>
<p>출저 - <a href="https://stackoverflow.com/questions/64315481/how-to-get-django-model-id-in-views-py">Stack Overflow</a></p>
<hr>
<h1 id="💎-reference">💎 Reference</h1>
<ul>
<li><a href="https://wikidocs.net/85739">2.6 사이트 정보 추출하기 - beautifulsoup 사용법 (1)</a></li>
<li><a href="https://wikidocs.net/137914">동적 웹크롤링 - selenium 소개 및 기초사용법</a></li>
<li><a href="https://code-angie.tistory.com/15">pip 패키지 목록 requirements.txt 생성 및 설치 방법</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pelican] Pelican Plugins]]></title>
            <link>https://velog.io/@developer_khj/Pelican-Add-Plugins</link>
            <guid>https://velog.io/@developer_khj/Pelican-Add-Plugins</guid>
            <pubDate>Wed, 03 Apr 2024 15:12:17 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 Pelican에 플러그인 추가하면서 삽질을 많이 해서 플러그인 추가하는 방법과 제가 사용했던 플러그인들을 공유해볼까 합니다.</p>
<hr>
<h1 id="1-pelican-plugin">1. Pelican Plugin</h1>
<h2 id="11-official-repository">1.1 Official Repository</h2>
<p>주소: <a href="https://github.com/getpelican/pelican-plugins">https://github.com/getpelican/pelican-plugins</a></p>
<p>위의 주소로 들어가면 공식 plugin 목록들을 볼수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c7c91c9a-beb1-4d90-9440-f4929dc10a12/image.png" alt="공식 레포 스크린샷"></p>
<br>

<h2 id="12-install">1.2 Install</h2>
<p>official repository를 다운받습니다.</p>
<pre><code class="language-shell">git clone --recursive https://github.com/getpelican/pelican-plugins</code></pre>
<p>원하는 플러그인들을 프로젝트안에 넣습니다.</p>
<p>설정 파일인 <code>pelicanconf.py</code>에 아래 항목을 채워넣습니다.</p>
<pre><code class="language-python">PLUGIN_PATHS = [&#39;path/to/pelican-plugins&#39;] # 플로그인 상위 경로
PLUGINS = [&#39;assets&#39;, &#39;sitemap&#39;, &#39;gravatar&#39;] # 사용하려는 플러그인 목록</code></pre>
<p>저는 테마 폴더 안에 플러그인을 내장해놔서 아래와 같이 구성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/80f7636b-a54b-46c0-9c23-8db46ab257dd/image.png" alt="파일 구조"></p>
<p>아래와 같은 플러그인들을 사용했습니다.</p>
<ul>
<li><code>sitemap</code></li>
<li><code>representative_image</code></li>
<li><code>share_posts</code></li>
<li><code>neighbors</code></li>
<li><code>custom_article_urls</code></li>
</ul>
<p>제가 사용했던 플러그인을 소개해볼까합니다.</p>
<hr>
<h1 id="2-pelican-plugins">2. Pelican Plugins</h1>
<p>플러그인을 설치하면 해당 플러그인의 md 파일에서 설정하는 방법에 대해 상세하게 알려줍니다.</p>
<br>

<h2 id="21-sitemap">2.1 sitemap</h2>
<p><strong>Sitemap</strong>을 생성해주는 플러그인입니다.</p>
<h3 id="usage">Usage</h3>
<p>해당 플러그인을 설치하면 <code>url/sitemap.xml</code> 경로로 사이트맵을 볼 수 있습니다.
예시: <a href="https://twinklekhj.xyz/blog/sitemap.xml">https://twinklekhj.xyz/blog/sitemap.xml</a></p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e042eb6d-17b7-4a18-819c-348931c6ed08/image.png" alt="Sitemap 예시"></p>
<h3 id="settings">Settings</h3>
<p><code>pelicanconf.py</code>에 아래와 같이 플러그인 목록에 추가해줍니다.</p>
<pre><code class="language-python">PLUGINS=[&#39;sitemap&#39;]</code></pre>
<br>

<p>SITEMAP의 기본적인 세팅은 다음과 같습니다.</p>
<pre><code class="language-python">SITEMAP = {
    &#39;format&#39;: &#39;xml&#39;
}</code></pre>
<p>자세한 설정은 <a href="https://github.com/pelican-plugins/sitemap">Sitemap GitHub Repository</a>에서 보실 수 있습니다.</p>
<h3 id="sitemap-definition">Sitemap Definition</h3>
<blockquote>
<h3 id="💡-sitemap">💡 Sitemap</h3>
<p>웹에서 특정 단어를 검색하게 되면, 검색엔진을 통해서 원하는 정보를 찾을 수 있게 해줍니다. </p>
<p>이때, 검색엔진은 크롤러라고 불리는 로봇을 이용하여 다양한 웹 정보를 수집합니다. 크롤러가 웹사이트들을 방문하여 웹 사이트 내의 여러 페이지들과 정보에 대해 수집하는데, 이때 도움을 주는 것이 바로 <strong>사이트 맵</strong>입니다.</p>
<p>사이트맵(sitemap)은 웹사이트의 구조를 크롤러가 찾기 쉽도록 정리한 XML 형식의 파일입니다.
<img src="https://velog.velcdn.com/images/developer_khj/post/c406aeb8-8ea7-4ba2-b6a1-bb0ab5ca4b2a/image.png" alt=""></p>
<p>사이트맵이라는 단어는 <strong>사이트</strong>와 <strong>맵</strong>의 합성어로, 웹사이트의 지도라는 뜻입니다. 사이트맵은 크롤러에게 웹사이트의 구조와 목차를 설명해 주고 보다 쉽게 정보를 수집해갈 수 있도록 하는 역할을 합니다.</p>
<p>출저: <a href="https://m.blog.naver.com/patchwork_corp/222337697188">사이트맵(sitemap)을 알아보자</a></p>
</blockquote>
<br>

<h2 id="22-representative-image">2.2 Representative Image</h2>
<p>Article의 대표 이미지를 설정해주는 플러그인입니다.
해당 플러그인을 설정하면 아래와 같이 article에 대표 이미지를 설정할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/ab57e90a-8d6b-44cf-ba25-d7d287f92274/image.png" alt="Post 목록"></p>
<p><code>pelicanconf.py</code>에 아래와 같이 플러그인 목록에 추가해줍니다.</p>
<pre><code class="language-python">PLUGINS = [&#39;representative_image&#39;]</code></pre>
<br>

<p>이제 template 파일에서 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-html">{% for article in articles_page.object_list %}
    {% if article.featured_image %}
        {# Representation Image Plugin Start #}
            &lt;img src=&quot;{{ article.featured_image }}&quot; 
                 alt=&quot;{{ article.title|capitalize }} Preview Image&quot;&gt;
        {# Representation Image Plugin End #}
    {% else %}
{% else %}</code></pre>
<br>

<h2 id="23-share-posts">2.3 Share Posts</h2>
<p>작성한 Article을 소셜 미디어에 공유할 수 있는 플러그인입니다.
해당 플러그인을 설정하면 Article에 아래와 같이 추가하여 사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/d623aeff-91f5-4591-a62d-65b0b5c01182/image.png" alt=""></p>
<p><code>pelicanconf.py</code>에 아래와 같이 플러그인 목록을 추가해줍니다.</p>
<pre><code class="language-python">PLUGINS = [&#39;share_posts&#39;]</code></pre>
<br>

<p>이제 템플릿에서 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-html">{# Share Post Plugin Start #}
{% if article.share_post %}
&lt;ul&gt;
&lt;li&gt;
  &lt;a href=&quot;{{ article.share_post[&#39;email&#39;] }}&quot;
     target=&quot;_blank&quot; title=&quot;Share via Email&quot;&gt;Email&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
  &lt;a href=&quot;{{ article.share_post[&#39;twitter&#39;] }}&quot;
     target=&quot;_blank&quot; title=&quot;Share on Twitter&quot;&gt;Twitter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
  &lt;a href=&quot;{{ article.share_post[&#39;facebook&#39;] }}&quot;
     target=&quot;_blank&quot; title=&quot;Share on Facebook&quot;&gt;Facebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
  &lt;a href=&quot;{{ article.share_post[&#39;linkedin&#39;] }}&quot;
     target=&quot;_blank&quot; title=&quot;Share on LinkedIn&quot;&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
{% endif %}
{# Share Post Plugin End #}</code></pre>
<p>저는 한국에서 사용할 만한 Social을 추가했는데 공식적으로는 <code>facebook</code>, <code>email</code>, <code>twitter</code>, <code>diaspora</code>, <code>linkedin</code>, <code>hacker-news</code>, <code>reddit</code> 항목들을 지원합니다.</p>
<p>아쉬운 점은 Pelican 레퍼런스가 외국이다 보니 한국에서 보통 많이 사용하는 소셜 미디어(Instagram, KakaoTalk 등)가 없다는 점입니다.😢 그래도 없는 것보단 나으니 추가해주었습니다.</p>
<br>

<h2 id="24-custom-article-url">2.4 Custom Article URL</h2>
<p>작성한 article의 경로를 바꿀수 있는 플러그인입니다.</p>
<p>기존에는 root에 생성되던 파일에 카테고리로 구분할 수 있는 경로를 추가해주었습니다.</p>
<ul>
<li>기존: <a href="https://localhost:8000/create-pelican-site.html">https://localhost:8000/create-pelican-site.html</a></li>
<li>변경: <a href="https://localhost:8000/posts/pelican/create-pelican-site.html">https://localhost:8000/posts/pelican/create-pelican-site.html</a></li>
</ul>
<br>

<p><code>pelicanconf.py</code>에 아래와 같이 플러그인 목록을 추가해줍니다.</p>
<pre><code class="language-python">PLUGINS = [&#39;custom_article_urls&#39;]</code></pre>
<p>Custom Article URL 설정은 다음과 같습니다.</p>
<pre><code class="language-python"># PLUGIN - custom_article_urls
CUSTOM_ARTICLE_URLS = {
    &#39;Category 1&#39;: {
        &#39;URL&#39;: &#39;{category}/{slug}.html&#39;,
        &#39;SAVE_AS&#39;: &#39;{category}/{slug}.html&#39;
    }
}</code></pre>
<br>

<h2 id="25-neighbors">2.5 neighbors</h2>
<p>작성한 Article의 이전/다음 포스트를 알려주는 플러그인입니다.</p>
<p><code>pelicanconf.py</code>에 아래와 같이 플러그인 목록을 추가해줍니다.</p>
<pre><code class="language-python">PLUGINS = [&#39;neighbors&#39;]</code></pre>
<p>이제 템플릿에서 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-html">&lt;ul&gt;
    {% if article.prev_article %}
        &lt;li&gt;
            &lt;a href=&quot;{{ SITEURL }}/{{ article.prev_article.url}}&quot;&gt;
                {{ article.prev_article.title }}
            &lt;/a&gt;
        &lt;/li&gt;
    {% endif %}
    {% if article.next_article %}
        &lt;li&gt;
            &lt;a href=&quot;{{ SITEURL }}/{{ article.next_article.url}}&quot;&gt;
                {{ article.next_article.title }}
            &lt;/a&gt;
        &lt;/li&gt;
    {% endif %}
&lt;/ul&gt;
&lt;ul&gt;
    {% if article.prev_article_in_category %}
        &lt;li&gt;
            &lt;a href=&quot;{{ SITEURL }}/{{ article.prev_article_in_category.url}}&quot;&gt;
                {{ article.prev_article_in_category.title }}
            &lt;/a&gt;
        &lt;/li&gt;
    {% endif %}
    {% if article.next_article_in_category %}
        &lt;li&gt;
            &lt;a href=&quot;{{ SITEURL }}/{{ article.next_article_in_category.url}}&quot;&gt;
                {{ article.next_article_in_category.title }}
            &lt;/a&gt;
        &lt;/li&gt;
    {% endif %}
&lt;/ul&gt;</code></pre>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li>Pelican Plugin - <a href="https://github.com/pelican-plugins/sitemap">Sitemap</a></li>
<li>Pelican Plugin - <a href="https://github.com/getpelican/pelican-plugins/tree/master/share_post">share_post</a></li>
<li>Pelican Plugin - <a href="https://github.com/getpelican/pelican-plugins/tree/master/representative_image">representative_image</a></li>
<li>Pelican Plugin - <a href="https://github.com/getpelican/pelican-plugins/tree/master/custom_article_urls">custom_article_urls</a></li>
<li>Pelican Plugin - <a href="https://github.com/pelican-plugins/neighbors">neighbors</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>Pelican에 대해 알아보면서 많이 사용되는 프로그램은 아니다보니 레퍼런스가 많이 없다는 것을 실감하면서 아쉬웠습니다.😢</p>
<p>그래도 Plugin을 추가하면 기능이 풍부해져 필요한 기능이 있으면 공식 레포에서 찾아보면서 추가하면 좋을 거 같습니다.🤗</p>
<p>이상으로 Pelican Plugin 소개를 마치겠습니다.🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pelican] Pelican Structure & Theme Demo]]></title>
            <link>https://velog.io/@developer_khj/Pelican-Make-Theme</link>
            <guid>https://velog.io/@developer_khj/Pelican-Make-Theme</guid>
            <pubDate>Tue, 02 Apr 2024 10:53:49 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>자체 제작 테마가 완성되었습니다~!🙌🙌
테마를 만들면서 경험했던 내용을 공유하고자 합니다.</p>
<p>테마를 만들어 GitHub에 <code>Public Repo</code>로 공유하고 있습니다.💖
제가 만든 테마는 하위 링크에서 소스 및 데모페이지를 보실 수 있습니다.😎</p>
<blockquote>
<ul>
<li>소스: <a href="https://github.com/twinklekhj/pelican-twinkle">https://github.com/twinklekhj/pelican-twinkle</a></li>
<li>데모: <a href="https://twinklekhj.xyz/blog/">https://twinklekhj.xyz/blog/</a></li>
</ul>
</blockquote>
<p>이번 포스팅에서는 Pelican의 구조와 템플릿 만드는 방법, 템플릿 별 설명 및 구현 화면, 커스텀 템플릿 추가 등 테마에 커스텀할 수 있는 것들에 대해 설명해볼까합니다.</p>
<hr>
<h1 id="1-pelican-structure">1. Pelican Structure</h1>
<h2 id="11-file-structure">1.1 File Structure</h2>
<p>테마를 개발하기 위해서는 아래의 파일 구조를 따라야합니다.</p>
<pre><code>├── static
│   ├── css
│   └── images
└── templates
    ├── archives.html         // to display archives
    ├── article.html          // processed for each article
    ├── author.html           // processed for each author
    ├── authors.html          // must list all the authors
    ├── categories.html       // must list all the categories
    ├── category.html         // processed for each category
    ├── index.html            // the index (list all the articles)
    ├── page.html             // processed for each page
    ├── period_archives.html  // to display time-period archives
    ├── tag.html              // processed for each tag
    └── tags.html             // must list all the tags. Can be a tag cloud.</code></pre><ul>
<li><code>static</code> 폴더: css, js 등 정적 리소스</li>
<li><code>templates</code> 폴더: 콘텐츠 생성에 사용될 템플릿 목록들</li>
</ul>
<br>

<blockquote>
<h3 id="템플릿">템플릿</h3>
<p>템플릿은 Pelican에서 직접 만들어주는 페이지로, 해당 페이지에 <strong>[1.2] Pelican 변수</strong>를 사용할 수 있습니다.</p>
<ul>
<li>템플릿 파일이 꼭 있을 필요는 없습니다.</li>
<li>또, <code>html</code>을 추가한다고 해서 템플릿으로 생성되는 것은 아닙니다.</li>
</ul>
</blockquote>
<br>

<p>중요도에 따라 템플릿을 정리해보면 다음과 같습니다.</p>
<blockquote>
<h3 id="템플릿-목록">템플릿 목록</h3>
<ul>
<li><code>index</code>: (필수) 메인 페이지 (포스트 목록)</li>
<li><code>article</code>: (필수) 포스트 별 내부 페이지</li>
<li><code>categories</code>: 카테고리 목록 보여주는 화면</li>
<li><code>category</code>: 카테고리 별 페이지 (포스트 목록)</li>
<li><code>tags</code>: 태그 목록 보여주는 화면</li>
<li><code>tag</code>: 태그 별 페이지 (포스트 목록)</li>
<li><code>archives</code>: date 순 포스트 목록 페이지 (포스트 목록)</li>
<li><code>period_archives</code>: 기간별 포스트 목록 페이지 (포스트 목록)</li>
</ul>
</blockquote>
<br>

<h2 id="12-pelican-variable">1.2 Pelican Variable</h2>
<p>Pelican Template에서 사용할 수 있는 변수 목록들입니다. <a href="https://docs.getpelican.com/en/4.9.1/themes.html">Pelican Docs</a>에서 자세히 볼 수 있습니다.</p>
<p>변수는 너무 많고, 설명이 부족해서 이해안되는 부분들이 많아서 최대한 이해하고 사용해보고, 다른 테마들의 소스 코드를 참고하였습니다.😂</p>
<p>Jekyll이나 기술처럼 알려지지 않았다보니 레퍼런스가 거의 없는게 아쉬웠습니다..</p>
<h3 id="common-variable">Common Variable</h3>
<p>모든 템플릿에서 사용할 수 있는 변수 목록들입니다.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>type</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>output_file</td>
<td><code>string</code></td>
<td>현재 페이지 파일명</td>
</tr>
<tr>
<td>articles</td>
<td><code>List&lt;Article&gt;</code></td>
<td>게시글 목록</td>
</tr>
<tr>
<td>dates</td>
<td><code>List&lt;Article&gt;</code></td>
<td>date로 오름차순 정렬된 게시글 목록</td>
</tr>
<tr>
<td>hidden_articles</td>
<td><code>List&lt;Article&gt;</code></td>
<td>숨겨진 게시글 목록</td>
</tr>
<tr>
<td>drafts</td>
<td><code>List&lt;Article&gt;</code></td>
<td>작성중인 게시글 목록</td>
</tr>
<tr>
<td>authors</td>
<td><code>Tuple&lt;Tuple&lt;Author, List&lt;Articles&gt;&gt;</code></td>
<td>모든 저자와 저자가 쓴 게시글 목록</td>
</tr>
<tr>
<td>categories</td>
<td><code>Tuple&lt;Tuple&lt;Category, List&lt;Articles&gt;&gt;</code></td>
<td>모든 카테고리와 카테고리 별 게시글 목록</td>
</tr>
<tr>
<td>tags</td>
<td><code>Tuple&lt;Tuple&lt;Tag, List&lt;Articles&gt;&gt;&gt;</code></td>
<td>모든 태그와 태그 별 게시글 목록</td>
</tr>
<tr>
<td>pages</td>
<td><code>List&lt;Page&gt;</code></td>
<td>페이지 목록</td>
</tr>
<tr>
<td>hidden_pages</td>
<td><code>List&lt;Page&gt;</code></td>
<td>숨겨진 페이지 목록</td>
</tr>
<tr>
<td>draft_pages</td>
<td><code>List&lt;Page&gt;</code></td>
<td>초안 페이지 목록</td>
</tr>
</tbody></table>
<br>

<h3 id="page-variable">Page Variable</h3>
<p>Pagination이 활성화 되어있다면, 아래와 같은 변수를 쓸 수 있습니다.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>articles_paginator</td>
<td>Paginator</td>
</tr>
<tr>
<td>articles_page</td>
<td>현재 페이지 정보</td>
</tr>
<tr>
<td>articles_previous_page</td>
<td>이전 페이지 정보</td>
</tr>
<tr>
<td>articles_next_page</td>
<td>다음 페이지 정보</td>
</tr>
<tr>
<td>dates_paginator</td>
<td>(오름차순 정렬) Paginator</td>
</tr>
<tr>
<td>dates_page</td>
<td>(오름차순 정렬) 현재 페이지 정보</td>
</tr>
<tr>
<td>dates_previous_page</td>
<td>(오름차순 정렬) 이전 페이지 정보</td>
</tr>
<tr>
<td>dates_next_page</td>
<td>(오름차순 정렬) 다음 페이지 정보</td>
</tr>
<tr>
<td>page_name</td>
<td>현재 페이지명 (index)</td>
</tr>
</tbody></table>
<br>

<h2 id="13-pelican-objects">1.3 Pelican Objects</h2>
<h3 id="article">Article</h3>
<p><strong>Article</strong>에 담긴 정보입니다.</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><strong>title</strong></td>
<td>Title of the article.</td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td>Page slug.</td>
</tr>
<tr>
<td><strong>url</strong></td>
<td>URL to the article page.</td>
</tr>
<tr>
<td><strong>author</strong></td>
<td>The Author of this article.</td>
</tr>
<tr>
<td>authors</td>
<td>A list of Authors of this article.</td>
</tr>
<tr>
<td><strong>category</strong></td>
<td>The Category of this article.</td>
</tr>
<tr>
<td><strong>status</strong></td>
<td>The article status, can be any of ‘published’ or ‘draft’.</td>
</tr>
<tr>
<td><strong>summary</strong></td>
<td>Rendered summary content.</td>
</tr>
<tr>
<td>tags</td>
<td>List of Tag objects.</td>
</tr>
<tr>
<td>translations</td>
<td>List of translations Article objects.</td>
</tr>
<tr>
<td>content</td>
<td>The rendered content of the article.</td>
</tr>
<tr>
<td><strong>date</strong></td>
<td>Datetime object representing the article date.</td>
</tr>
<tr>
<td>date_format</td>
<td>Either default date format or locale date format.</td>
</tr>
<tr>
<td>locale_date</td>
<td>Date formatted by the date_format.</td>
</tr>
<tr>
<td><strong>template</strong></td>
<td>Template name to use for rendering.</td>
</tr>
<tr>
<td>default_template</td>
<td>Default template name.</td>
</tr>
<tr>
<td>lang</td>
<td>Language of the article.</td>
</tr>
<tr>
<td>in_default_lang</td>
<td>Boolean representing if the article is written in the default language.</td>
</tr>
<tr>
<td>metadata</td>
<td>Article header metadata dict.</td>
</tr>
<tr>
<td><strong>save_as</strong></td>
<td>Location to save the article page.</td>
</tr>
<tr>
<td>source_path</td>
<td>Full system path of the article source file.</td>
</tr>
<tr>
<td>relative_source_path</td>
<td>Relative path from PATH to the article source file.</td>
</tr>
</tbody></table>
<br>

<h3 id="author--category--tag">Author / Category / Tag</h3>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>Name of this object</td>
</tr>
<tr>
<td>page_name</td>
<td>Author page name.</td>
</tr>
<tr>
<td>save_as</td>
<td>Location to save the author page.</td>
</tr>
<tr>
<td>slug</td>
<td>Page slug.</td>
</tr>
<tr>
<td>url</td>
<td>URL to the author page.</td>
</tr>
</tbody></table>
<br>

<h2 id="14-template-extension">1.4 Template Extension</h2>
<p><code>Jinja</code>의 핵심은 <strong>상속</strong>에 있습니다. 상위 템플릿을 상속받아 하위 템플릿을 구성합니다.</p>
<h3 id="base-template">Base Template</h3>
<p><strong>base.html</strong> 파일을 생성하고, 모든 템플릿에서 이 파일을 상속받도록 했습니다.</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;{{ DEFAULT_LANG }}&quot;&gt;
&lt;head&gt;
    {% block head %}
        &lt;title&gt;{% block title %}{{ SITENAME }}{% endblock title %}&lt;/title&gt;
        {% block include %}
            {% include &quot;components/_include.html&quot; %}
        {% endblock %}
    {% endblock head %}
&lt;/head&gt;
&lt;body&gt;
{# Header #}
{% block header %}
    {% include &quot;components/_header.html&quot; %}
{% endblock %}
&lt;main&gt;
    {# Sidebar #}
    {% block sidebar %}
        {% include &quot;components/_sidebar.html&quot; %}
    {% endblock %}

    {# Content #}
    &lt;section&gt;{% block content %}{% endblock %}&lt;/section&gt;
&lt;/main&gt;

{# Footer #}
{% block footer %}
    {% include &quot;components/_footer.html&quot; %}
{% endblock %}
&lt;/body&gt;
&lt;/html&gt;</code></pre><br>

<p><code>block</code> 구문을 이용하여 위와 같이 템플릿을 구성했습니다.
아래 영역들은 어느 템플릿 엔진을 쓰던간에 제가 주로 지정하는 영역들입니다.🤗</p>
<blockquote>
<h3 id="템플릿-영역">템플릿 영역</h3>
<ul>
<li><code>head</code>: head 영역</li>
<li><code>include</code>: include 영역; static 파일 import 영역</li>
<li><code>header</code>: 헤더 영역; <code>&lt;header&gt;</code> tag</li>
<li><code>footer</code>: 푸터 영역; <code>&lt;footer&gt;</code> tag</li>
<li><code>sidebar</code>: 메뉴 링크 영역; <code>&lt;aside&gt;</code> tag</li>
<li><code>content</code>: 실제 페이지 마다 내용이 삽입되는 영역</li>
</ul>
</blockquote>
<br>

<h3 id="sub-template">Sub Template</h3>
<p>하위 템플릿 페이지에서 아래와 같이 작성합니다. 아래는 <code>authors.html</code> 템플릿 예시입니다.</p>
<pre><code class="language-html">{% extends &quot;base.html&quot; %}
{% block content %}
    &lt;ul&gt;
        {% for author, articles in authors %}
            &lt;li&gt;&lt;a href=&quot;{{ SITEURL }}/{{ author.url }}&quot;&gt;
              {{ author.name }} ({{ articles|length }})
            &lt;/a&gt;&lt;/li&gt;
        {% endfor %}
    &lt;/ul&gt;
{% endblock %}</code></pre>
<p>하위 템플릿에서 block 구문을 이용하면 간편하게 상위 템플릿인 <code>base.html</code>에서 사용한 block 영역을 대체할 수 있습니다.</p>
<hr>
<h1 id="2-template-implementation">2. Template Implementation</h1>
<p>저는 아래와 같은 페이지들을 구현했습니다.</p>
<blockquote>
<h3 id="구현한-템플릿">구현한 템플릿</h3>
<ul>
<li><code>index</code>: (필수) 메인 페이지 (포스트 목록)</li>
<li><code>article</code>: (필수) 포스트 별 내부 페이지</li>
<li><code>categories</code>: 카테고리 목록 보여주는 화면</li>
<li><code>category</code>: 카테고리 별 페이지 (포스트 목록)</li>
<li><code>tags</code>: 태그 목록 보여주는 화면</li>
<li><code>tag</code>: 태그 별 페이지 (포스트 목록)</li>
<li><code>archives</code>: date 순 포스트 목록 페이지 (포스트 목록)</li>
</ul>
</blockquote>
<p>겹치는 페이지는 <code>index</code>, <code>category</code>, <code>tag</code>, <code>archives</code>는 포스트 목록을 보여주는 페이지기 때문에 화면이 거의 유사하여 따로 설명하지 않겠습니다.</p>
<p>아래는 주요 구현한 화면들입니다.</p>
<br>

<h2 id="21-indexhtml">2.1 index.html</h2>
<p><code>index.html</code>은 블로그의 초기 페이지입니다.</p>
<p>&#39;/&#39;로 들어오는 메인페이지기 때문에 가장 핵심⭐ 페이지입니다.</p>
<p>Header, Sidebar, Content, Footer 영역을 조합하여 아래와 같이 페이지를 구성하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/1153ac9e-f963-427b-9706-cd1d5683cd36/image.png" alt="Screenshot Light Theme"></p>
<p>Header 영역의 우측 아이콘을 클릭하면 Dark 모드로도 전환할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/77f0cc75-03de-44ad-9b32-389360dac82d/image.png" alt="Screenshot Dark Theme"></p>
<br>

<h2 id="22-articlehtml">2.2 article.html</h2>
<p><code>article.html</code>은 게시글 상세 페이지입니다. <code>content</code> 폴더 밑에 작성한 게시글(<code>.rst</code>, <code>.md</code> 파일들)을 보여주는 화면입니다.</p>
<p>주요 기능은 스타일링 + 플러그인을 이용하여 아래와 같이 구현하였습니다.</p>
<blockquote>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li>게시글 상세</li>
<li>코드 블럭 (맥 스타일, Syntax Highlighting)</li>
<li>공유 기능</li>
<li>이전/다음 포스트 이동 기능</li>
<li>코멘트 기능</li>
</ul>
</blockquote>
<table>
<thead>
<tr>
<th>Light</th>
<th>Dark</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/03a76e37-2149-426b-ab14-e06f4afd0171/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/developer_khj/post/93f4d5c5-6655-4d9e-87d1-5d4c9053e2a9/image.png" alt=""></td>
</tr>
</tbody></table>
<br>

<h2 id="23-categorieshtml">2.3 categories.html</h2>
<p><code>categories.html</code>은 카테고리 목록을 보여주는 화면입니다.</p>
<p>Category 목록은 이미 사이드바에 있기 때문에 다른 화면이 필요했고, velog의 시리즈 기능을 모티브로 <code>categories</code> 화면을 구상하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/9aba980e-492f-4c76-8150-44e4a6d453a8/image.png" alt=""></p>
<br>

<h2 id="24-tagshtml">2.4 tags.html</h2>
<p><code>tags.html</code>은 태그 목록을 보여주는 화면입니다.</p>
<p>Tag 목록도 이미 사이드바에 있기 때문에, <code>Highcharts</code> 라이브러리를 이용하여 Bubble 형태의 차트로 화면을 구상하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/92f05b2c-2085-4dda-9c3a-437c19386a75/image.png" alt=""></p>
<h3 id="highcharts">Highcharts</h3>
<p>아래 코드를 참고했습니다.</p>
<p>!codepen[twinklekhj/embed/MWRrmKY?Cresult&amp;theme-id=light&amp;height=300]</p>
<hr>
<h1 id="3-custom-template">3. Custom Template</h1>
<p>블로그 테마를 만들다보니 문득 궁금해졌습니다.</p>
<blockquote>
<ul>
<li>포스트 목록외에는 경로를 내맘대로 지정할 수는 없는건가?</li>
<li>html 파일을 추가하면 나만의 홈페이지 경로를 추가할 수는 없는건가?</li>
</ul>
</blockquote>
<p>콘텐츠에 메타 정보를 이용해서 커스텀 템플릿을 만들 수 있습니다.</p>
<br>

<h2 id="31-md-파일-생성">3.1 md 파일 생성</h2>
<p>content 폴더에 md 파일을 생성하여 원하는 커스텀 정보로 수정합니다.</p>
<blockquote>
<h3 id="수정할-메타-정보">수정할 메타 정보</h3>
<ul>
<li><code>url</code>: 접근할 url</li>
<li><code>save_as</code>: content 파일 생성 경로</li>
<li><code>template</code>: 사용할 템플릿 파일</li>
</ul>
</blockquote>
<p>저는 drafts 목록을 볼수 있도록 커스텀 페이지를 추가로 만들었습니다.</p>
<pre><code class="language-md">Title: Drafts
Slug: drafts
Date: 2024-04-02
url: drafts/
Save_as: drafts/index.html
Template: custom/drafts</code></pre>
<br>

<h2 id="32-template-파일-생성">3.2 Template 파일 생성</h2>
<p>template 메타 정보에 적은 파일을 생성합니다.</p>
<pre><code class="language-html">&lt;!-- 파일 위치: templates/custom/drafts.html --&gt;
{% extends &quot;template/base.html&quot; %}
{% block title %}{{ super() }} - Drafts{% endblock %}
{% block content_title %}
    &lt;h6 class=&quot;desc&quot;&gt;Drafts&lt;/h6&gt;
    &lt;h3 class=&quot;title&quot;&gt;Posts&lt;/h3&gt;
{% endblock %}
{% block content %}
    &lt;ol class=&quot;post-list&quot;&gt;
        {% for article in drafts %}
            {{ section(article) }}
        {% endfor %}
    &lt;/ol&gt;
{% endblock content %}</code></pre>
<br>

<h2 id="33-demo">3.3 Demo</h2>
<p>이제 커스텀 url로 접근하면 렌더링한 페이지를 볼 수 있습니다.</p>
<p><a href="http://localhost:8000/drafts/">http://localhost:8000/drafts/</a></p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/c29db6f7-6b90-4617-b248-e9b7f13b2fbb/image.png" alt=""></p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://docs.getpelican.com/en/4.9.1/themes.html#article">Pelican Docs - Themes</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<h2 id="insights">Insights</h2>
<p>만들다보니 여러가지 추가적인 인사이트를 얻을 수 있었습니다.</p>
<blockquote>
<p>정적 사이트 동작원리</p>
</blockquote>
<p>당연하게도 정적 사이트 생성기 프로그램을 분석했으니, 대강의 흐름을 알 수 있었습니다.</p>
<ul>
<li>형식에 맞춰 포스팅 작성</li>
<li>빌드 &amp; 실행 &amp;배포</li>
<li>커스터마이징하는 방법</li>
</ul>
<p>아마 유사한 프로그램인 Jekyll을 파보다보면 비슷할 거 같다는 느낌을 받았습니다.😁</p>
<blockquote>
<p>RSS Feed</p>
</blockquote>
<p>RSS라는 것을 전혀 몰랐는데 사이트에 RSS 기능 추가해보고, 다른 사이트 RSS 구해보기도 하고 굉장히 신선한 기술을 알게되었습니다.</p>
<p>velog도 RSS를 지원하구요. 현재 블로그 RSS: <a href="https://api.velog.io/atom/@developer_khj/">https://api.velog.io/atom/@developer_khj/</a></p>
<blockquote>
<p>Store 기능 Vanilla로 구현하기</p>
</blockquote>
<p>theme 기능을 구현하면서 전역 상태 관리가 필요해졌습니다. 요즘에 react를 쓰면서 전역상태 관리를 <code>redux</code>로 했더니, 없이 하려니까 없인 못살겠다 싶었습니다 🤦‍♀️</p>
<p>Vailla JS에서 순수 Store를 만들면서 redux가 얼마나 편리한 라이브러리인지, 어떤 원리로 동작하는지 조금 더 깨닫게 되는 계기가 되었습니다</p>
<h2 id="conclusion">Conclusion</h2>
<p>갑자기 신기술에 꽂혀 목매고 있는 나 자신...
velog에 포스팅을 하면서 블로그를 만드는 나 자신...</p>
<p>GitHub Blog는 <strong>개발자의 숙원</strong>이라 불릴 만큼 대단한 일이라고 생각하며, 열심히 기술을 연마해보았습니다. 기술 분석에 대한 스킬이 한층 더 업그레이드 된 기분입니다.🤗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pelican] Jinja Template Engine]]></title>
            <link>https://velog.io/@developer_khj/Pelican-Make-Theme-Jinja</link>
            <guid>https://velog.io/@developer_khj/Pelican-Make-Theme-Jinja</guid>
            <pubDate>Fri, 29 Mar 2024 05:41:59 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>지난 포스팅까지는 테마를 만드는 것 대해 간단히 생각했습니다.</p>
<blockquote>
<p>레이아웃 적당한 테마를 하나 골라서 CSS만 변경하면 끝~!</p>
</blockquote>
<p>딱 원하는 구성이 있는 테마를 찾기도 어려울 뿐더러, 레이아웃 적당한 테마를 골랐더니 CSS가 엉망이라 다뜯어고치게 생겼으니😣😣</p>
<p>궁금한게 생기면 결판을 지을때까지 생각이 드는 성격이라, 결론은 <strong>테마 개발의 A-Z까지</strong> 알아야겠다는 생각이 들었습니다. 🔥🔥</p>
<p>커스텀 테마 개발에 대해 본격적으로 공부해가며 포스팅해보고자 합니다. 🤗</p>
<p>우선, Pelican은 <code>Jinja</code> Template Engine을 기반으로 작동하기 때문에 Jinja에 대해 알필요가 있습니다.</p>
<hr>
<h1 id="1-template">1. Template</h1>
<p>본격적으로 테마를 개발하기에 앞서 템플릿의 구조에 대해 살펴보겠습니다 🙌</p>
<h2 id="11-template-engine">1.1 Template Engine</h2>
<blockquote>
<h3 id="template-engine">Template Engine</h3>
<p><strong>A template processor</strong> (also known as a template engine or template parser) is <strong>software</strong> designed to <strong>combine templates with data</strong> (defined by a data model) to produce resulting documents or programs.</p>
<p>출저 - <a href="https://en.wikipedia.org/wiki/Template_processor">Wiki: Template processor</a></p>
</blockquote>
<p><code>Template Engine</code>이란, 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어입니다.</p>
<p>쉽게 말해, 웹사이트 화면을 어떤 형태로 만들지 도와주는 양식입니다.</p>
<br>

<h2 id="12-jinja">1.2 Jinja</h2>
<h3 id="definition">Definition</h3>
<blockquote>
<h3 id="jinja">Jinja</h3>
<p>Jinja is a web template engine for the Python programming language.
<img src="https://velog.velcdn.com/images/developer_khj/post/f2a13cab-749e-49f0-b9b1-545ed4238621/image.svg" alt="Jinja Logo"></p>
<p>It was created by <strong>Armin Ronacher</strong> and is licensed under a <strong>BSD</strong> License.</p>
<p>Jinja is similar to the Django template engine but provides Python-like expressions while ensuring that the templates are evaluated in a sandbox.</p>
<p>It is a text-based template language and thus can be used to generate any markup as well as source code.
출저 - <a href="https://en.wikipedia.org/wiki/Jinja_(template_engine)">Wiki: Jinja</a></p>
</blockquote>
<p><code>Jinja</code>는 <code>Python</code>언어를 위한 템플릿 엔진입니다.
Armin Ronacher가 개발, BSD 라이센스를 두고 있습니다.
파이써닉한 문법을 갖고있으며, 템플릿 언어로 마크업을 만들어냅니다.</p>
<p>Jinja에 대한 추가적인 정보는 <a href="https://jinja.palletsprojects.com/en/3.1.x/">Jinja Docs</a>에서 보실 수 있습니다.</p>
<h3 id="version">Version</h3>
<p>Pelican을 설치하면 <code>dependency</code>로 자동으로 <code>Jinja</code>가 설치됩니다.</p>
<blockquote>
<h3 id="version-info">Version Info</h3>
<ul>
<li><code>Pelican</code>: 4.9.1</li>
<li><code>Jinja</code>: 3.1.2</li>
</ul>
</blockquote>
<br>

<h1 id="2-jinja-syntax">2. Jinja Syntax</h1>
<p>Jinja의 문법을 간략하게 정리해보면 아래와 같습니다.</p>
<ul>
<li>Statements: 제어 구조, <code>{% ... %}</code></li>
<li>Expressions: 표현식, <code>{{ ... }}</code></li>
<li>Comments: 주석, <code>{# ... #}</code></li>
</ul>
<br>

<h2 id="21-statements">2.1 Statements</h2>
<p>Statements는 <strong>제어 구조</strong>를 가지는 문법입니다.</p>
<p>제어 구조에는 조건문(if), 반복문(for), 매크로(macro), 블록(block) 등 프로그램의 흐름을 제어하는 모든 것을 나타냅니다.</p>
<h3 id="syntax">syntax</h3>
<pre><code>{% ... %}</code></pre><br>

<h3 id="if">if</h3>
<p><strong>if-elif-else</strong> 예시입니다.</p>
<pre><code>{% if kenny.sick %}
    Kenny is sick.
{% elif kenny.dead %}
    You killed Kenny!  You bastard!!!
{% else %}
    Kenny looks okay --- so far
{% endif %}</code></pre><p><strong>인라인</strong> 구조도 가질 수 있습니다.</p>
<pre><code>{{ &quot;[{}]&quot;.format(page.title) if page.title }}</code></pre><br>

<h3 id="for">for</h3>
<p><strong>루프 필터링</strong>을 사용한 for문 예시입니다.</p>
<pre><code>&lt;ul&gt;
{% for user in users %}
    &lt;li&gt;{{ user.username|e }}&lt;/li&gt;
{% else %}
    &lt;li&gt;&lt;em&gt;no users found&lt;/em&gt;&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;</code></pre><br>

<h3 id="macro">macro</h3>
<p>매크로는 함수와 유사한 기능을 합니다</p>
<pre><code>{% macro input(name, value=&#39;&#39;, type=&#39;text&#39;, size=20) -%}
    &lt;input type=&quot;{{ type }}&quot; name=&quot;{{ name }}&quot; value=&quot;{{
        value|e }}&quot; size=&quot;{{ size }}&quot;&gt;
{%- endmacro %}</code></pre><pre><code>&lt;p&gt;{{ input(&#39;username&#39;) }}&lt;/p&gt;
&lt;p&gt;{{ input(&#39;password&#39;, type=&#39;password&#39;) }}&lt;/p&gt;</code></pre><br>


<h3 id="filter">filter</h3>
<p><code>filter</code> 구문을 이용하면 <strong>Jina 정규 Filter</strong>를 이용할 수 있습니다.</p>
<pre><code>{% filter upper %}
    This text becomes uppercase
{% endfilter %}</code></pre><p>혹은 <code>Expressions</code>에서 아래와 같이 사용할 수 있습니다.</p>
<pre><code>{{ name|upper }}</code></pre><br>

<h3 id="set">set</h3>
<p><code>set</code> 구문을 이용하면 변수를 할당할 수 있습니다.</p>
<pre><code>{% set navigation = [(&#39;index.html&#39;, &#39;Index&#39;), (&#39;about.html&#39;, &#39;About&#39;)] %}</code></pre><br>

<h3 id="block">block</h3>
<p><code>block</code>은 상속에 사용되며 동시에 자리 표시자 및 대체 역할을 합니다.</p>
<p><code>base.html</code>으로 템플릿을 만들고 하위 템플릿에서 상속받습니다.</p>
<p>상위 템플릿 예시</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    {% block head %}
        &lt;title&gt;{% block title %}{{ SITENAME }}{% endblock title %}&lt;/title&gt;
    {% endblock head %}
&lt;/head&gt;
&lt;body&gt;
    {% block content %}
    {% endblock %}
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>하위 템플릿 예시 - <code>categories.html</code></p>
<pre><code>{% extends &quot;template/base.html&quot; %}
{% block title %}
    {{ super() + &#39;- Categories&#39; }}
{% endblock %}

{% block content %}
    &lt;dl id=&quot;tag_list&quot;&gt;
        &lt;ul&gt;
        {% for category, articles in categories %}
            &lt;li&gt;&lt;a href=&quot;{{ SITEURL }}/{{ category.url }}&quot;&gt;{{ category }}&lt;/a&gt;&lt;/li&gt;
        {% endfor %}
        &lt;/ul&gt;
    &lt;/dl&gt;
{% endblock %}</code></pre><br>

<h3 id="include">include</h3>
<p><code>include</code>는 다른 템플릿을 렌더링하고 결과를 현재 템플릿에 출력합니다.</p>
<pre><code>{% include &#39;header.html&#39; %}
Body goes here.
{% include &#39;footer.html&#39; %}</code></pre><br>

<h2 id="22-expressions">2.2 Expressions</h2>
<p><code>Expressions</code> 문은 값을 표현하는데 사용됩니다.</p>
<h3 id="syntax-1">syntax</h3>
<pre><code>{{ ... }}</code></pre><ul>
<li>리터럴(literal) 값 - Python 객체(<code>str</code>, <code>int</code>, <code>float</code>, <code>list</code>,<code>tuple</code>, <code>dict</code>)</li>
<li>사칙 연산(<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code>, <code>*</code>, <code>**</code>)</li>
<li>비교 연산(<code>==</code>, <code>!=</code>, <code>&gt;</code>, <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</code>)</li>
<li>논리 연산(<code>and</code>, <code>or</code>, <code>not</code>)</li>
<li>기타 연산(<code>in</code>, <code>is</code>: 테스트 수행, <code>|</code>: 필터적용, <code>~</code>: 문자연결, <code>()</code>: 콜러블 호출, <code>./[]</code>: 객체 속성 가져옴,)</li>
</ul>
<br>

<h2 id="23-comments">2.3 Comments</h2>
<p>주석문을 표현하는데 사용됩니다.</p>
<pre><code>{# note: commented-out template because we no longer use this
    {% for user in users %}
        ...
    {% endfor %}
#}</code></pre><hr>
<h1 id="💎-rerferences">💎 Rerferences</h1>
<ul>
<li><a href="https://docs.getpelican.com/en/4.9.1/themes.html">Pelican Docs - Themes</a></li>
<li><a href="https://jinja.palletsprojects.com/en/3.1.x/templates/">Jinja Docs - Template</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>지금까지 <strong>Jinja Template Engine</strong>에 대해 살펴보았습니다. 확실히 문법을 알게되니 템플릿 개발에 있어 한층 이해도가 올라가는 거 같습니다. 😎</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pelican] Pelican Quick Start - GitHub Blog 만들기]]></title>
            <link>https://velog.io/@developer_khj/Pelican-Create-Blog-With-GitHub</link>
            <guid>https://velog.io/@developer_khj/Pelican-Create-Blog-With-GitHub</guid>
            <pubDate>Wed, 27 Mar 2024 19:41:14 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>코딩테스트 연습겸 <code>Python</code>을 시작하였는데, 레퍼런스를 찾다보니 <code>Pelican</code>이란 신기한걸 발견하여 츄라이~해보면서 소개해보고자 포스팅을 작성하게 되었습니다.</p>
<p>참고로, <strong>Pelican</strong>은 GitHub Pages로 블로그를 만드는 프로그램으로 유명한 <strong>Jekyll</strong>과 유사한 프로그램입니다.</p>
<blockquote>
<p>최종 목표는 coding test 코드를 작성하면 <code>blog</code>에 자동으로 포스팅을 해주는 것인데,, 가능할런지*^^* ...</p>
</blockquote>
<p>이번 포스팅에서는 <code>Pelican</code>과 <code>GitHub Pages</code>로 Blog를 만드는 방법에 대해 소개합니다. 😊</p>
<blockquote>
<h3 id="깃허브-링크">깃허브 링크</h3>
<p>혹여나 참고하실 분을 위해 깃허브 링크를 남겨두었습니다.</p>
<ul>
<li>Repository: <a href="https://github.com/hjkim1004/pelican-demo/">https://github.com/hjkim1004/pelican-demo/</a></li>
<li>GitHub Pages: <a href="https://twinklekhj.xyz/pelican-demo/">https://twinklekhj.xyz/pelican-demo/</a></li>
</ul>
</blockquote>
<hr>
<h1 id="1-pelican">1. Pelican</h1>
<h2 id="11-definition">1.1 Definition</h2>
<blockquote>
<h3 id="pelican">Pelican</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/a063975e-23c7-44b2-b9dd-17f42a95dfbd/image.png" alt="Pelican 로고">
Static site generator powered by Python
“Pelican” is an anagram of calepin, which means “notebook” in French.</p>
</blockquote>
<p><code>Pelican</code>은 <strong>Python 기반 정적 사이트 생성기</strong> 입니다.</p>
<p>왜 이름이 Pelican일까? Pelican은 프랑스어로 <code>notebook</code>을 의미한다고 합니다.
노트, 블로그 같은 느낌으로 이름을 지은것이 아닐런지요 ㅎㅎ</p>
<p><a href="https://getpelican.com/">Pelican 공식 사이트</a>를 보시면 다양한 정보를 얻을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/db6c0e2c-60c8-4acb-a9c8-e6d7055b6be0/image.gif" alt="Pelican 공식 사이트"></p>
<h2 id="12-install-dependencies">1.2 Install Dependencies</h2>
<blockquote>
<h3 id="💡-prerequisites">💡 Prerequisites</h3>
<p>pelican은 <code>python</code> 기반 프로그램이기 때문에 python이 설치되어 있어야합니다.
다음과 같은 명령이 실행되야합니다</p>
<ul>
<li><code>python</code></li>
<li><code>pip</code></li>
</ul>
</blockquote>
<pre><code class="language-shell"># 기본 설치
pip install pelican

# 추가 설치: markdown
# markdown 형식으로 글을 쓰려면 패키지를 추가로 설치해주세요.
pip install markdown

# 추가 설치: typogrify
# typographically-improved HTML 향상된 HTML을 이용하려면 추가로 설치해주세요.
pip install typogrify</code></pre>
<p>pelican을 설치하면 아래 종속 패키지가 자동으로 설치됩니다.</p>
<ul>
<li><code>feedgenerator</code>: Atom feeds 생성기</li>
<li><code>jinja2</code>: Template 엔진</li>
<li><code>pygments</code>: syntax highlighting</li>
<li><code>docutils</code>: reStructuredText as an input format</li>
<li><code>blinker</code>: an object-to-object and broadcast signaling system</li>
<li><code>unidecode</code>: for ASCII transliterations of Unicode text utilities</li>
<li><code>MarkupSafe</code>: for a markup-safe string implementation</li>
<li><code>python-dateutil</code>: date metadata 해석</li>
</ul>
<br>

<h2 id="13-new-project">1.3 New Project</h2>
<p><code>pelican-quickstart</code> 명령어를 통해 <strong>skeleton 프로젝트</strong>를 생성할 수 있습니다.</p>
<pre><code class="language-shell">mkdir [projectname]
cd [projectname]

pelican-quickstart</code></pre>
<br>

<p>저는 <code>pelican-demo</code>이라는 프로젝트를 생성하였습니다.</p>
<pre><code class="language-shell">mkdir pelican-demo
cd pelican-demo
pelican-quickstart</code></pre>
<pre><code class="language-shell">pelican-quickstart
Welcome to pelican-quickstart v4.9.1.

This script will help you create a new Pelican-based website.

Please answer the following questions so this script can generate the files
needed by Pelican.


&gt; Where do you want to create your new web site? [.]
&gt; What will be the title of this web site? Pelican Demo
&gt; Who will be the author of this web site? Heejeong Kim
&gt; What will be the default language of this web site? [Korean]
&gt; Do you want to specify a URL prefix? e.g., https://example.com   (Y/n) n
&gt; Do you want to enable article pagination? (Y/n) y
&gt; How many articles per page do you want? [10]
&gt; What is your time zone? [Europe/Rome]
&gt; Do you want to generate a tasks.py/Makefile to automate generation and publishing? (Y/n) y
&gt; Do you want to upload your website using FTP? (y/N) n
&gt; Do you want to upload your website using SSH? (y/N) n
&gt; Do you want to upload your website using Dropbox? (y/N) n
&gt; Do you want to upload your website using S3? (y/N) n
&gt; Do you want to upload your website using Rackspace Cloud Files? (y/N) n
&gt; Do you want to upload your website using GitHub Pages? (y/N) y
&gt; Is this your personal page (username.github.io)? (y/N) n
Done. Your new project is available at D:\Git\pelican-demo</code></pre>
<p>아래와 같은 스크립트가 실행되면서 <strong>Pelican 프로젝트</strong>가 생성됩니다.</p>
<br>

<h2 id="14-pelican-structure">1.4 Pelican Structure</h2>
<p><code>pelican-quickstart</code>로 프로젝트를 생성하면 다음과 같은 구조로 생성됩니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/1bc15afb-8dc1-45d9-bd21-8e93f11e88bf/image.png" alt="Pelican 파일 구조"></p>
<pre><code class="language-shell">yourproject/
├── content                # 포스팅 디렉터리
│   └── (pages)
├── output                # build 디렉터리
├── tasks.py            # 실행 관련 설정 파일
├── Makefile            # build 파일
├── pelicanconf.py        # 주요 환경 설정 파일
└── publishconf.py        # Settings to use when ready to publish</code></pre>
<br>

<h2 id="15-build--run">1.5 Build &amp; Run</h2>
<h3 id="build">build</h3>
<p>아래 명령어로 pelican 홈페이지를 빌드할 수 있습니다.</p>
<pre><code class="language-shell">pelican content</code></pre>
<br>

<h3 id="run">run</h3>
<p>아래 명령어로 서버를 실행할 수 있습니다.</p>
<pre><code class="language-shell">pelican --listen</code></pre>
<p>기본 포트는 <code>8000</code>번으로, <code>tasks.py</code>에서 수정할 수 있습니다. <a href="http://localhost:8000">http://localhost:8000</a> 에서 접속하실 수 있습니다.</p>
<p>초기 프로젝트 실행화면은 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b0eb4f4e-0688-40fb-a96a-48df222ad7bc/image.png" alt="pelican 실행 화면"></p>
<hr>
<h1 id="2-write-content">2. Write Content</h1>
<p>Pelican은 기본적으로 HTML 태그 지원을 활성화하기 위해 <code>reStructuredText</code>에 대한 확장을 구현합니다. 하지만 <strong>markdown 패키지를 설치</strong>하면 markdown 형식으로 포스팅할 수 있습니다.</p>
<p>하지만 저는 markdown을 이용할 것입니다🤣</p>
<br>

<h2 id="21-sample">2.1 Sample</h2>
<p>Markdown 게시물의 메타데이터 구문은 다음 패턴을 따라야 합니다.</p>
<pre><code class="language-md">Title: My super title
Date: 2024-03-27 22:14
Modified: 2024-03-27 22:14
Category: Pelican
Tags: python, pelican
Slug: create pelican site
Authors: Heejeong Kim
Summary: Pelican Posts

This is the content of my super blog post.</code></pre>
<p>Pelican은 Markdown에 기재한 메타데이터와 본문으로 HTML을 생성합니다.</p>
<pre><code class="language-html">&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;My super title&lt;/title&gt;
        &lt;meta name=&quot;tags&quot; content=&quot;thats, awesome&quot; /&gt;
        &lt;meta name=&quot;date&quot; content=&quot;2024-03-27 20:14&quot; /&gt;
        &lt;meta name=&quot;modified&quot; content=&quot;2024-03-27 20:14&quot; /&gt;
        &lt;meta name=&quot;category&quot; content=&quot;yeah&quot; /&gt;
        &lt;meta name=&quot;authors&quot; content=&quot;Alexis Métaireau, Conan Doyle&quot; /&gt;
        &lt;meta name=&quot;summary&quot; content=&quot;Short version for index and feeds&quot; /&gt;
    &lt;/head&gt;
    &lt;body&gt;
        This is the content of my super blog post.
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<h2 id="22-metadata">2.2 Metadata</h2>
<p>Meta Data 목록은 다음과 같습니다.</p>
<table width="100%">
<thead>
<tr><th><p>Metadata</p></th>
<th><p>Description</p></th>
</tr>
</thead>
<tbody>
<tr>
  <td>title</td>
  <td>Title of the article or page</td>
</tr>
<tr>
  <td>date</td>
  <td>Publication date (e.g., YYYY-MM-DD HH:SS)</td>
</tr>
<tr>
  <td>modified</td>
  <td>Modification date (e.g., YYYY-MM-DD HH:SS)</td>
</tr>
<tr>
  <td>tags</td>
  <td>Content tags, separated by commas</td>
</tr>
<tr>
  <td>keywords</td>
  <td>Content keywords, separated by commas (HTML content only)</td>
</tr>
<tr>
  <td>category</td>
  <td>Content category (one only — not multiple)</td>
</tr>
<tr>
  <td>slug</td>
  <td>Identifier used in URLs and translations</td>
</tr>
<tr>
  <td>author</td>
  <td>Content author, when there is only one</td>
</tr>
<tr>
  <td>authors</td>
  <td>Content authors, when there are multiple</td>
</tr>
<tr>
  <td>summary</td>
  <td>Brief description of content for index page</td>
</tr>
<tr>
  <td>lang</td>
  <td>Content language ID (en, fr, etc.)</td>
</tr>
<tr>
  <td>translation</td>
  <td>If content is a translation of another (true or false)</td>
</tr>
<tr>
  <td>status</td>
  <td>Content status: draft, hidden, or published</td>
</tr>
<tr>
  <td>template</td>
  <td>Name of template to use to generate content (without extension)</td>
</tr>
<tr>
  <td>save_as</td>
  <td>Save content to this relative file path</td>
</tr>
<tr>
  <td>url</td>
  <td>URL to use for this article/page</td>
</tr>
</tbody>
</table>

<br>

<h2 id="23-link-static-files">2.3 Link static files</h2>
<p>블로그 글에는 역시 사진이 같이 있어야 좋겠죠? 하지만 이미지를 사용하려면 경로 문제가 생기기 때문에 추가적으로 설정해주어야 합니다.</p>
<h3 id="settings">Settings</h3>
<p><code>pelicanconf.py</code> 파일에 아래 항목을 붙여넣습니다.</p>
<pre><code class="language-python"># 파일 위치: pelicanconf.py

# path-specific metadata
EXTRA_PATH_METADATA = {
    &quot;extra/robots.txt&quot;: {&quot;path&quot;: &quot;robots.txt&quot;},
}

# static paths will be copied without parsing their contents
STATIC_PATHS = [
    &quot;images&quot;,
    &quot;extra/robots.txt&quot;,
]</code></pre>
<p><code>content</code> 폴더에 <code>images</code>와 <code>extra</code> 폴더를 생성합니다.</p>
<h3 id="usage">Usage</h3>
<p><code>md 파일</code> 내부에서 아래와 같이 사용합니다.</p>
<pre><code class="language-md">![Pelican Logo]({static}/path/to/image.png)</code></pre>
<hr>
<h1 id="3-github-pages">3. GitHub Pages</h1>
<h2 id="31-repository-settings">3.1 Repository Settings</h2>
<h3 id="create-repository">Create Repository</h3>
<p><code>Pages</code>로 올릴 <code>Repository</code>를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/7ac8795a-0075-42bf-bbc2-7d1ac067042f/image.png" alt="Repository 생성"></p>
<br>

<h3 id="workflow-key-세팅">workflow key 세팅</h3>
<p><code>Repository Secrets</code> 세팅에서 workflow에서 사용할 키를 등록해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/671fa042-2912-4f57-9e97-f0c4287091e5/image.png" alt=""></p>
<br>


<h2 id="32-cicd-with-github-actions">3.2 CI/CD with GitHub Actions</h2>
<p><strong>GitHub Actions Marketplace</strong>에 등록된 <code>Pelican Action</code>를 이용하여 CI/CD를 구축했습니다.</p>
<blockquote>
<h3 id="github-pages-pelican-build-action">GitHub Pages Pelican Build Action</h3>
<p><a href="https://github.com/marketplace/actions/github-pages-pelican-build-action?version=0.2.0">https://github.com/marketplace/actions/github-pages-pelican-build-action?version=0.2.0</a></p>
<ul>
<li>action version: 0.2.0</li>
<li>python version: <strong>python:3.12-slim</strong></li>
</ul>
</blockquote>
<br>

<h3 id="requirementstxt-생성">requirements.txt 생성</h3>
<pre><code class="language-shell">pelican
Markdown
typogrify</code></pre>
<p><code>requirements.txt</code> 파일을 생성하고, 다운로드할 라이브러리들을 적어줍니다.</p>
<pre><code class="language-shell">pelican===4.9.1</code></pre>
<p>위와 같이 버전도 명시할 수 있습니다.</p>
<br>

<h3 id="workflow-생성">Workflow 생성</h3>
<p>프로젝트 안에 <code>.github/workflows</code> 폴더를 만들고, <code>pelican.yml</code> 파일을 생성해주고 아래 내용을 복사해 붙여넣습니다. (파일명은 변경하셔도 상관없습니다.)</p>
<pre><code class="language-yml"># 파일 위치: .github/workflows/pelican.yml

name: Pelican CI/CD

on:
  # Trigger the workflow on push or pull request,
  # but only for the master branch
  push:
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - uses: nelsonjchen/gh-pages-pelican-action@0.2.0
      env:
        # GH_PAGES_BRANCH: gh-pages
        # GH_PAGES_CNAME: custom domain
        # PELICAN_CONFIG_FILE: pelicanconf.py
        PELICAN_CONFIG_FILE: publishconf.py
        # PELICAN_CONTENT_FOLDER: content
        # PELICAN_THEME_FOLDER: package.json
        # GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
        GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_TOKEN }}</code></pre>
<br>

<h3 id="additional-setting">Additional Setting</h3>
<p>추가적으로 환경설정을 해주었습니다.</p>
<h4 id="1-siteurl">1. SITEURL</h4>
<pre><code class="language-python"># 파일명: publishconf.py
SITEURL = &quot;https://twinklekhj.xyz/coding/&quot;</code></pre>
<p>저는 기본 <code>[username].github.io</code>가 아닌 레포 이름으로 추가할 거라 publishconf.py 파일에 <code>SITEURL</code> 항목을 추가해주었습니다.</p>
<h4 id="2-timezone">2. TIMEZONE</h4>
<pre><code class="language-python"># 파일명: pelicanconf.py
TIMEZONE = &#39;Asia/Seoul&#39;</code></pre>
<p>프로젝트를 처음 생성하면 <code>TIMEZONE</code>이 <code>Europe/Rome</code>으로 생성되는데, 한국으로 변경해주었습니다.</p>
<h4 id="3-default_lang">3. DEFAULT_LANG</h4>
<pre><code class="language-python"># 파일명: pelicanconf.py
DEFAULT_LANG = &#39;en&#39;</code></pre>
<p>프로젝트를 처음 생성하면 <code>DEFAULT_LANG</code>이 <code>Korea</code>로 생성되는데, 콘솔에서 자꾸 WARNING이 떠서 <code>en</code>으로 변경해주었습니다. (기본 언어를 한국으로 하실 분은 <code>ko</code>로 변경하시면 됩니다.)</p>
<br>

<h2 id="33-upload-project">3.3 Upload Project</h2>
<p>만든 프로젝트를 올려줍니다.</p>
<blockquote>
<h3 id="💡-주의">💡 주의</h3>
<p>content 폴더 아래의 하나의 포스팅이 꼭 있어야합니다.</p>
</blockquote>
<pre><code class="language-shell"># 깃 세팅 방법
git init
git add README.md
git commit -m &quot;first commit&quot;
git branch -M master
git remote add origin [깃 주소]
git push -u origin master</code></pre>
<p>프로젝트 안에 <code>workflow</code> 가 있으니 프로젝트를 올리면 <code>Action</code>이 자동으로 수행됩니다. </p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/cd552730-cb24-4e67-a251-e4fd922a59dc/image.png" alt="Pages 세팅"></p>
<p><code>GitHub Pages</code> 를 세팅해준적이 없는데, 알아서 세팅이 되었습니다.🙌
친절한 깃허브씨..😍</p>
<br>

<h2 id="34-demo-page">3.4 Demo Page</h2>
<p><code>workflow</code>가 잘 실행되어 아래와 같은 화면을 띄웠습니다. 오예~~!!😎</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/59bb75d1-09b4-4306-a10e-36c0181ab253/image.png" alt="GitHub Pages"></p>
<p>혹여나 참고하실 분을 위해 상단에 깃허브 링크를 남겨두었습니다.</p>
<br>

<h2 id="35-trouble-shooting">3.5 Trouble Shooting</h2>
<p>환경세팅을 따라한답시고, 열심히 했는데 약 3시간동안 지속된 <code>path</code> 에러로 골머리를 썩었습니다. 혹시나 저와 같은 문제를 겪으실 까봐 공유합니다.😢</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f7d06f05-cc9b-40a0-a2bf-e139fd6f7c80/image.png" alt=""></p>
<pre><code class="language-shell">CRITICAL Exception: You need to specify a path __init__.py:666
containing the content (see pelican --help for more information)</code></pre>
<blockquote>
<p>역시나 프로그램은 거짓말을 하지 않더군요
무엇이 이상한가 싶으면 내 코드가 문제니라.. 🤦‍♀️</p>
</blockquote>
<p>튜토리얼을 따라해 <code>content</code> 폴더에 아무것도 없어서 문제가 발생한 것이었습니다.
<del>이 짓을 3시간이나 했다니...</del> 타오른 시간🔥🤗</p>
<hr>
<h1 id="4-pelican-theme">4. Pelican Theme</h1>
<h2 id="41-official-theme">4.1 Official Theme</h2>
<p>아무래도 기본적으로 제공해주는 화면은 매우 매우 불편합니다.
그래서 테마를 적용해보려고 합니다.😊</p>
<p>펠리칸 공식 테마는 아래에서 찾을 수 있습니다.</p>
<ul>
<li>Pelican 테마 공식 홈페이지: <a href="https://github.com/getpelican/pelican-themes">https://github.com/getpelican/pelican-themes</a></li>
<li>Preview 페이지: <a href="https://pelicanthemes.com/">https://pelicanthemes.com/</a></li>
</ul>
<br>

<h2 id="42-apply-theme">4.2 Apply Theme</h2>
<p>저는 <code>SVBTLE</code> 테마를 다운받아 적용하였습니다.
⇒ Link: <a href="https://github.com/wting/pelican-svbtle">https://github.com/wting/pelican-svbtle</a></p>
<h3 id="theme-추가">theme 추가</h3>
<p><code>themes</code> 폴더를 하나 생성하고, 폴더 밑에 다운받은 테마를 추가합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/df684a4a-f5cb-4413-ae8f-d1a3853c2936/image.png" alt="디렉터리 구조"></p>
<h3 id="pelicanconfpy-수정">pelicanconf.py 수정</h3>
<p><code>pelicanconf.py</code> 파일에서 <code>THEME</code>의 경로와 <code>THEME_STATIC_PATHS</code> 경로를 지정해줍니다.</p>
<pre><code class="language-python"># THEME settings
# THEME = &#39;path/to/theme&#39;
THEME = &quot;themes/svbtle&quot;
THEME_STATIC_DIR = &#39;theme&#39;</code></pre>
<br>

<h2 id="43-demo-page">4.3 Demo Page</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/0cbbb5cd-1884-46a0-b182-1c813d7b4df7/image.png" alt="데모 페이지"></p>
<p>짜잔~! 완성-⭐
테마까지 완벽히 적용되었습니다.💖
감격의 눈물이😂😂</p>
<br>

<h2 id="44-custom-theme">4.4 Custom Theme</h2>
<p><a href="https://docs.getpelican.com/en/4.9.1/themes.html">Pelican 공식 문서</a>를 참고하시면 나만의 테마를 개발할 수 있습니다. 하지만 그것은 FM! 정석!</p>
<p>쉬운 방법은 그냥 레이아웃 적당한 테마를 하나 골라서 CSS만 변경하면 끝~! (그래도 필요한 변수들은 공식 문서에서 찾을 수 있습니다)</p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://docs.getpelican.com/en/4.9.1/index.html">Pelican Docs</a></li>
<li><a href="https://github.com/wting/pelican-svbtle">Pelican Theme - svbtle</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
<p>약 12시간동안 Pelican 이란 것에 꽂혀 주구장창 하다보니 어느새 숙달되어버려서 Pelican이란 단어가 귀여워보이네요🤣</p>
<p>다음은 테마 개발을 해볼까합니다🤗🤗
모두 해피 코딩하세요.💖</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Portfolio] GitHub Pages Custom Domain with Gabia ]]></title>
            <link>https://velog.io/@developer_khj/Portfolio-GitHub-Pages-Custom-Domain-with-Gabia</link>
            <guid>https://velog.io/@developer_khj/Portfolio-GitHub-Pages-Custom-Domain-with-Gabia</guid>
            <pubDate>Mon, 25 Mar 2024 10:54:21 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>혼자 <strong>나만의 페이지 만들기</strong> 프로젝트를 하기 위해 <code>도메인</code>을 사놓고 약 1년을 방치해두었습니다. 🤣 방치되어 있는 도메인을 적극 활용하기 위해 GitHub Pages에 연결해보려고 합니다.</p>
<p>이번 포스팅에서는 만든 <code>GitHub Pages</code>에 커스텀 도메인을 연결하는 방법을 소개합니다.😊</p>
<hr>
<h1 id="1-도메인이란">1. 도메인이란?</h1>
<h2 id="11-도메인-관련-용어">1.1 도메인 관련 용어</h2>
<h3 id="도메인domain">도메인(Domain)</h3>
<p><code>도메인</code>은 인터넷에 연결된 컴퓨터의 IP를 사람이 기억하기 어렵기 때문에 이를 위해서 <strong>각 IP에 사람이 기억하기 쉬운 별칭을 붙인 것</strong>입니다.</p>
<br>

<h3 id="도메인-체계">도메인 체계</h3>
<p>도메인은 “.”또는 루트(root)라 불리는 도메인 이하에 아래 그림과 같이 <strong>역트리(Inverted tree)구조</strong>로 구성되어 있으며, 아래와 같이 <strong>3단계</strong>로 구분됩니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/a7205787-c8ff-4fa4-bfe1-7271c4f4f95d/image.png" alt="도메인 체계"></p>
<ul>
<li>1단계: 1단계 도메인 또는 최상위 도메인(<strong>TLD</strong>: Top Level Domain)</li>
<li>2단계: 2단계 도메인(<strong>SLD</strong>: Second Level Domain)</li>
</ul>
<br>

<h3 id="도메인-종류">도메인 종류</h3>
<p>도메인의 종류에는 <code>일반 도메인(gTLD)</code>과 <code>국가 도메인(ccTLD)</code>이 있습니다.</p>
<table>
  <thead>
    <tr>
<th>&nbsp;</th>
<th>gTLD</th>
<th>ccTLD</th>
<th>New gTLD</th>
</tr>
  </thead>
<tbody>

<tr>
<th style="width: 80px">정의</th>
<td>일반 최상위 도메인</td>
<td>국가 코드 최상위 도메인</td>
<td>신규 일반 최상위 도메인</td>
</tr>
<tr>
<th>설명</th>
<td>전세계 누구나 사용 가능</td>
<td>국가/지역 등을 나타냄 <br>해당 국가, 지역에 거주하는 단체나 개인만이 취득 가능</td>
<td>기존 gTLD의 수량적 한계로 새로운 gTLD를 만들어냄</td>
</tr>
<tr>
<th>예시</th>
<td>com, net, org, biz, info, name, asia, jobs, mobi, tel, travel, xxx</td>
<td>kr, jp, cn, in, mx, us, de, tv, me</td>
<td>email, coffee, camera, rent, website, xyz, news, blackfriday, loan</td>
</tr>
</tbody>
</table>



<br>


<h3 id="네임서버dns">네임서버(DNS)</h3>
<p><code>네임서버(DNS: Domain Name Server)</code>는 대표적으로 IP 주소와 도메인 주소를 연결해주는 역할을 합니다.</p>
<p>인터넷 주소창에 도메인을 입력할 때, 도메인 등록시 지정된 네임서버를 통해 해당 도메인과 연결된 IP주소를 확인하여 연결하게 됩니다.</p>
<p>따라서 도메인 등록시에 네임서버를 지정하고, 해당 네임서버에 연결 설정을 해야 정상 이용하 가능합니다.</p>
<br>

<h2 id="12-도메인-구매-업체-선정">1.2 도메인 구매 업체 선정</h2>
<p>도메인을 연결하려면 먼저 도메인이 있어야겠죠?
도메인은 전문 업체를 통해 구매할 수 있습니다.</p>
<p>도메인 전문 업체를 선정할 때는 도메인 가격, 도메인 등록업체의 안정성 및 편의성, 편의성 및 부가서비스 등등 여러가지를 고려해야합니다.</p>
<p><a href="https://krnic.or.kr/jsp/popup/agencyFeePop.jsp">한국인터넷진흥원 공인 등록대행사</a>에 등록된 업체가 안정성으로 좋다고 하네요.</p>
<blockquote>
<p>너무나 많은 고려사항이 있는데, 아래 블로그에 잘 정리되어 있어 확인해보시면 좋을 것같습니다.
참조 - <a href="https://haepos.com/entry/%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B9%84%EA%B5%90-%EC%B6%94%EC%B2%9C-%ED%98%B8%EC%8A%A4%ED%8C%85">도메인 비교 및 추천, 구매 전 고려사항(호스팅 등)</a></p>
</blockquote>
<p>Top 2를 꼽자면, <strong>호스팅케이알</strong>과 <strong>가비아</strong>가 가장 인기있는데요. 더이상 고민하는건 머리가 아프니... 가장 많이 들어보고 규모가 크다고 생각되는 가비아로 선정했습니다.</p>
<hr>
<h1 id="2-가비아-gabia">2. 가비아 (Gabia)</h1>
<h2 id="21-도메인-구매">2.1 도메인 구매</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/194c552d-a3bc-418c-a83a-784ce47b6a56/image.png" alt=""></p>
<p>가비아 홈페이지에 들어가서 원하는 <strong>도메인명</strong>을 입력합니다.
도메인을 클릭하여 신청하기를 클릭하면 구매 홈페이지로 이동합니다.</p>
<br>

<h2 id="22-도메인-연결">2.2 도메인 연결</h2>
<h3 id="dns-관리">DNS 관리</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/434041b5-9f37-4e2c-bbd6-393cd0ed6018/image.png" alt="마이페이지"></p>
<p><code>My 페이지</code>에서 <code>DNS 관리툴</code> 버튼을 클릭하시면 <strong>DNS 관리 사이트</strong>로 이동합니다.</p>
<br>

<h3 id="도메인-설정">도메인 설정</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/09bc2584-3a38-4e4a-867c-9bf6d4d23807/image.png" alt="DNS 설정"></p>
<p>DNS 설정에서 <strong>도메인 설정</strong>을 눌러주세요</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/257d1941-3868-430b-9702-5d94caaec635/image.png" alt="DNS 관리"></p>
<p>DNS 설정 수정하여 아래 레코드를 추가해줍니다.</p>
<ul>
<li><code>A 레코드</code>를 만들어 아래 <strong>IP 목록</strong>을 추가해주세요.</li>
<li><code>CNAME 레코드</code>를 만들어 <strong>github 주소</strong>를 추가해주세요.</li>
</ul>
<pre><code>185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153</code></pre><table>
  <thead>
    <tr>
      <th scope="col">시나리오</th>
      <th scope="col">DNS 레코드 종류</th>
      <th scope="col">DNS 레코드 이름</th>
      <th scope="col">DNS 레코드 값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>apex 도메인<br>(<code>example.com</code>)</td>
      <td><code>A</code></td><td><code>@</code></td>
      <td><code>185.199.108.153</code><br><code>185.199.109.153</code><br><code>185.199.110.153</code><br><code>185.199.111.153</code></td>
    </tr>
    <tr>
      <td>하위 도메인<br>(<code>www.example.com</code>,<br><code>blog.example.com</code>)</td>
      <td><code>CNAME</code></td>
      <td><code>SUBDOMAIN.example.com.</code></td>
      <td><code>USERNAME.github.io</code> 또는<br> <code>ORGANIZATION.github.io</code></td>
    </tr>
  </tbody>
</table>

<p>만약, 접속이 안된다면 GitHub Pages IP가 변경되었을 수 있습니다.
<a href="https://docs.github.com/ko/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site">GitHub Docs</a>에서 IP정소를 확인할 수 있습니다.</p>
<hr>
<h1 id="3-github-세팅">3. GitHub 세팅</h1>
<p>레포지토리 세팅에 들어가서 <code>Pages</code> 메뉴를 엽니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/7df75bd6-bc92-4d3d-83cf-8231c1acbf95/image.png" alt="github 레포지토리 세팅"></p>
<p><code>Custom domain</code> 입력란에 구매한 도메인을 입력해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/845dbf46-2235-40bf-85d4-ead9c2cea0c7/image.png" alt="custom domain 입력"></p>
<p>도메인을 클릭하면, 자동으로 <code>DNS check</code> 프로세스가 진행됩니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/cbf775cb-a4f7-4d8d-ab77-99f8898dbc35/image.png" alt="DNS check"></p>
<p>설정이 완료되었다면, 위와 같이 성공 메시지가 나타납니다.
이제 기존 <code>[username].github.io</code>로 접속하던 url을 커스텀 도메인으로 접속할 수 있습니다.</p>
<ul>
<li><a href="https://hjkim1004.github.io">https://hjkim1004.github.io</a></li>
<li><a href="https://twinklekhj.xyz">https://twinklekhj.xyz</a></li>
</ul>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/11fa7398-a090-473f-9c31-76ddaa39aaed/image.png" alt="포트폴리오 사이트"></p>
<p>완성-⭐</p>
<hr>
<h1 id="4-gh-pages-추가-설정">4. gh-pages 추가 설정</h1>
<h2 id="41-문제-발생-🤢">4.1 문제 발생 🤢</h2>
<p><code>GitHub Actions</code>으로 빌드배포 자동화를 구성했는데, 푸쉬했더니 커스텀 도메인이 풀리는 현상이 발견되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b353d8f6-9341-4d9d-b763-4d2f6bc8a57e/image.png" alt="GitHub Actions 화면"></p>
<p>위와 같이 <code>workflow</code>는 성공적으로 마쳤지만, 실제 사이트에 접속해보면 아래와 같이 <code>404</code> 페이지가 뜨는 것을 볼수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/fe88b4fa-7e13-4c00-aeee-5a53f27a48e4/image.png" alt="404 페이지"></p>
<p>그렇다고 CI/CD 구축해놓고 배포할 때마다 <code>Custom Domain</code>을 설정해주는 것은 용납못함.😣</p>
<br>

<h2 id="42-해결방안">4.2 해결방안</h2>
<h3 id="build-option-수정">build option 수정</h3>
<pre><code class="language-js">// 파일 위치: package.json

&quot;scripts&quot;: {
    &quot;deploy&quot;: &quot;echo &#39;도메인주소&#39; &gt; ./build/CNAME &amp;&amp; gh-pages -d build&quot;,
}</code></pre>
<p>서칭 결과 빌드 옵션을 수정하면 된다고 하네요.
하지만 뭔가 하드 코딩인거 같아서 좀더 서칭해봤습니다.</p>
<br>


<h3 id="cname-webpack-plugin-플러그인-설치">cname-webpack-plugin 플러그인 설치</h3>
<p>webpack을 사용한다면, <code>cname-webpack-plugin</code>을 설치하면 깔끔하게 해결됩니다.</p>
<pre><code class="language-shell">npm i cname-webpack-plugin -D</code></pre>
<pre><code class="language-js">// 파일 위치: webpack.config.js
const CnameWebpackPlugin = require(&#39;cname-webpack-plugin&#39;);

plugins: [
    new CnameWebpackPlugin({
        domain: &#39;도메인명&#39;,
        // domain: &#39;twinklekhj.xyz&#39;,
    }),
]</code></pre>
<br>

<h2 id="43-원리-분석">4.3 원리 분석</h2>
<blockquote>
<p>도메인을 찾는 기준이 무엇인가?</p>
</blockquote>
<p><code>CNAME</code> 파일을 만드는 것입니다.</p>
<pre><code class="language-text">// 파일명: CNAME

twinkle.xyz</code></pre>
<p>확장자가 없는 파일로, CNAME 파일을 생성하여 안에 도메인명을 붙여넣습니다.</p>
<br>

<h2 id="44-결과">4.4 결과</h2>
<p><code>gh-pages</code> workflow 결과 확인시, 도메인 네임이 추가된 것을 볼수 있었습니다.🤗🔥
<img src="https://velog.velcdn.com/images/developer_khj/post/578b6eea-f3cc-4a85-a52d-c28b3cfcdf2c/image.png" alt="gh-pages workflow 결과"></p>
<p>이슈 트래킹까지 완벽🙆‍♀️</p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li>한국 인터넷정보센터 - <a href="https://xn--3e0bx5euxnjje69i70af08bea817g.xn--3e0b707e/jsp/resources/domainInfo/domainInfo.jsp">도메인이란?</a></li>
<li>GitHub Docs - <a href="https://docs.github.com/ko/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site">GitHub Pages 사이트의 사용자 지정 도메인 관리</a></li>
<li><a href="https://wallel.com/github-pages-%EB%B0%B0%ED%8F%AC-%EC%8B%9C-custom-domain%EC%9D%B4-%EC%B4%88%EA%B8%B0%ED%99%94-%EB%90%A0-%EB%95%8C/">GitHub Pages 배포 시 Custom Domain이 초기화 될 때</a></li>
<li><a href="https://github.com/lozinsky/cname-webpack-plugin">cname-webpack-plugin</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Spring Boot 프로젝트 세팅하기 with Spring Boot 3]]></title>
            <link>https://velog.io/@developer_khj/Spring-Boot-New-Project</link>
            <guid>https://velog.io/@developer_khj/Spring-Boot-New-Project</guid>
            <pubDate>Fri, 22 Mar 2024 08:39:55 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 Spring Initializer를 통해 새 프로젝트를 생성하면서 프로젝트를 세팅하는 과정을 공유해보고자 합니다.</p>
<hr>
<h1 id="1-프로젝트-생성">1. 프로젝트 생성</h1>
<h2 id="11-새-프로젝트">1.1 새 프로젝트</h2>
<p><code>Spring Initializer</code>를 통해 새 프로젝트를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/85ea8945-95a4-4c39-9eb9-bdc5c8c2b77b/image.png" alt=""></p>
<p>이 때 우리는 또 한번의 선택을 거쳐야 하는데요, 바로 프로젝트 <strong>기술 환경 설정</strong>입니다.
프로젝트 이름부터 사용할 언어, 빌드 툴, JDK 버전, 패키징 타입까지 선택해줍니다.</p>
<blockquote>
<h3 id="💡-check-list">💡 Check List</h3>
<ul>
<li><code>Project Name</code>, <code>Group</code>, <code>Artifact</code></li>
<li>Language: <code>Java</code> or <code>Kotlin</code> or ex</li>
<li>JDK, Java Version: <code>8</code> or <code>11</code> or <code>17</code></li>
<li>Build Tool: <code>Gradle</code> or <code>Maven</code></li>
<li>Packing: <code>jar</code> or <code>war</code></li>
<li><code>Spring Boot</code> version</li>
</ul>
</blockquote>
<p>블로그 작성용 프로젝트이지만 나름 기술 선택에 있어 의의가 있습니다.</p>
<ul>
<li><code>project 이름</code>: Test용이라 spring이 정해준 대로 (demo...ㅋ)</li>
<li><code>group id</code>: github에 작성용으로 올릴거라 내 아이디 경로로</li>
<li><code>language</code>: 아직 코푸링을 할 자신이 없어 <code>Java</code> 선택</li>
<li><code>Build Tool</code>: <code>Gradle</code>, <code>Maven</code> 둘다 써봤지만 느낀점: <code>Gradle</code>이 레퍼런스가 더 많고, Maven은 익숙. 보통 <code>Maven</code> 선택, 이번엔 <code>Gradle</code> 선택해봄</li>
<li><code>Java Version</code>: 주로 Java 8 버전을 선택했지만, 신기술 도입(공부)를 위해 <code>17</code>로 선택</li>
<li><code>Packing</code>: Spring Boot는 내장 톰캣이기 때문에 주로 <code>jar</code>로 패키징 하는 편</li>
</ul>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/fd79c5cf-c9a9-4596-b415-bc486fd3601b/image.png" alt=""></p>
<p><code>Spring Boot</code> 버전을 선택하고, 사용할 라이브러리들(dependencies)을 선택합니다.</p>
<blockquote>
<h3 id="💡-spring-boot-version">💡 Spring Boot Version</h3>
<p>Spring Boot 버전을 선택할 때는 신중히 선택해야합니다.</p>
<ul>
<li>Java 버전에 따라 가용한 Boot 버전</li>
<li>Boot 버전에 따라 관련 라이브러리들 자동 다운로드, 기존에 돌아가던 코드 안돌아갈 수 있음</li>
</ul>
<p>가장 큰 예시로는 Spring Boot 2 =&gt; 3 Migration시 발생하는 문제들입니다.</p>
<ul>
<li><code>javax 패키지</code> ⇒ <code>jarkata 패키지</code></li>
<li><code>Spring Security 5</code> ⇒ <code>Spring Security 6</code></li>
</ul>
</blockquote>
<br>

<h2 id="12-기술-고민에-관한-고찰">1.2 기술 고민에 관한 고찰</h2>
<p>직접 기술을 선택해보지 않았다면 기술을 선택하면서 <strong>기존에 쓰던데로 쓰면 되지, 이걸 왜 고민해?</strong> 하실 수는 있겠지만, 이는 새로운 기술을 도입(도전)하느냐 마느냐의 차이입니다.</p>
<ul>
<li>기존에 <code>Java-Spring</code> 스택을 <code>Kotlin-Spring</code>으로 변경해볼까?</li>
<li>기존에 <code>Java 8</code>만 썼는데, <code>17</code>로 해볼까?</li>
<li>기존에 <code>Maven</code>만 고집했는데, 요즘에 <code>Gradle</code>을 많이 쓴다는데 써볼까?</li>
</ul>
<p>많은 회사 혹은 개발자들은 이런 고민을 하죠</p>
<ul>
<li>요기요 - <a href="https://techblog.yogiyo.co.kr/%EC%9A%94%EA%B8%B0%EC%9A%94%EB%8A%94-kotlin%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%96%88%EB%82%98-3c95ba25382c">요기요는 Kotlin을 어떻게 적용했나?</a></li>
<li>배달의 민족 - <a href="https://techblog.woowahan.com/12720/">Spring Boot Kotlin Multi Module로 구성해보는 헥사고날 아키텍처</a></li>
</ul>
<blockquote>
<p>백엔드 개발자들은 프로젝트를 처음 구성할 때 사용할 기술에 관해 많은 고민을 합니다.</p>
<ul>
<li>우리 데이터베이스(DB)로 CockroachDB가 요즘 흥한다던데 이걸 사용해 보는 건 어떨까?</li>
<li>HTTP 클라이언트로 Retrofit이 좋을까 Feign을 사용하는 것이 좋을까?</li>
<li>MySQL은 좀 진부하지 않아? 새로운 걸 좀 해볼까?</li>
</ul>
<p>이 과정은 짧게는 개발자들 사이의 갑론을박에서 길게는 윗선의 결정까지 적지 않은 시간적 비용이 소모됩니다.</p>
<p>문제는 이렇게 어려운 선택들이 프로젝트를 성공적으로 이끌게 되었는가? 물론 그럴 때도 있겠지만 프로젝트 중후반에서야 &quot;아.. 이거 OO를 사용하면 안 되는 거였는데..&quot;라는 후회(?)를 하게 되는 경우가 적지 않게 있고, 이미 선정된 기술들이 이것 저곳 비즈니스 코드들 깊이 뿌리내려 있는 상황이라 이러지도 저러지도 못하는 상황에서 힘들게 프로젝트를 마무리하게 되는 것을 심심치 않게 보았습니다.</p>
<p>출저 - <a href="https://techblog.woowahan.com/12720/">Spring Boot Kotlin Multi Module로 구성해보는 헥사고날 아키텍처</a> 일부 발췌</p>
</blockquote>
<hr>
<h1 id="2-configuration">2. Configuration</h1>
<h2 id="21-package-생성">2.1 Package 생성</h2>
<p>우선, 프로젝트의 큰 뼈대를 잡기 위해 <code>Package</code>들을 생성합니다.</p>
<blockquote>
<h3 id="💡-왜-패키지를-미리-정의할까">💡 왜 패키지를 미리 정의할까?</h3>
<p>사실 이 Package 구조는 언제든 변할 수 있습니다. 저는 규칙을 정하는 것을 좋아하기에
무엇을 어디에 저장할 지 미리 구조를 정의해두면, 클래스가 이리저리 튀는 것을 방지할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/5b215377-7724-4d1e-bee8-5b0a9cc4bd7e/image.png" alt=""></p>
<p>이 패키지들은 제가 주로 사용하는 구조입니다.</p>
<ul>
<li><code>annotation</code>: 커스텀 어노테이션 클래스 모음</li>
<li><code>aop</code>: AOP와 관련된 클래스(Aspect, ExceptionHandler, 등) 모음</li>
<li><code>api</code>: API 작업과 관련된 클래스(RestController, Service, VO, 등) 모음</li>
<li><code>config</code>: 전역 빈들을 관리하기 위한 클래스 모음</li>
<li><code>dao</code>: DB 작업과 관련된 클래스(Entity, Repository, 등) 모음</li>
<li><code>utils</code>: 기타 static 작업 유틸 클래스 모음</li>
</ul>
<br>

<h2 id="22-bean-configuration">2.2 Bean Configuration</h2>
<h3 id="spring-bean">Spring Bean</h3>
<p><code>Spring</code>에서는 Spring의 <code>IoC Container</code>에 의해 관리되는 <code>POJO(Plain Old Java Object)</code>를 <code>Bean</code>이라고 부르며, 이러한 Bean들은 Spring을 구성하는 핵심 요소입니다.</p>
<blockquote>
<h3 id="spring-bean-1">Spring Bean</h3>
<ul>
<li>POJO(Plain Old Java Object)로써 Spring 애플리케이션을 구성하는 핵심 객체</li>
<li>Spring IoC 컨테이너(또는 DI 컨테이너)에 의해 생성 및 관리</li>
<li>class, id, scope, constructor-arg 등을 주요 속성</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/4d108c32-3d3b-4e46-a743-d817505d1bf6/image.png" alt=""></p>
<p>스프링은 컴포넌트 스캔을 통해 스프링 IoC 컨테이너에서 사용할 빈들을 등록합니다.</p>
<p>스프링에서 사용하는 컴포넌트(빈)에는 <code>@Component</code>, <code>@Controller</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Configuration</code> 들이 있습니다.</p>
<blockquote>
<ul>
<li><code>@Component</code> : 컴포넌트 스캔에서 사용</li>
<li><code>@Controlller</code> : 스프링 MVC 컨트롤러에서 사용. 스프링 MVC 컨트롤러로 인식</li>
<li><code>@Service</code> : 스프링 비즈니스 로직에서 사용. 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 &#39;핵심 비즈니스 로직이 여기에 있겠구나.&#39; 라고 비즈니스 계층을 인식하는데 도움이 된다.</li>
<li><code>@Repository</code> : 스프링 데이터 접근 계층에서 사용. 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.</li>
<li><code>@Configuration</code> : 스프링 설정 정보에서 사용. 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.</li>
</ul>
</blockquote>
<p>이러한 어노테이션을 POJO에 등록하면, <code>Spring</code>은 시작과 동시에 아래와 같이 지정된 <code>basePackages</code>의 하위 패키지들을 탐색하여 <code>Spring Container</code>에 등록합니다.</p>
<pre><code class="language-java">@SpringBootApplication
@ComponentScan(basePackages = &quot;io.github.twinklekhj.demo&quot;)
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}</code></pre>
<p>최상위 경로를 입력하면 아래와 같은 경고 메시지가 뜨는데요, 이는 이미 내장하고 있기 때문에 쓸 필요없다는 것입니다.</p>
<pre><code>Redundant declaration: @SpringBootApplication already applies given @ComponentScan</code></pre><p>즉, 프로젝트가 인지하는 최상위 경로라면 사용할 필요없고, 그 외 다른 경로를 지정하고자 하면 <code>basePackages</code>를 따로 지정해야된다는 의미입니다.</p>
<br>

<h3 id="설정-bean-생성하기">설정 Bean 생성하기</h3>
<p>패키지를 생성한 뒤에는 <code>config</code> 패키지에서 <code>@Configuration</code>, <code>@Bean</code> 어노테이션을 이용하여 전역적으로 사용할 <code>Bean</code>들을 생성합니다.</p>
<blockquote>
<h4 id="configuration-bean-어노테이션은-언제-사용할까">@Configuration, @Bean 어노테이션은 언제 사용할까?</h4>
<p>이러한 어노테이션은 수동으로 빈을 직접 등록해줘야만 하는 상황일 때 사용합니다.</p>
<ol>
<li>개발자가 직접 제어가 불가능한 라이브러리를 활용할 때</li>
<li>애플리케이션 전범위적으로 사용되는 클래스를 등록할 때</li>
<li>다형성을 활용하여 여러 구현체를 등록해주어야 할 때</li>
</ol>
</blockquote>
<p>예를 들면 아래와 같은 설정들이 있습니다.</p>
<ul>
<li>DB 설정</li>
<li>RestTemplate 설정</li>
<li>WebMvcConfig 설정</li>
<li>Spring Security 설정</li>
<li>QueryDSL 설정</li>
</ul>
<hr>
<h1 id="3-spring-security-설정">3. Spring Security 설정</h1>
<p><code>Spring Boot 3</code>를 이용하면 자동으로 <code>Spring Security</code>가 설정됩니다. 특히 가장 골치 썩는것은 CSRF 설정이죠</p>
<pre><code class="language-java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = &quot;io.github.twinklekhj&quot;)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(registry -&gt; {
                    registry
                            .requestMatchers(&quot;/**&quot;).permitAll()
                            .requestMatchers(&quot;/api/**&quot;).permitAll()
                            .requestMatchers(&quot;/static/**&quot;).permitAll();
                })
                .csrf(configurer -&gt; {
                    configurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
                })
                .exceptionHandling(configurer -&gt; {
                    configurer.accessDeniedPage(&quot;/error&quot;);
                });

        return http.build();
    }
}</code></pre>
<hr>
<h1 id="4-thymeleaf-설정">4. Thymeleaf 설정</h1>
<p>화면을 만들기 위해서 Template Engine인 <code>Thymeleaf</code>를 추가해주었습니다.</p>
<p>Spring Boot에서도 JSP를 사용할 수 있지만, 굳이 Spring에서도 빼고싶어하는 거를 넣고 싶지 않았기에 사뿐히 Thymeleaf로 갈아탔습니다.</p>
<p>Thymealeaf 설정은 간단합니다.</p>
<br>

<h2 id="41-dependency-추가">4.1 Dependency 추가</h2>
<h3 id="maven">Maven</h3>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<h3 id="gradle">Gradle</h3>
<pre><code class="language-kotlin">implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;</code></pre>
<br>

<h2 id="42-applicationyml-수정">4.2 application.yml 수정</h2>
<p><code>Thymeleaf</code>는 기본적으로 <code>application.properties</code> 혹은 <code>application.yml</code>에서 설정 가능합니다. 아래는 기본적으로 많이 사용하는 설정 내용입니다.</p>
<pre><code class="language-yaml"># Spring Configuration
# - thymeleaf
spring:
  thymeleaf:
    suffix: .html
    prefix: classpath:/templates/
    enabled: true
    cache: false    # 개발중 false, 배포시 true</code></pre>
<p>사실 이 설정은 <code>Spring Auto Configure</code> 기능에 의해 자동으로 설정됩니다.</p>
<br>

<h2 id="43-view-controller-및-템플릿-파일-생성">4.3 View Controller 및 템플릿 파일 생성</h2>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e0a02715-b0d8-4393-8c95-2a2dc4bd771d/image.png" alt=""></p>
<h3 id="homecontroller">HomeController</h3>
<p>화면을 그릴 <code>Controller</code>를 생성합니다.</p>
<pre><code class="language-java">import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping(&quot;/&quot;)
    public String home(){
        return &quot;index&quot;;
    }
}</code></pre>
<h3 id="thymeleaf-fragment">Thymeleaf Fragment</h3>
<p><code>Thymeleaf</code>의 <code>Fragment</code> 기능을 이용하여 html을 분해하여 조각 조각 붙일 수 있습니다.</p>
<p>기존에 Tiles를 이용하여 Template을 만들던 저에겐 코드 복붙이란 있을 수 없는 일이었죠. 아쉽게도 Thymeleaf에는 Tiles는 없지만 Fragment 기능을 활용하여 코드를 분리할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/8da61416-8e1a-4b55-aa45-48303dfef163/image.png" alt=""></p>
<pre><code class="language-html">&lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;

&lt;header th:fragment=&quot;header&quot;&gt;
    header
&lt;/header&gt;</code></pre>
<p>나누고자 하는 <code>th:fragment</code> 속성을 사용하여 <code>fragment</code>를 선언합니다.</p>
<pre><code class="language-index.html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;body&gt;
    &lt;th:block th:replace=&quot;~{components/_header::header}&quot;&gt;&lt;/th:block&gt;
    &lt;th:block th:replace=&quot;~{components/_side::sidebar}&quot;&gt;&lt;/th:block&gt;
    &lt;main&gt;
        Body
    &lt;/main&gt;
    &lt;th:block th:replace=&quot;~{components/_footer::footer}&quot;&gt;&lt;/th:block&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><code>&lt;th:block&gt;</code> 태그를 이용하여 Fragment를 불러옵니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/6a5b19b4-fc6c-4719-b10a-90be4395720d/image.png" alt=""></p>
<p>완성-⭐</p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li><a href="https://www.inflearn.com/chats/700670/querydsl-springboot-3-0%EC%9D%98-gradle-%EC%84%A4%EC%A0%95%EC%9D%84-%EA%B3%B5%EC%9C%A0%ED%95%A9%EB%8B%88%EB%8B%A4?gad_source=1&amp;gclid=CjwKCAjwte-vBhBFEiwAQSv_xR_HPPRBgX2fpMelyzfYEdN2lYHxDeJvJwCZwaS5jERNTxk5i2KV1xoCiL4QAvD_BwE">QueryDsl SpringBoot 3.0의 gradle 설정을 공유합니다.</a></li>
<li><a href="https://mangkyu.tistory.com/75">[Spring] 빈 등록을 위한 어노테이션 @Bean, @Configuration, @Component 차이 및 비교 - (1/2)</a></li>
</ul>
<hr>
<h1 id="💎-마치며">💎 마치며</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트] 시저 암호]]></title>
            <link>https://velog.io/@developer_khj/Coding-Test-Caesar-Cipher</link>
            <guid>https://velog.io/@developer_khj/Coding-Test-Caesar-Cipher</guid>
            <pubDate>Thu, 21 Mar 2024 12:13:21 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>이번 포스팅에서는 시저 암호에 대해 다뤄보고자 합니다.</p>
<hr>
<h1 id="시저-암호">시저 암호</h1>
<blockquote>
<h3 id="시저-암호caesar-cipher">시저 암호(Caesar cipher)</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/372350d1-590e-46ba-92ec-9919c29473bb/image.png" alt=""></p>
<p>시저 암호(Caesar cipher) 또는 카이사르 암호는 암호학에서 다루는 간단한 치환암호의 일종이다.</p>
<p>실제로 로마의 황제 카이사르는 이 카이사르 암호를 사용하기도 했다. 카이사르 암호는 암호화하고자 하는 내용을 알파벳별로 일정한 거리만큼 밀어서 다른 알파벳으로 치환하는 방식이다. 예를 들어 3글자씩 밀어내는 카이사르 암호로 &#39;COME TO ROME&#39;을 암호화하면 &#39;FRPH WR URPH&#39;가 된다.</p>
<p>카이사르 암호는 약 기원전 100년경에 만들어져 로마의 장군인 카이사르가 동맹군들과 소통하기 위해 만든 암호이다.</p>
</blockquote>
<p>시저 암호는 암호학에서 다루는 간단한 치환암호입니다. 암호화하고자 하는 내용을 알파벳별로 일정한 거리만큼 밀어서 다른 알파벳으로 치환하는 방식입니다.</p>
<hr>
<h1 id="💡-문제">💡 문제</h1>
<h2 id="1-시저암호">1. 시저암호</h2>
<blockquote>
<h3 id="시저암호">시저암호</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/5242dc04-ac4e-4ddf-8e10-502fcadb2f7b/image.png" alt=""></p>
<p>문제 링크: <a href="https://school.programmers.co.kr/learn/courses/30/lessons/12926">https://school.programmers.co.kr/learn/courses/30/lessons/12926</a></p>
</blockquote>
<h2 id="2-둘만의-암호">2. 둘만의 암호</h2>
<blockquote>
<h3 id="시저암호-1">시저암호</h3>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/e2a6b989-10b6-4cc6-bfb9-98a02a88ea1c/image.png" alt=""></p>
<p>문제 링크: <a href="https://school.programmers.co.kr/learn/courses/30/lessons/155652">https://school.programmers.co.kr/learn/courses/30/lessons/155652</a></p>
</blockquote>
<hr>
<h1 id="🔍-해결법">🔍 해결법</h1>
<ul>
<li>암호 리스트를 정의합니다.</li>
<li>밀려나는 숫자 만큼 더해줍니다.</li>
<li>모듈러 함수(%)를 통해 순환되도록 합니다.</li>
</ul>
<pre><code class="language-python">def decode(str_list, e, n):
    idx = (str_list.index(e)+n) % len(str_list)
    return str_list[idx]</code></pre>
<pre><code class="language-python">import string
def solution(s, n):
    return &#39;&#39;.join(list(map(lambda x: decode(string.ascii_lowercase. x, 5), s)))</code></pre>
<p>알파벳을 5만큼 민 경우</p>
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
<th>c</th>
<th>d</th>
<th>e</th>
<th>f</th>
<th>g</th>
<th>h</th>
<th>i</th>
<th>j</th>
<th>k</th>
<th>l</th>
<th>m</th>
<th>n</th>
<th>o</th>
<th>p</th>
<th>q</th>
<th>r</th>
<th>s</th>
<th>t</th>
<th>u</th>
<th>v</th>
<th>w</th>
<th>x</th>
<th>y</th>
<th>z</th>
</tr>
</thead>
<tbody><tr>
<td>f</td>
<td>g</td>
<td>h</td>
<td>i</td>
<td>j</td>
<td>k</td>
<td>l</td>
<td>m</td>
<td>n</td>
<td>o</td>
<td>p</td>
<td>q</td>
<td>r</td>
<td>s</td>
<td>t</td>
<td>u</td>
<td>v</td>
<td>w</td>
<td>x</td>
<td>y</td>
<td>z</td>
<td>a</td>
<td>b</td>
<td>c</td>
<td>d</td>
<td>e</td>
</tr>
</tbody></table>
<pre><code class="language-python">solution(&quot;cvkkt&quot;, 5) # 결과: &quot;happy&quot;</code></pre>
<br>

<p>알파벳 사용시에는 아래와 같이 암호 리스트를 정의하면 됩니다.</p>
<pre><code class="language-python"># string 모듈 사용시
import string
string.ascii_letters    # 결과: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
string.ascii_lowercase    # 결과: abcdefghijklmnopqrstuvwxyz
string.ascii_uppercase    # 결과: ABCDEFGHIJKLMNOPQRSTUVWXYZ

# string 모듈 미사용시
lower = &#39;abcdefghijklmnopqrstuvwxyz&#39;
upper = &#39;ABCDEFGHIJKLMNOPQRSTUVWXYZ&#39;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Velog에 외부 서비스 넣기]]></title>
            <link>https://velog.io/@developer_khj/velog-external-service</link>
            <guid>https://velog.io/@developer_khj/velog-external-service</guid>
            <pubDate>Thu, 21 Mar 2024 04:35:18 GMT</pubDate>
            <description><![CDATA[<h1 id="💎-들어가며">💎 들어가며</h1>
<p>블로그 포스팅을 하면서 velog가 처음에는 커스텀 없이 이용할 수 있다는 점이 매력이었지만 시간이 지날수록 커스텀에 대한 욕심이 나날로 커지고 있습니다.</p>
<p>다른 블로그들을 보면서 가장 부러웠던 것은 code 블록이었습니다. 이는 서칭해보니 velog에서 이미 지원하고 있는 기능이있었습니다.</p>
<p>바로 <code>Codepen</code>과 <code>Code Sandbox</code> !!🙌 외에도 <code>youtube</code> 및 <code>twitter</code> 삽입도 지원합니다.😁</p>
<p>아무것도 안될 때보단 이정도면 양호?🤣</p>
<p>지금부터 velog에 여러가지 외부 서비스를 넣는 방법을 소개해보겠습니다.</p>
<hr>
<h1 id="🎈-외부-서비스-개체-삽입">🎈 외부 서비스 개체 삽입</h1>
<blockquote>
<p>현재 다음 서비스들에 대하여 포스트에 개체 삽입 기능이 지원되고 있습니다.</p>
<ul>
<li>Youtube</li>
<li>Twitter</li>
<li>CodeSandbox</li>
<li>Codepen</li>
</ul>
<p>지원 서비스 추가 요청은 <a href="https://github.com/velopert/velog-client/issues">GitHub Issue</a>를 통해서 요청해주시길 바랍니다.</p>
</blockquote>
<p>개체 삽입 Syntax는 다음과 같습니다.</p>
<pre><code class="language-md">![service_name](id)</code></pre>
<ul>
<li>service_name: youtube, twitter, codesandbox, codepen</li>
<li>id: 고유 ID 값</li>
</ul>
<hr>
<h1 id="1-codepen">1. Codepen</h1>
<blockquote>
<h3 id="💡-codepen-이란">💡 Codepen 이란?</h3>
<p>웹 개발자들이 HTML, CSS, JavaScript를 쉽게 테스트하고 공유할 수 있는 온라인 코드 편집기 및 개발 플랫폼</p>
</blockquote>
<p>코드펜은 구글링하다보면 가장 많이 볼 수 있는 플랫폼입니다. <a href="https://codepen.io/trending">Codepen 사이트</a>에서 회원가입 후, 코드를 저장한 뒤 공유할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/94ff034b-30a6-440e-be1d-e1de2f8c8f9a/image.png" alt="codepen hello world"></p>
<p>공유할 코드를 작성한 뒤, 우측 하단에 있는 <code>Embed</code> 버튼을 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/3d3e9708-71c6-45d0-86f4-775cf4c5b8c1/image.png" alt="codepen embed share"></p>
<p>Iframe 탭을 열어 아래 밑줄 친 코드를 복사하여 코드 블록에 삽입합니다.</p>
<pre><code>!codepen[twinklekhj/embed/bGJqaxg?default-tab=html%2Cresult&amp;theme-id=light&amp;height=265]</code></pre><p>!codepen[twinklekhj/embed/bGJqaxg?default-tab=html%2Cresult&amp;theme-id=light&amp;height=265]</p>
<br>

<hr>
<h1 id="2-code-sandbox">2. Code Sandbox</h1>
<blockquote>
<h3 id="code-sandbox">Code Sandbox</h3>
<p>온라인 IDE, 즉 CDE(Cloud Development Environment)입니다.</p>
</blockquote>
<p><a href="https://codesandbox.io/">codesandbox 사이트</a>에서 가입하여, 코드를 작성하고 공유할 수 있습니다. 주로 <code>React</code>, <code>Vue</code>, <code>Angular</code>, <code>Node.js</code>, <code>Next.js</code> 등 Javascript 단을 지원하며 그 밖에 <code>Python</code>, <code>Go</code>, <code>PHP</code> 등을 지원합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b2d32db9-8f20-4718-9ce3-da3dfe6a3e2b/image.png" alt="Create Devbox"></p>
<p>Dev box를 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/a241296d-7d05-486d-9152-1f9bbe7075d4/image.png" alt="helloworld devbox"></p>
<p>저는 test 용도로 <code>velog</code>를 생성하였습니다. <code>share 버튼</code>을 클릭하여 접근 권한, 수정 권한을 수정합니다. </p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/f964f335-2816-44a8-bdf0-007f033ca179/image.png" alt="share 옵션"></p>
<p><code>Embed</code> 옵션을 클릭하여 Embed 창을 열어줍니다.</p>
<p><img src="https://velog.velcdn.com/images/developer_khj/post/b781a125-c158-4ad0-9fe0-7ac979de254b/image.png" alt="Embed 옵션 수정창"></p>
<p>embeded 코드를 복사하여 아래와 유사하게 작성해줍니다.</p>
<pre><code>!codesandbox[velog-29782r?theme=dark&amp;hidenavigation=1]</code></pre><p>!codesandbox[velog-29782r?theme=dark&amp;hidenavigation=1]</p>
<p>위와 같이 코드가 뜨면 완성-⭐</p>
<hr>
<h1 id="3-youtube">3. youtube</h1>
<p>유투브 영상을 공유할 수 있는 기능입니다.</p>
<pre><code class="language-url">https://www.youtube.com/watch?v=1MAgHVs5XZE&amp;ab_channel=OlafLyrics</code></pre>
<p>유투브의 id는 url에서 찾을 수 있습니다. <code>v 파라미터</code>가 <code>id</code> 입니다.</p>
<pre><code class="language-md">!youtube[1MAgHVs5XZE]</code></pre>
<p>!youtube[1MAgHVs5XZE]</p>
<p>제가 좋아하는 디즈니 플레이 리스트를 공유해보았습니다.💖</p>
<hr>
<h1 id="4-twitter">4. Twitter</h1>
<p>twitter의 게시글을 공유할 수 있는 기능입니다.</p>
<pre><code class="language-curl">https://twitter.com/NetflixKR/status/1770631413458415624</code></pre>
<p>트위터의 id도 url에서 찾을 수 있습니다. url에서 <a href="https://twitter.com/">https://twitter.com/</a> 을 제거한 뒤에 pathname을 붙여넣으면 됩니다.</p>
<pre><code>!twitter[NetflixKR/status/1770631413458415624]</code></pre><p>!twitter[NetflixKR/status/1770631413458415624]</p>
<hr>
<h1 id="💎-references">💎 References</h1>
<ul>
<li>velog 공식 블로그 - <a href="https://velog.io/@velog/katex-and-embed-support">수식 입력 및 외부 서비스 개체 삽입 기능 업데이트</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>