<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>harry-jang.log</title>
        <link>https://velog.io/</link>
        <description>software engineer</description>
        <lastBuildDate>Sun, 21 Jan 2024 14:06:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>harry-jang.log</title>
            <url>https://images.velog.io/images/harry-jang/profile/2d768814-9c1f-4965-a63a-9a95b3b4b6a6/75602190_2798336053726785_2180174127174974061_n.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. harry-jang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/harry-jang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[helm] values.yaml 추출하기]]></title>
            <link>https://velog.io/@harry-jang/helm-values.yaml-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/helm-values.yaml-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Jan 2024 14:06:58 GMT</pubDate>
            <description><![CDATA[<h3 id="values-내용-확인">values 내용 확인</h3>
<pre><code class="language-bash"> helm show values ${차트명}</code></pre>
<h3 id="values-내용을-파일로-추출">values 내용을 파일로 추출</h3>
<pre><code class="language-bash"> helm show values ${차트명} &gt; values.yaml</code></pre>
<h3 id="valuesyaml을-차트에-오버라이드해서-설치하기">values.yaml을 차트에 오버라이드해서 설치하기</h3>
<pre><code class="language-bash">helm install -f values.yaml ${사용자 지정 Release 이름} ${설치하려는 차트 이름}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Rancher Desktop] kubectl 에러 해결법]]></title>
            <link>https://velog.io/@harry-jang/Rancher-Desktop-kubectl-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@harry-jang/Rancher-Desktop-kubectl-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Thu, 11 Jan 2024 02:43:15 GMT</pubDate>
            <description><![CDATA[<h3 id="이슈">이슈</h3>
<p>windows 환경에서 <code>Rancher Desktop</code>을 사용 중인데, kubernetes 버전을 1.28.5 버전으로 올린 이후로  kubectl 명령어 사용시 해당 에러가 발생하였다.</p>
<pre><code class="language-bash">I0111 11:20:45.321675   34884 versioner.go:88] Right kubectl missing, downloading version 1.15.3
F0111 11:20:45.635763   34884 main.go:70] error while trying to get contents of https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/windows/amd64/kubectl.exe.sha256: GET https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/windows/amd64/kubectl.exe.sha256 returned http status 404 Not Found</code></pre>
<h3 id="원인">원인</h3>
<p>kubectl 과 Rancher로 기동한 kubernetes간 버전이 달라서 발생하는 이슈로 kubectl 버전을 업그레이드하는 방법으로 해결할 수 있다.</p>
<h3 id="해결법">해결법</h3>
<p>cmd창(명령 프롬프트)에서 아래 명령어를 입력하여 신규 버전으로 설치하는 방법
    <code>bash
    curl.exe -LOk &quot;https://dl.k8s.io/release/&lt;kubectl 버전&gt;/bin/windows/amd64/kubectl.exe&quot;</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ReactJS + TypeScript 프로젝트 기본 세팅]]></title>
            <link>https://velog.io/@harry-jang/ReactJS-TypeScript-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@harry-jang/ReactJS-TypeScript-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Sat, 02 Sep 2023 14:49:27 GMT</pubDate>
            <description><![CDATA[<h3 id="프로젝트-초기화">프로젝트 초기화</h3>
<ol>
<li><p>프로젝트 초기화 및 필수 모듈 설치</p>
<pre><code class="language-bash">
npx create-react-app myApp --template typescript
npm i --save-dev @types/styled-components
npm i styled-components
npm i recoil</code></pre>
</li>
<li><p><strong>/src</strong> 디렉토리에 <code>App.tsx</code>, <code>index.tsx</code> 파일 제외한  불필요한 파일 삭제</p>
</li>
<li><p><code>index.tsx</code>파일 기본 세팅 적용</p>
<pre><code class="language-typescript">
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import App from &#39;./App&#39;;
import { RecoilRoot } from &#39;recoil&#39;;
import { ThemeProvider } from &#39;styled-components&#39;;
import { darkTheme } from &#39;./theme&#39;;

const root = ReactDOM.createRoot(
 document.getElementById(&#39;root&#39;) as HTMLElement
);
root.render(
 &lt;React.StrictMode&gt;
     &lt;RecoilRoot&gt;
       &lt;ThemeProvider theme={darkTheme}&gt;
         &lt;App /&gt;
       &lt;/ThemeProvider&gt;
     &lt;/RecoilRoot&gt;
 &lt;/React.StrictMode&gt;
);
</code></pre>
</li>
</ol>
<h3 id="스타일-적용">스타일 적용</h3>
<h4 id="기본-테마-인터페이스-재정의">기본 테마 인터페이스 재정의</h4>
<ol>
<li><p><strong>/src</strong> 디렉토리에 <code>style.d.ts</code>파일 생성</p>
<pre><code class="language-typescript">
import &quot;styled-components&quot;;

declare module &quot;styled-components&quot; {
   export interface DefaultTheme {
       textColor: string;
       bgColor: string;
       accentColor: string;
   }
}</code></pre>
</li>
<li><p><strong>/src</strong> 디렉토리에 <code>theme.ts</code>파일 생성</p>
<pre><code class="language-typescript">
import { DefaultTheme } from &quot;styled-components&quot;;

export const darkTheme : DefaultTheme = {
   bgColor: &quot;#2f3640&quot;,
   textColor: &quot;#7f8fa6&quot;,
   accentColor: &quot;#9c88ff&quot;,
};

export const lightTheme : DefaultTheme = {
   bgColor: &quot;whitesmoke&quot;,
   textColor: &quot;black&quot;,
   accentColor: &quot;#9c88ff&quot;,
};</code></pre>
</li>
<li><p><code>App.tsx</code>에 전역 스타일 설정 추가</p>
<pre><code class="language-typescript">
 import React from &#39;react&#39;;
 import { createGlobalStyle } from &#39;styled-components&#39;;

 const GlobalStyle = createGlobalStyle`
 @import url(&#39;https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300&amp;display=swap&#39;);
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, menu, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td,
 article, aside, canvas, details, embed,
 figure, figcaption, footer, header, hgroup,
 main, menu, nav, output, ruby, section, summary,
 time, mark, audio, video {
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   font: inherit;
   vertical-align: baseline;
 }
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure,
 footer, header, hgroup, main, menu, nav, section {
   display: block;
 }
 /* HTML5 hidden-attribute fix for newer browsers */
 *[hidden] {
     display: none;
 }
 body {
   line-height: 1;
 }
 menu, ol, ul {
   list-style: none;
 }
 blockquote, q {
   quotes: none;
 }
 blockquote:before, blockquote:after,
 q:before, q:after {
   content: &#39;&#39;;
   content: none;
 }
 table {
   border-collapse: collapse;
   border-spacing: 0;
 }
 * {
   box-sizing : border-box;
 }
 body {
   font-family: &#39;Source Sans 3&#39;, sans-serif;
   background-color: ${props =&gt; props.theme.bgColor};
   color:${props =&gt; props.theme.textColor}

 }
 a {
   text-decoration: none;
   color:inherit;
 }
 `;

 function App() {
   return (
     &lt;&gt;
       &lt;GlobalStyle /&gt;
     &lt;/&gt;
   );
 }

 export default App;</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(12) - 클래스 기반 뷰 적용하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B012-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%B7%B0-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B012-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%B7%B0-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 09 Aug 2023 14:48:18 GMT</pubDate>
            <description><![CDATA[<p><strong>Django</strong>는 <strong>클래스 기반 뷰(class-based views)</strong>라는 함수가 아닌 파이썬 객체를 이용하여 뷰를 생성하는 방법도 제공합니다. 클래스 기반 뷰는 함수 기반 뷰와는 달리 다음 장점을 갖고 있습니다.</p>
<ul>
<li>특정 HTTP 메서드(GET, POST 등)와 관련된 코드를 조건 분기 대신 별도의 메서드를 사용하여 처리할 수 있습니다.</li>
<li>다중상속(multiple inheritance)과 같은 객체 지향 기법을 사용하여 코드를 재사용 가능한 구성 요소로 분리할 수 있습니다.</li>
</ul>
<p>그리고 뷰 개발에서 흔히 발견되는 관용구와 패턴을 추상화하고 일반적인 경우에 뷰 개발을 용이하게 하기 위해 자주 쓰는 클래스 기반 뷰들을 미리 만들어서 제공하고 있는데, 이 뷰를 <strong><code>제네릭 뷰</code></strong>라고 합니다.</p>
<p>Django에서 제공하는 클래스 기반 뷰 및 제네릭 뷰에 대한 자세한 정보는 아래 링크를 참고바랍니다.</p>
<p><a href="https://docs.djangoproject.com/ko/3.1/ref/class-based-views/">Built-in class-based views API</a></p>
<p>자 그러면 지금까지 만든 코드에 이것을 한 번 적용해봅시다.</p>
<h3 id="뷰-파일-수정하기">뷰 파일 수정하기</h3>
<p>먼저, 기존에 함수 기반 뷰로 작성된 <strong>test_app/views.py</strong>파일의 해당 부분을 수정해봅시다.</p>
<pre><code class="language-python">...
def index(request):
    latest_book_list = TestBook.objects.order_by(&quot;-reg_datetime&quot;)[:5]
    context = {&quot;latest_book_list&quot; : latest_book_list,}
    return render(request, &quot;book/index.html&quot;, context)

def book(request, book_id):
    book = get_object_or_404(TestBook, pk=book_id)
    return render(request, &quot;book/detail.html&quot;,  {&quot;book&quot;: book, &quot;range&quot;:range(10)})

def comment(request, book_id):
...</code></pre>
<p>아래는 클래스 기반 뷰로 변경한 형태입니다.</p>
<pre><code class="language-python">...
from django.views.generic import (
    ListView,
    DetailView,
)

class BookIndexView(ListView):
    template_name = &quot;book/index.html&quot;
    context_object_name = &quot;latest_book_list&quot;

    def get_queryset(self):
        return TestBook.objects.order_by(&quot;-reg_datetime&quot;)[:5]

class BookDetailView(DetailView):
    model = TestBook
    template_name = &quot;book/detail.html&quot;
    context_object_name = &#39;book&#39;

    def get_context_data(self, **kwargs: Any) -&gt; Dict[str, Any]:
        context_data = super().get_context_data(**kwargs)
        context_data[&#39;range&#39;] = range(10)      
        return context_data

def comment(request, book_id):
...</code></pre>
<p>기존 <code>index()</code> 함수는 <code>BookIndexView</code> 클래스로 전환되었으며 <code>BookIndexView</code>는 목록의 형태로 데이터를 보여주기 때문에 <code>ListView</code>라는 제네릭뷰를 상속받도록 설정합니다. </p>
<blockquote>
<p>Python 언어에서의 상속은 아래와 같은 형태로 상속관계를 설정할 수 있습니다. </p>
<pre><code class="language-python">class 클래스명(상속받을 부모클래스명)</code></pre>
<p>함수의 입력 파라미터와 형태가 비슷해 보이나 전혀 다른 개념이므로 혼동되지 않도록 합니다.</p>
</blockquote>
<p>그리고 필요한 값들을 미리 정해둔 변수값에 입력받는 형식으로 작성을 하게 됩니다. <code>BookIndexView</code> 예시는 <strong><code>template_name</code></strong> 변수에 사용자 화면에 바탕이 될 템플릿의 이름(파일경로)를 작성하고, <strong><code>context_object_name</code></strong>값을 설정하여 화면에 목록으로 보여질 모델의 목록 객체의 이름을 &#39;latest_book_list&#39;로 선언했습니다. </p>
<p>그리고 화면에 뿌려질 모델을 가져오기 위한 메서드인 <code>get_queryset()</code> 함수는 기존에 모델을 최근 5개까지의 모델들을 목록으로 리턴하도록 <strong>재정의(override)</strong>하였습니다.</p>
<p>목록을 보여주는 뷰 뿐만 아니라 실제 다른 제네릭 뷰를 사용하더라도 위 예시와 같이 용도에 맞는 <code>제네릭 뷰</code> 클래스를 상속받게 설정한 후 부모클래스에서 미리 정의된 멤버변수에 필요한 값들을 채워 넣고 필요한 메서드를 <strong>재정의(override)</strong>하는 형식으로 구현된다고 보시면 됩니다. </p>
<p>기존 book 함수도 <code>BookDetailView</code> 클래스로 전환되었으며, 마찬가지로 제네릭 뷰의 <code>DetailView</code>를 상속받아서 구현되었습니다.</p>
<p><code>model</code>변수에 모델로 사용할 테이블 모델 클래스를 지정하고, <code>template_name</code>과 <code>context_object_name</code>에 각각 화면에 노출시킬 템플릿 명과 그 템플릿을 이용하여 화면에 그릴 때 필요한 모델 객체 데이터를 담을 변수의 이름을 선언했습니다.
또 <code>context_data</code>라는 딕셔너리 객체에 <code>range</code>라는 키를 추가하여 템플릿에서 평점 입력을 위한 라디오 버튼을 생성할 때 필요한 값을 함께 리턴하도록 <strong>재정의(override)</strong>하였습니다.</p>
<p>참고로 <code>context_object_name</code>은 별도로 설정하지 않으면 기본값으로 모델 객체가 내려가게 됩니다. 하지만 기본값을 사용하지 않고 수정한 이유는 만들어 두었던 템플릿 파일을 그대로 활용할 수 있게 하기 위함입니다. </p>
<p>예를 들어 기존에 작성해뒀던 <code>templates/book/index.html</code>를 한 번 살펴봅시다.</p>
<h4 id="templatesbookindexhtml">templates/book/index.html</h4>
<pre><code class="language-html">&lt;h3&gt;신간도서&lt;/h3&gt;
{% if latest_book_list %}
&lt;ul class=&quot;list-group&quot;&gt;
  {% for book in latest_book_list %}
  &lt;li&gt;
    &lt;a href=&quot;{% url &#39;bookstore:detail&#39; book.id %}&quot;&gt;{{ book.title }}&lt;/a&gt;
  &lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;
{% else %}
&lt;p&gt;No books are available.&lt;/p&gt;
{% endif %}</code></pre>
<p><strong>views.py</strong>에서 <code>context_object_name</code>을 이미 기존 템플릿 파일내에서 사용하는 이름으로 바꿔주었기 때문에 템플릿 파일들은 별도 수정없이 그대로 사용할 수 있게 되었습니다.</p>
<blockquote>
<p><strong><code>context_object_name</code></strong>의 기본값은 제네릭 뷰가 상속받는 Mixin에 따라 이름이 결정됩니다. 예를 들어 제네릭 뷰가 <code>SingleObjectMixin</code>을 상속받는 단일 개체를 위한 뷰라면 모델명이 곧 이름이 됩니다. 하지만 <code>ListView</code>와 같이 복수 개체를 위한 뷰라면 <code>MultipleObjectMixin</code>을 상속받기 때문에 모델명 뒤에 &#39;_list&#39;라는 이름으로 디폴트 값이 정해지게 됩니다.
ex) 
Book 모델 단일 개체용 뷰에서의 기본값 : <strong><em>book</em></strong>
Book 모델 복수 개체용 뷰에서의 기본값 : <strong><em>book_list</em></strong></p>
</blockquote>
<blockquote>
<p><strong><em>믹스인(Mixin) 이란?</em></strong>
<code>Mixin</code>은 python에서 제공하는 다중상속을 활용한 클래스의 한 종류입니다. 하지만 일반적인 클래스 상속의 개념과는 다르게 재사용되는 코드의 파편, 즉 스스로는 별 의미를 가지지 않지만 상속받는 클래스에게 부가적인 기능을 추가시켜주는 용도로 사용하는 클래스입니다.</p>
</blockquote>
<h3 id="url-설정파일-수정하기">URL 설정파일 수정하기</h3>
<p>다음으로 만들어진 클래스 기반뷰로 라우팅 되도록 <code>test_app/urls.py</code> 파일도 수정해 줍니다.</p>
<pre><code class="language-python">...
from . import views

app_name = &quot;bookstore&quot;
urlpatterns = [
    path(&quot;&quot;, views.BookIndexView.as_view(), name=&quot;index&quot;),
    path(&quot;&lt;int:pk&gt;/&quot;, views.BookDetailView.as_view(), name=&quot;detail&quot;),
...
]</code></pre>
<br>
이번 포스팅에서는 함수 기반 뷰를 클래스 기반 뷰로 전환하는 방법을 정리하였고, 다음 포스팅에 클래스 기반뷰와 폼을 사용하여 서평을 등록하는 코드를 개선해 보겠습니다.]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(11) - 템플릿 활용 및 폼 사용하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B011-%ED%85%9C%ED%94%8C%EB%A6%BF-%ED%99%9C%EC%9A%A9-%EB%B0%8F-%ED%8F%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B011-%ED%85%9C%ED%94%8C%EB%A6%BF-%ED%99%9C%EC%9A%A9-%EB%B0%8F-%ED%8F%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 29 Jul 2023 08:33:37 GMT</pubDate>
            <description><![CDATA[<p>오늘은 기존에 만든 샘플 앱에 추가 기능을 넣어 봅시다.</p>
<ul>
<li>서평(comment) 달기</li>
<li>평점 등록</li>
<li>서평에 좋아요, 싫어요 기능</li>
</ul>
<p>먼저 <strong>test_app/models.py</strong>에 서평에 대한 내용을 담을 모델을 신규로 생성합니다.</p>
<pre><code class="language-python">...
class TestBook(models.Model):
    ...
    rating=models.FloatField(null=True, blank=True)

    def __str(self):
    ...


class TestBookComment(models.Model):
    book = models.ForeignKey(TestBook, on_delete=models.CASCADE)
    content = models.TextField(max_length=1000)
    scroe = models.IntegerField(default=0)
    likes = models.IntegerField(default=0)
    dislikes = models.IntegerField(default=0)

    def __str__(self):
        return self.content

    def check_acceptance(self, threshold_ratio=0.5):
        total_votes = self.likes + self.dislikes
        if total_votes == 0:
            return False

        like_ratio = self.likes / total_votes

        if like_ratio &gt;= threshold_ratio:
            return True
        else:
            return False</code></pre>
<p><code>TestBook</code>에 평점을 저장할 변수 <code>rating</code>을 추가하고, 여러 개의 서평을 달 수 있도록 <code>TestBookComment</code> 테이블 모델 클래스와 1:N 관계를 만들어 주기 위해 ForeignKey 필드를 통해 관계를 정의했습니다. 그리고 나머지 각 변수의 역할은 다음과 같습니다.</p>
<ul>
<li><code>content</code> : 서평 본문 내용</li>
<li><code>score</code> : 추천도(점수)</li>
<li><code>likes</code> : 이 서평에 대한 좋아요 수</li>
<li><code>dislikes</code> : 이 서평에 대한 싫어요 수</li>
</ul>
<p>그리고 <code>check_acceptance()</code>라는 함수를 만들어 &#39;좋아요&#39; 수와 &#39;싫어요&#39; 수의 비율에 따라 이 서평의 평가점수가 책의 평점 집계에 채택되는지 여부를 확인할 수 있게 했습니다.</p>
<p>다음으로 <code>test_app/templates/book/detail.html</code> 뷰에 서평을 달 수 있는 코드 및 html 마크업을 추가합니다.</p>
<p>** /templates/book/detail.html 전문 **</p>
<pre><code class="language-html">&lt;h3&gt;{{ book.title }}&lt;/h3&gt;
&lt;table border=&quot;1&quot;&gt;
  &lt;th&gt;도서번호&lt;/th&gt;
  &lt;th&gt;카테고리&lt;/th&gt;
  &lt;th&gt;가격&lt;/th&gt;
  &lt;th&gt;등록일자&lt;/th&gt;
  &lt;tr&gt;
    &lt;td&gt;{{ book.id }}&lt;/td&gt;
    &lt;td&gt;{{ book.category.categoryName }}&lt;/td&gt;
    &lt;td&gt;{{ book.price }}&lt;/td&gt;
    &lt;td&gt;{{ book.reg_datetime }}&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;h4&gt;서평 목록&lt;/h4&gt;
&lt;ul&gt;
  {% for comment in book.testbookcomment_set.all %}
  &lt;li&gt;
    &lt;p&gt;{{ comment.content }} -- {{ comment.score }}&lt;/p&gt;
    &lt;p&gt;
      &lt;button type=&quot;button&quot; onclick=&quot;&quot;&gt;👍&lt;/button&gt; : {{ comment.likes }}
      &lt;button type=&quot;button&quot;&gt;👎&lt;/button&gt; : {{ comment.dislikes }}
    &lt;/p&gt;
  &lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;

&lt;h4&gt;서평 쓰기&lt;/h4&gt;
&lt;form action=&quot;{% url &#39;bookstore:comment&#39; book.id %}&quot; method=&quot;post&quot;&gt;
  {% csrf_token %}

  &lt;fieldset class=&quot;rate&quot; style=&quot;width: 500&quot;&gt;
    &lt;b&gt;추천도&lt;/b&gt;
    {% for i in range %}
    &lt;input
      type=&quot;radio&quot;
      id=&quot;score{{ forloop.revcounter }}&quot;
      name=&quot;score&quot;
      value=&quot;{{ forloop.revcounter }}&quot;
    /&gt;&lt;label
      class=&quot;half&quot;
      for=&quot;score{{ forloop.revcounter }}&quot;
      title=&quot;{{ forloop.revcounter }}점&quot;
      &gt;{{ forloop.revcounter }}&lt;/label
    &gt;
    {% endfor %}
  &lt;/fieldset&gt;
  &lt;textarea name=&quot;comment&quot; cols=&quot;100&quot; rows=&quot;10&quot; maxlength=&quot;1000&quot;&gt;&lt;/textarea&gt;
  &lt;input type=&quot;submit&quot; value=&quot;Comment&quot; /&gt;
&lt;/form&gt;

&lt;a href=&quot;{% url &#39;bookstore:index&#39; %}&quot;&gt;홈으로&lt;/a&gt;
</code></pre>
<p>기존에 작성했던 책의 자세한 정보를 보여주는 테이블 아래에 해당 책에 달려있는 서평 목록과 그 밑에 <code>form</code> 태그를 이용하여 서평을 작성할 수 있는 양식을 추가했습니다.
추천도는 10점부터 1점까지 점수를 줄 수 있도록 라디오 버튼을 10개 만들기 위해 django templates의 _<strong><code>for</code></strong>_태그 이용해서 버튼을 만들어 줬습니다. 
_<strong><code>for</code></strong>_에 대한 설명은 이 <a href="https://velog.io/@harry-jang/Django-django-templates%EC%9D%98-%EB%B0%98%EB%B3%B5%EB%AC%B8-%EC%82%AC%EC%9A%A9">포스팅</a>을 참고바랍니다.</p>
<blockquote>
<p><strong>참고</strong><br>django templates의 보안정책으로 인해 _<strong><code>for</code></strong>_태그는 python의 <code>range()</code> 함수를 바로 사용할 수 없습니다. 우선은 <code>range</code>라는 리스트변수로 for 루프를 돌게 작성해두고, 추후 view 코드에서 <code>range</code> 변수를 넘겨받아 for 루프를 돌 수 있게 해줍니다. </p>
</blockquote>
<blockquote>
<p><strong>참고</strong> 
Django 템플릿 시스템에서 python의 for 반복문과 range 함수를 지원하지 않는 이유는 <strong><em>보안</em></strong>과 <strong><em>템플릿의 분리</em></strong>를 강화하기 위함입니다.
Django 템플릿 시스템은 주로 HTML과 같은 템플릿 코드를 뷰 코드와 분리하여 유지보수를 용이하게 합니다. 따라서 템플릿 코드는 보통 웹 디자이너나 프론트엔드 개발자가 작성하고, 뷰 코드는 백엔드 개발자가 작성하는 것이 일반적입니다. 이러한 템플릿과 뷰의 분리는 보안 측면에서 중요합니다.
 python의 for 반복문과 range 함수를 템플릿에서 사용한다면, 템플릿 코드에 python의 로직이 포함되기 때문에 보안상 취약점이 발생할 수 있습니다. 특히, 템플릿 코드에 복잡한 로직이 포함되면 웹 애플리케이션의 취약점이 노출될 수 있으며, 이는 악의적인 사용자에게 악용될 수 있습니다.</p>
</blockquote>
<p>다음으로 <code>test_app/templates/book/results.html</code> 파일을 추가하여 서평을 작성하고 제출을 했을 때 노출할 페이지를 만들어 줍니다.</p>
<pre><code class="language-html">&lt;h1&gt;{{ book.title }}&lt;/h1&gt;

&lt;p&gt;서평이 등록되었습니다.&lt;/p&gt;

&lt;a href=&quot;{% url &#39;bookstore:detail&#39; book.id %}&quot;&gt;돌아가기&lt;/a&gt;</code></pre>
<p>이제 <code>test_app/views.py</code>에 서평을 달 수 있는 뷰 코드를 작성합니다.</p>
<pre><code class="language-python">def book(request, book_id):
    book = get_object_or_404(TestBook, pk=book_id)
    return render(request, &quot;book/detail.html&quot;,  {&quot;book&quot;: book, &quot;range&quot;:range(10)})


def comment(request, book_id):
    book = get_object_or_404(TestBook, pk=book_id)
    comment = TestBookComment(
        book = book,
        content = request.POST[&quot;comment&quot;],
        score = request.POST[&quot;score&quot;]
    )
    comment.save()
    return HttpResponseRedirect(reverse(&quot;bookstore:results&quot;, args=(book.id,)))

def results(request, book_id):
     book = get_object_or_404(TestBook, pk=book_id)
     return render(request, &quot;book/results.html&quot;, {&quot;book&quot;:book})    
</code></pre>
<p>먼저 <code>book()</code>함수에 추천도 버튼을 만드는데 사용할 <code>range</code>변수를 추가해줍니다. 10개의 값이 필요하므로 range 변수에 <code>range(10)</code>함수를 담아 <code>book/detail.html</code>로 보내도록 수정해줍니다.</p>
<p><code>comment()</code>함수는 detail 뷰에서 서평 및 점수를 작성한 후 submit 버튼을 눌렀을 때 호출되는 함수입니다.
먼저 해당 책이 존재하는지 체크하고 있으면 해당 책의 서평을 저장하는 로직으로 동작합니다. 그 후 <code>bookstore:results</code> url로 리다이렉트 되도록 설정했습니다.</p>
<p>마지막으로 서평 작성 완료 화면을 노출시키기 위한 <code>result()</code>함수를 추가해 줍니다.</p>
<p>이제 작성한 내용은 url를 연결시켜주기 위해 <code>test_app/urls.py</code>를 수정합니다.</p>
<pre><code class="language-python">...
   path(&quot;&lt;int:book_id&gt;/comment&quot;, views.comment, name=&quot;comment&quot;),
   path(&quot;&lt;int:book_id&gt;/results&quot;, views.results, name=&quot;results&quot;),
...</code></pre>
<p>다 됐으면 서버를 기동시켜 봅니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/e634b981-28f6-47d4-8950-33ced09c805e/image.png" alt="자세히 보기 뷰"></p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/e75f8806-4000-467e-86f2-f70c776638c7/image.png" alt="서평 작성 결과 뷰"></p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/89b9e894-3e2d-4470-b8a3-aba4b9717c9e/image.png" alt="자세히 보기 뷰(서평추가)"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Django] django templates의 반복문 사용]]></title>
            <link>https://velog.io/@harry-jang/Django-django-templates%EC%9D%98-%EB%B0%98%EB%B3%B5%EB%AC%B8-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@harry-jang/Django-django-templates%EC%9D%98-%EB%B0%98%EB%B3%B5%EB%AC%B8-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Sat, 29 Jul 2023 08:00:05 GMT</pubDate>
            <description><![CDATA[<p>Django templates 엔진에는 python 코드로 부터 받아온 변수의 값을 사용할 수 있는 <strong><em>built-in template tags</em></strong>을 제공합니다. 
그 중 자주 사용하게 될 <strong><code>for</code></strong> 태그에 대해 간단히 정리해봤습니다.</p>
<p>반복문을 통해 리스트를 순회하려면 다음과 같이 사용합니다.</p>
<pre><code class="language-python">{% for x in x_list %}
    {{ x }}
{% endfor %}</code></pre>
<p>뒤에서 부터 역순으로 순회하기 위한 다음 옵션도 있습니다.</p>
<pre><code class="language-python">{% for x in x_list reversed %}
    {{ x }}
{% endfor %}</code></pre>
<p>그리고 다음과 같은 다양한 기본 변수들도 제공합니다.</p>
<h4 id="반복문-관련-제공-변수">반복문 관련 제공 변수</h4>
<table>
<thead>
<tr>
<th align="center">변수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">forloop.counter</td>
<td>반복문 순번 (1부터 시작해서 N로 끝남)</td>
</tr>
<tr>
<td align="center">forloop.counter0</td>
<td>반복문 순번 (0부터 시작해서 N-1로 끝남)</td>
</tr>
<tr>
<td align="center">forloop.revcounter</td>
<td>반복문 역순 순번(N부터 시작해서 1로 끝남)</td>
</tr>
<tr>
<td align="center">forloop.revcounter0</td>
<td>반복문 역순 순번(N-1부터 시작해서 0으로 끝남)</td>
</tr>
<tr>
<td align="center">forloop.first</td>
<td>반복문 첫 순서 체크</td>
</tr>
<tr>
<td align="center">forloop.last</td>
<td>반복문 마지막 순서 체크</td>
</tr>
</tbody></table>
<h4 id="비어있는-리스트에-대한-처리">비어있는 리스트에 대한 처리</h4>
<p><strong><em>for ... empty</em></strong> 태그를 통해 반복문을 실행하고자 하는 리스트가 비어 있을 경우에 대한 처리 방법을 제공합니다.</p>
<pre><code class="language-python">&lt;ul&gt;
{% for x in x_list %}
    &lt;li&gt;{{ x }}&lt;/li&gt;
{% empty %}
    &lt;li&gt;no items in this list.&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(10) - 민감정보 숨기기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B010-%EB%AF%BC%EA%B0%90%EC%A0%95%EB%B3%B4-%EC%88%A8%EA%B8%B0%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B010-%EB%AF%BC%EA%B0%90%EC%A0%95%EB%B3%B4-%EC%88%A8%EA%B8%B0%EA%B8%B0</guid>
            <pubDate>Sun, 23 Jul 2023 09:49:36 GMT</pubDate>
            <description><![CDATA[<p>오늘은 개발을 계속 진행하기 전에 챙겨야 할 것에 대해 알아보겠습니다.</p>
<p>따라오면서 자세히 보셨으면 아시겠지만, Database의 연결정보와 SecretKey 정보와 DB 연결정보 등의 보안이 필요한 민감성 정보들은 대부분 <code>setting.py</code>에 등록해서 사용하고 있습니다. 그렇기 때문에 이 파일을 그대로 <strong>github</strong> 등에 올리는 것은 보안위협에 그대로 노출되는 매우 어처구니 없는 상황이 발생하게 됩니다.</p>
<p>그래서 이번에는 settings의 민감성 정보들을 어떻게 숨기고 별도로 관리하는지에 대해 알아봅시다.</p>
<h4 id="시크릿-파일-생성">시크릿 파일 생성</h4>
<p>먼저 별도로 관리할 시크릿 파일을 하나 만듭니다. 저는 프로젝트 디렉토리 최상단 <code>manage.py</code>와 같은 위치에 <code>.secrets.json</code> 파일을 하나 생성했습니다.</p>
<pre><code class="language-json">{
    &quot;secretKey&quot; : &quot;iam-secret-key-for-django&quot;,

    &quot;databases&quot;:{
        &quot;common&quot;: {
            &quot;username&quot;: &quot;username123&quot;,
            &quot;password&quot;: &quot;password123&quot;,
            &quot;host&quot;: &quot;127.0.0.1&quot;,
            &quot;port&quot;: &quot;3306&quot;
        },
    }
}</code></pre>
<p>이 json 파일을 읽어올 수 있도록 <code>/test_project/settings.py</code>에 다음 코드를 추가합니다.</p>
<pre><code class="language-python">import os, json

secret_file = os.path.join(BASE_DIR, &#39;.secrets.json&#39;)

# secrets.json 파일을 읽은 후 secrets 변수에 저장
with open(secret_file) as f:
    secrets = json.loads(f.read())

# get_secret 함수를 생성하여 호출 시 해당 키 값 리턴
def get_secret(*args, secrets=secrets):
    if len(args) == 0:
        raise ValueError(&quot;At least one JSON string is required.&quot;)

    try:
        result = secrets
        for key in args[0:]:
            result = result[key]
        return result
    except KeyError as e:
        raise ValueError(f&quot;Key &#39;{str(e)}&#39; not found in the JSON data.&quot;)    </code></pre>
<p>이렇게 시크릿 파일을 읽을 준비가 되면, 민감성 정보가 필요한 곳에 적용해줍니다.</p>
<pre><code class="language-python">...
SECRET_KEY = get_secret(&quot;secretKey&quot;)

...

DATABASES = {
    &#39;default&#39;: {
        &#39;ENGINE&#39;: &#39;django.db.backends.mysql&#39;,
        &#39;NAME&#39;: &#39;common&#39;,
        &#39;USER&#39;: get_secret(&quot;databases&quot;, &quot;common&quot;, &quot;username&quot;),
        &#39;PASSWORD&#39;: get_secret(&quot;databases&quot;, &quot;common&quot;, &quot;password&quot;),
        &#39;HOST&#39;: get_secret(&quot;databases&quot;, &quot;common&quot;, &quot;host&quot;),
        &#39;PORT&#39;: get_secret(&quot;databases&quot;, &quot;common&quot;, &quot;port&quot;),
    },
...
</code></pre>
<p>이렇게 설정하고 정상적으로 서버가 동작하는지 확인해봅시다.
정상적으로 동작한다면 git에 푸시할 때 .secrets.json 파일은 .gitignore 파일에 추가하여 git 저장소에 같이 푸시되지 않도록 처리합니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/495ed906-4f51-4e4d-953b-55761cf9cb8f/image.png" alt=""></p>
<p>이렇게 작업하면 일단 민감성 정보 숨기는 작업을 끝났습니다.</p>
<h4 id="git-history에-이미-푸시된-민감정보-삭제">Git History에 이미 푸시된 민감정보 삭제</h4>
<p>만약 이미 git에 해당 정보가 포함된 settings.py가 git에 푸시되어 있다면, 이 작업을 해도 히스토리 보기를 통해 과거의 내용을 볼 수 있습니다.
이런 경우를 위해 히스토리까지 싹 날리는 여러 방법이 존재합니다.
그 중 간단한 방법으로 <code>git-filter-repo</code> 패키지를 이용하여 삭제해봅시다.</p>
<p>먼저 다음 명령어로 <code>git-filter-repo</code> 패키지를 설치합니다.</p>
<pre><code class="language-bash">sudo apt-get install git-filter-repo</code></pre>
<p>그리고 다음 명령을 통해 히스토리를 삭제할 파일을 정해 삭제합니다.</p>
<blockquote>
<p><strong>주의!</strong>
해당 명령을 실행하면 <code>settings.py</code> 파일자체가 삭제되어 버리니 시크릿파일을 통해 민감정보 가져오기 작업된 <code>settings.py</code> 파일은 따로 백업해놓고 해당 작업 이후 재커밋해주셔야 합니다.</p>
</blockquote>
<pre><code class="language-bash">git filter-repo --invert-paths --path config/settings.py --force</code></pre>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/fb2cf9d6-8892-4275-894e-e563fe736234/image.png" alt=""></p>
<p>이제 히스토리 보기를 해도 민감정보 작업 전 히스토리 내역은 보이지 않게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Windows Terminal] WSL2 Ubuntu가 갑자기 실행되지 않는 경우 해결법]]></title>
            <link>https://velog.io/@harry-jang/Windows-Terminal-WSL2-Ubuntu%EA%B0%80-%EA%B0%91%EC%9E%90%EA%B8%B0-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@harry-jang/Windows-Terminal-WSL2-Ubuntu%EA%B0%80-%EA%B0%91%EC%9E%90%EA%B8%B0-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Tue, 18 Jul 2023 14:03:39 GMT</pubDate>
            <description><![CDATA[<p>가끔 설정을 변경하거나 잘 못 건드리는 경우, Windows Terminal로 <code>Ubuntu</code> 실행 시 다음과 같은 화면이 뜨고 실행이 되지 않는 경우가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/4b3ff297-8412-430f-b302-5576267ba45a/image.png" alt="에러 화면"></p>
<p>이 경우 환경변수가 어떠한 이유로 인해 제거된 것으로 다시 환경변수를 추가해주면 정상적으로 동작하게 됩니다.</p>
<p><strong>[해결 방법]</strong></p>
<ol>
<li><p>Windows 키 + r 입력하여 실행창을 열어 <code>sysdm.cpl ,3</code> 입력 후 엔터. (혹은 작업표시줄 검색창에서 환경변수 검색)
 <img src="https://velog.velcdn.com/images/harry-jang/post/c7325e97-0507-43a1-82d3-23e5f7895685/image.png" alt=""></p>
</li>
<li><p>고급 탭에서 환경 변수(N)버튼 클릭
 <img src="https://velog.velcdn.com/images/harry-jang/post/b5244a83-3336-47b1-ae9c-7e206ce34d79/image.png" alt=""></p>
</li>
<li><p>사용자 변수 혹은 시스템 변수에서 <code>Path</code> 변수 편집버튼 클릭 
 <img src="https://velog.velcdn.com/images/harry-jang/post/089c0d5a-f465-4720-8b6a-ccd33007aac2/image.png" alt=""></p>
</li>
<li><p><code>Path</code> 변수항목에 <strong>C:\Users\{사용자이름}\AppData\Local\Microsoft\WindowsApps</strong>을 추가한다. _(Microsoft store를 통해 설치한 경우) _
 <img src="https://velog.velcdn.com/images/harry-jang/post/56ab76b7-0b93-4082-8515-a0ef655a51d4/image.png" alt=""></p>
</li>
<li><p>확인버튼을 눌러 설정창을 닫고 Windows Terminal 다시 실행하여 정상 출력 확인!
<img src="https://velog.velcdn.com/images/harry-jang/post/ed6a0487-267e-441f-81d1-e58c74a041c7/image.png" alt=""></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(9) - view 작성 및 template 연동하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B08-view-%EC%9E%91%EC%84%B1-%EB%B0%8F-template-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B08-view-%EC%9E%91%EC%84%B1-%EB%B0%8F-template-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 16 Jul 2023 15:02:31 GMT</pubDate>
            <description><![CDATA[<p><strong>Django</strong>의 디자인 패턴은 <code>MTV(Model - Template - View)</code>패턴입니다. <strong><code>Model</code></strong>은 DB에 저장되는 데이터, <strong><code>Template</code></strong>은 유저에게 보여지는 화면, <strong><code>View</code></strong>는 요청에 따라 적절한 로직을 수행하여 결과를 템플릿으로 렌더링하여 응답하는 것을 의미합니다. </p>
<p>오늘은 지금까지 작성했던 테스트코드를 바탕으로 아래와 같은 <code>View</code>를 작성하고, 사용자에게 보여질 <code>Template</code>연동까지 해보도록 하겠습니다. </p>
<ul>
<li>도서 <strong>색인</strong> 페이지 : 최근에 등록된 도서목록을 최근 5개까지 보여줍니다.</li>
<li>도서 <strong>상세</strong> 페이지 : 도서의 상세정보를 보여 줍니다.</li>
<li>도서 <strong>분류</strong> 페이지 : 카테고리별로 대상 카테고리에 속하는 도서 목록을 보여줍니다.</li>
</ul>
<h3 id="view-적용하기">View 적용하기</h3>
<p>먼저 도서 색인 페이지를 추가해보도록 하겠습니다.
<strong>test_app/views.py</strong> 파일 내의 <code>index()</code> 뷰를 다음과 같이 변경합니다.</p>
<pre><code class="language-python">from django.http import HttpResponse

def index(request):
    latest_book_list = TestBook.objects.order_by(&quot;-reg_datetime&quot;)[:5]
    output = &quot;, &quot;.join([b.title for b in latest_book_list])
    return HttpResponse(output)</code></pre>
<p>코드를 간략히 설명하면 <code>TestBook</code> 테이블 오브젝트로 부터 reg_datetime 내림차순으로 정렬된 데이터(오름차순으로 정렬하려면 &#39;-&#39; 제거) 중 상위 5개만 가져와 <code>latest_book_list</code> 변수에 담아 응답을 내려주고 있습니다.</p>
<p>다음으로 <strong>test_app/urls.py</strong> 파일을 수정하여 각 페이지로 접속할 수 있는 url를 매핑합니다.</p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [
    path(&quot;&quot;, views.index, name=&quot;index&quot;),
    path(&quot;book/&lt;int:book_id&gt;/&quot;, views.book, name=&quot;book&quot;),
    path(&quot;book/category/&lt;int:category_id&gt;/&quot;, views.bookCategory, name=&quot;category&quot;)
]</code></pre>
<h3 id="template-연동하기">Template 연동하기</h3>
<p>이제 템플릿을 연동하여 사용자에게 보여줄 화면을 그리는 작업을 진행하겠습니다.</p>
<p>먼저 <code>index()</code> 뷰 로직을 다음과 같이 수정합니다.</p>
<pre><code class="language-python">from django.http import HttpResponse

def index(request):
    latest_book_list = TestBook.objects.order_by(&quot;-reg_datetime&quot;)[:5]
    template = loader.get_template(&quot;index.html&quot;)
    context = {&quot;latest_book_list&quot; : latest_book_list,}
    return HttpResponse(template.render(context, request))</code></pre>
<p>기존 로직으로 <code>latest_book_list</code>를 가져와서 <code>context</code>변수에 담고 <strong>index.html</strong> 템플릿을 불러와서 동적으로 <strong>index.html</strong>에 <code>context</code> 데이터를 넣어 화면을 그려서 응답을 주도록 코드를 작성했습니다.   </p>
<p>다음으로 템플릿으로 사용할 <strong>index.html</strong> 파일을 만들어봅시다.</p>
<p>기본적으로 django 템플릿은 <strong>/templates</strong> 디렉토리에서 스캔하게 되므로 <strong>test_app/templates</strong> 폴더를 하나 생성합니다.</p>
<p>그리고 나서 도서 색인페이지를 위한 <strong>index.html</strong> 파일을 아래 코드와 같이 생성합니다. </p>
<pre><code class="language-html">&lt;h3&gt;신간도서&lt;/h3&gt;
{% if latest_book_list %}
&lt;ul&gt;
  {% for book in latest_book_list %}
  &lt;li&gt;&lt;a href=&quot;test/book/{{ book.id }}/&quot;&gt;{{ book.title }}&lt;/a&gt;&lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;
{% else %}
&lt;p&gt;No books are available.&lt;/p&gt;
{% endif %}
</code></pre>
<p>중괄호로 시작하는 부분들이 <code>DjangoTemplates</code>언어로 작성된 코드들이며 HTML 마크업 사이에 끼어서 동적으로 데이터를 표시하여 유저에게 보여줄 수 있도록 합니다.</p>
<blockquote>
<p><strong>DjangoTemplates이란?</strong>
 Django 웹 프레임워크에서 사용되는 템플릿 언어입니다.  Template은 HTML과 Python 코드를 결합하여 동적으로 데이터를 표시하고 조작할 수 있도록 도와줍니다.
<strong>settings.py</strong>에서 <code>TEMPLATES</code>항목을 통해 템플릿 엔진을 설정할 수 있으며, 기본값으로 <code>DjangoTemplates</code>를 사용하도록 설정되어 있습니다.</p>
</blockquote>
<h4 id="shortcut-모듈을-활용하여-코드-간결화하기">Shortcut 모듈을 활용하여 코드 간결화하기</h4>
<p>템플릿에 <code>context</code>를 채워넣어 표현한 결과를 <code>HttpResponse</code> 객체와 함께 돌려주는 구문은 자주 쓰는 용법입니다. <strong>Django</strong>는 이러한 작업을 쉽게 작성 할 수 있도록 단축 기능(shortcuts)을 제공합니다. <code>index()</code> 뷰를 단축 기능으로 작성하면 다음과 같습니다.</p>
<pre><code class="language-python">from django.shortcuts import render

def index(request):
    latest_book_list = TestBook.objects.order_by(&quot;-reg_datetime&quot;)[:5]
    context = {&quot;latest_book_list&quot; : latest_book_list,}
    return render(request, &quot;index.html&quot;, context)</code></pre>
<p>모든 뷰에 적용한다면 더 이상 <code>loader</code>와 <code>HttpResponse</code>를 별도로 임포트하지 않아도 되기 때문에 앞으로 나올 코드들은 <code>shortcuts</code>를 적용하여 코드를 작성하도록 하겠습니다.</p>
<p>이제 코드를 실행해서 인덱스 페이지에 접근하면 다음과 같은 화면이 노출됩니다. 책의 목록은 관리자페이지에서 임의로 생성하였으므로, 실습하실 때 여러개 임의로 추가해서 확인하면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/35e68828-168f-4e85-9842-c41d47e1e82a/image.png" alt="도서 색인 페이지"></p>
<h3 id="에러-발생시키기">에러 발생시키기</h3>
<p>이제 도서 상세 페이지와 도서 분류페이지도 작성하도록 하겠습니다. 코드를 작성하면서 존재하지 않는 상세 페이지나 분류페이지를 호출했을 때 에러를 노출하도록 해보겠습니다.</p>
<p>먼저 <strong>/test_app/views.py</strong> 파일에 다음 함수들을 추가합니다.</p>
<pre><code class="language-python">from django.http import Http404
//...

def book(request, book_id):
    try:
        book = TestBook.objects.get(pk=book_id)
    except TestBook.DoesNotExist:
        raise Http404(&quot;The Book does not exist&quot;)
    return render(request, &quot;book/detail.html&quot;,  {&quot;book&quot;: book})

def bookCategory(request, category_id):
    try:
        category = TestBookCategory.objects.get(pk=category_id)
        books = TestBook.objects.filter(category_id=category_id)
    except TestBookCategory.DoesNotExist:
        raise Http404(&quot;The Category does not exist&quot;)
    return render(request, template_name=&quot;category/detail.html&quot;, context={&quot;category&quot;: category, &quot;books&quot;:books})
</code></pre>
<p><strong>Django</strong>는 Python의 <code>try-except</code>문을 이용하여 예외를 잡아낼 수 있으며 이를 이용하여 해당 테이블 오브젝트에 요청된 데이터가 없을 시에 Http404에러를 던지도록 코드를 작성했습니다. </p>
<h4 id="shortcut-모듈을-활용하여-코드-간결화하기-1">Shortcut 모듈을 활용하여 코드 간결화하기</h4>
<p>404에러를 반환하는 로직도 render 로직과 마찬가지로 Shortcut을 제공하여 간결하게 표현할 수 있습니다.</p>
<pre><code class="language-python">from django.shortcuts import get_object_or_404, render
//...

def book(request, book_id):
    book = get_object_or_404(TestBook, pk=book_id)
    return render(request, &quot;book/detail.html&quot;,  {&quot;book&quot;: book})

def bookCategory(request, category_id):
    category = get_object_or_404(TestBookCategory, pk=category_id)
    books = TestBook.objects.filter(category_id=category_id)
    return render(request, template_name=&quot;category/detail.html&quot;, context={&quot;category&quot;: category, &quot;books&quot;:books})</code></pre>
<h3 id="템플릿-활용하기">템플릿 활용하기</h3>
<p>도서 상세 페이지와 도서 분류 페이지의 템플릿파일을 <strong>/test_app/templates/book/detail.html</strong> 과 <strong>/test_app/templates/category/detail.html</strong>에 각각 생성한 후 템플릿을 다음과 같이 작성합니다.</p>
<h4 id="bookdetailhtml">/book/detail.html</h4>
<pre><code class="language-html">&lt;h3&gt;{{ book.title }}&lt;/h3&gt;
&lt;table border=&quot;1&quot;&gt;
  &lt;th&gt;도서번호&lt;/th&gt;
  &lt;th&gt;카테고리&lt;/th&gt;
  &lt;th&gt;가격&lt;/th&gt;
  &lt;th&gt;등록일자&lt;/th&gt;
  &lt;tr&gt;
    &lt;td&gt;{{ book.id }}&lt;/td&gt;
    &lt;td&gt;{{ book.category.categoryName }}&lt;/td&gt;
    &lt;td&gt;{{ book.price }}&lt;/td&gt;
    &lt;td&gt;{{ book.reg_datetime }}&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;</code></pre>
<h4 id="categorydetailhtml">/category/detail.html</h4>
<pre><code class="language-html">&lt;h3&gt;{{ category.categoryName }}&lt;/h3&gt;
&lt;table border=&quot;1&quot;&gt;
  &lt;th&gt;도서명&lt;/th&gt;
  {% for book in books %}
  &lt;tr&gt;
    &lt;td&gt;{{ book.title}}&lt;/td&gt;
  &lt;/tr&gt;
  {% endfor %}
&lt;/table&gt;</code></pre>
<p><strong>/book/detail.html</strong>과 같이 템플릿 시스템에서 각 변수의 속성에 접근하기 위해서는 점-탐색(dot-lookup)문법을 사용해서 접근합니다. <strong>django</strong>는 먼저 <code>book</code> 객체에 대해 사전형으로 먼저 탐색을 시도하고 실패하면 속성값으로 탐색을 시도합니다. 속성으로도 탐색을 실패하면 리스트의 인덱스 탐색을 시도합니다.</p>
<p><strong>/category/detail.html</strong>과 같이 <code>{% for %}</code>을 이용하여 반복분도 사용할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/28169207-5c4e-428b-b2bf-66a8603c3e59/image.png" alt="도서 상세 페이지">
<img src="https://velog.velcdn.com/images/harry-jang/post/6e649f48-0320-42af-8d96-74e87e4c66cb/image.png" alt="도서 분류 페이지"></p>
<h3 id="템플릿에서-하드코딩된-url-제거하기">템플릿에서 하드코딩된 url 제거하기</h3>
<p>앞서 <strong>/test/index.html</strong>에서 상세 페이지로 접근하는 링크가 하드코딩되어 있었습니다. 하드코딩된 URL은 코드가 강한 의존성을 갖게 되어 수많은 템플릿을 가질수록 수정하기 힘들어집니다.</p>
<p>이 부분을 해소하기 위해 템플릿에서 하드코딩 된 url을 제거해봅시다.</p>
<p><strong>/test_app/urls.py</strong>에서 path() 함수에서 인수의 이름을 정의했으므로, {% url %} template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성을 제거할 수 있습니다.</p>
<p><strong>/test_app/templates/index.html</strong></p>
<pre><code class="language-html">&lt;h3&gt;신간도서&lt;/h3&gt;
{% if latest_book_list %}
&lt;ul&gt;
  {% for book in latest_book_list %}
  &lt;li&gt;&lt;a href=&quot;{% url &#39;book&#39; book.id %}&quot;&gt;{{ book.title }}&lt;/a&gt;&lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;
{% else %}
&lt;p&gt;No books are available.&lt;/p&gt;
{% endif %}</code></pre>
<h3 id="url의-네임스페이스-지정하기">URL의 네임스페이스 지정하기</h3>
<p>실제로 <strong>Django</strong> 프로젝트는 앱이 여러개가 될 수 있습니다. 그렇게 되면 이 앱들간의 URL을 구분하는 방법이 필요해지게 됩니다. </p>
<p>예를 들어 우리가 만든 <strong>test_app</strong>의 <code>index</code>라는 이름의 url이 있지만 다른 앱에서도 <code>index</code>라는 url이 있을 경우 서로 URL을 구분할 수 있어야 합니다.</p>
<p>즉, <strong>Django</strong>가 <code>{% url %}</code> 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 알 수 있어야 합니다. </p>
<p>이것은 <code>URLconf</code>에 네임스페이스(namespace)을 추가하여 해결할 수 있습니다.</p>
<p><strong>test_app/urls.py</strong>파일에 다음과 같이 <code>app_name</code>을 추가하여 어플리케이션의 네임스페이스를 설정할 수 있습니다.</p>
<p><strong>test_app/urls.py</strong></p>
<pre><code class="language-python">app_name = &quot;bookstore&quot;
urlpatterns = [
    path(&quot;&quot;, views.index, name=&quot;index&quot;),
    path(&quot;book/&lt;int:book_id&gt;/&quot;, views.book, name=&quot;book&quot;),
    path(&quot;book/category/&lt;int:category_id&gt;/&quot;, views.bookCategory, name=&quot;category&quot;)
]</code></pre>
<p><strong>test_app/templates/index.html</strong></p>
<pre><code class="language-html">&lt;h3&gt;신간도서&lt;/h3&gt;
{% if latest_book_list %}
&lt;ul&gt;
  {% for book in latest_book_list %}
  &lt;li&gt;&lt;a href=&quot;{% url &#39;bookstore:book&#39; book.id %}&quot;&gt;{{ book.title }}&lt;/a&gt;&lt;/li&gt;
  {% endfor %}
&lt;/ul&gt;
{% else %}
&lt;p&gt;No books are available.&lt;/p&gt;
{% endif %}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(8) - Django 관리자 살펴보기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B08-Django-%EA%B4%80%EB%A6%AC%EC%9E%90-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B08-Django-%EA%B4%80%EB%A6%AC%EC%9E%90-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 13 Jul 2023 14:34:04 GMT</pubDate>
            <description><![CDATA[<h2 id="django-관리자-사이트">Django 관리자 사이트</h2>
<p>컨텐츠를 수정하기 위해 관리자 사이트를 별도로 따로 만들 필요없이 제법 그럴듯한 관리자 사이트를 제공합니다. Django는 모델에 대한 관리용 인터페이스를 모두 자동 생성하고, 이를 admin 모듈에 알려주기만 하면 바로 관리 기능을 사용할 수 있습니다. </p>
<h3 id="관리자-계정-생성하기">관리자 계정 생성하기</h3>
<p>먼저 다음 명령으로 관리자 사이트에 로그인 할 수 있는 사용자를 생성합니다.</p>
<pre><code class="language-bash">$ python manage.py createsuperuser
Username (leave blank to use &#39;admin&#39;): harry
Email address: harry@email.com
Password:
Password (again):
Superuser created successfully.</code></pre>
<ul>
<li><strong>Username</strong> : 원하는 운영자 id를 입력합니다.</li>
<li><strong>Email address</strong> : 원하는 이메일 주소를 입력합니다.</li>
<li><strong>Password</strong> : 로그인 시 사용할 암호를 입력합니다. (두 번 입력)</li>
</ul>
<h3 id="관리자-사이트-접속하기">관리자 사이트 접속하기</h3>
<p>관리자 사이트는 별도 프로젝트가 따로 있는게 아니라 우리가 작업하던 프로젝트에 같이 포함되어 실행이 됩니다.
다음 명령으로 서버를 시작합니다.</p>
<pre><code class="language-bash">$ python manage.py runserver</code></pre>
<p>서버가 뜨면 웹 브라우저를 열고 <code>/admin</code> 도메인으로 이동합니다. (예: <a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a>). </p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/2670a186-6c06-46f0-b421-5200b2f9245c/image.png" alt="어드민 사이트 로그인 페이지"></p>
<p>로그인 화면이 노출되면 위에서 생성한 운영자 계정으로 로그인을 합니다. 
<img src="https://velog.velcdn.com/images/harry-jang/post/3c2e197e-e7f6-4ebc-b8ea-a3a580424d16/image.png" alt="어드민 사이트 인덱스 페이지">
로그인에 성공하게 되면 다음과 같은 화면이 보입니다. 이 페이지에서는 그룹과 사용자의 인증,권한을 관리하는 기능이 보입니다. 이 기능들은 <code>django.contrib.auth</code> 모듈에서 제공하는데 <strong>INSTALLED_APPS</strong>항목에 기본적으로 등록되어 있던 모듈입니다. </p>
<p>그런데 관리자페이지에 권한관리 기능만 있으니 허전합니다. 우리가 만든 앱에 대해서 관리 페이지를 만들어 봅시다. </p>
<h3 id="관리자-사이트에-관리기능-등록하기">관리자 사이트에 관리기능 등록하기</h3>
<p>우리가 만든 모델 클래스의 관리 인터페이스가 있다는 것을 인식시키기 위해 <code>test_app/admin.py</code>파일을 열어 다음과 같이 모델을 등록하고 코드를 추가합니다. </p>
<pre><code class="language-python">from django.contrib import admin
from .models import TestBook
from .models import TestBookCategory
from .models import TestMember

# Register your models here.
admin.site.register(TestBook)
admin.site.register(TestBookCategory)
admin.site.register(TestMember)</code></pre>
<p>코드를 추가한 뒤 다시 실행시켜 접속해보면 다음과 같이 테이블 모델들의 목록이 보이게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/feb0bddd-8040-4c5c-904b-03b89f1ca4f2/image.png" alt="변경된 어드민 사이트 인덱스 페이지"></p>
<p>여기서 Test Books 항목으로 들어가보면, 이전에 추가한 책 목록들이 보이고, 여기서 <code>ADD TEST BOOK</code>버튼을 눌러 신규 책을 등록할 수 있습니다.</p>
<p>그리고 여기서 기존에 등록했던 책 타이틀을 클릭하면 내용을 수정할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/1b747639-f17a-4a38-bf76-937787ea4a6b/image.png" alt="Test Book 수정 페이지"></p>
<p>모델의 각 필드 유형들은 <code>models.py</code>에 작성된 타입에 맞게 적절한 HTML 입력 위젯으로 표현됩니다. 그리고 연결관계로 묶여있는 <strong>Category</strong>까지 여기서 추가, 편집할 수 있다니 신경써서 잘 만든게 느껴지네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(7) - 테이블 모델을 통해 스키마 구성하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B07-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%AA%A8%EB%8D%B8%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B07-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%AA%A8%EB%8D%B8%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 12 Jul 2023 14:12:19 GMT</pubDate>
            <description><![CDATA[<h2 id="모델-직접-만들어보기">모델 직접 만들어보기</h2>
<p>지난 번에는 DB에 수동으로 테이블을 생성한 뒤 <code>inspectdb</code> 명령을 통해서 만들어진 모델을 이용해서 테이블에 데이터를 삽입해 봤다면, 이번에는 모델을 통해 테이블 생성 및 데이터 삽입하는 방법을 알아보겠습니다.</p>
<p>먼저 models.py에 신규로 생성할 테이블의 모델을 추가합니다.</p>
<pre><code class="language-python">from django.db import models

...
class TestBookCategory(models.Model):
    categoryName = models.CharField(max_length=20)

class TestBook(models.Model):
    title = models.CharField(max_length=200)
    category = models.ForeignKey(TestBookCategory, on_delete=models.SET_NULL, null=True)
    price = models.IntegerField()
    reg_datetime = models.DateTimeField()
...</code></pre>
<p>데이터베이스의 각 필드는 <a href="https://docs.djangoproject.com/ko/4.2/ref/models/fields/#django.db.models.Field"><code>Field</code></a>클래스의 인스턴스로 표현합니다. 이 작업을 통해 각 필드가 어떤 자료형을 가질 수 있는지를 Django에게 말해줍니다.</p>
<ul>
<li><p>CharField : 문자(character)형 타입 필드</p>
</li>
<li><p>DateTimeField : 날짜와 시간형 타입 필드</p>
</li>
<li><p>IntegerField : 정수형 타입 필드</p>
</li>
<li><p>AutoField : 자동증가하는 정수형 타입 필드, 주로 pk를 저장할 때 사용하지만 django 내부적으로 id라는 컬럼을 자동생성하므로, A model can&#39;t have more than one AutoField 에러가 발생할 수 있습니다. 이를 방지하려면 id를 생성하지 않고 해당 필드가 PK가 되도록 <code>primary_key=True</code>옵션을 추가해야 합니다.</p>
<p>ex) category_id = models.AutoField(<code>primary_key=True</code>)</p>
</li>
</ul>
<p>이 외의 필드 타입에 대한 내용은 다음 <a href="https://docs.djangoproject.com/ko/4.2/ref/models/fields/#field-types">링크</a>를 참고하여 사용하도록 합니다.</p>
<p>ForeignKey 클래스를 통해 테이블 간의 관계를 설정할 수도 있습니다.
Django 는 다-대-일(many-to-one), 다-대-다(many-to-many), 일-대-일(one-to-one) 과 같은 모든 일반 데이터베이스의 관계들를 지원합니다.</p>
<h2 id="모델-활성화하기">모델 활성화하기</h2>
<p>이 모델을 통해 Django에서는 다음과 같은 작업을 진행합니다.</p>
<ul>
<li>이 앱을 위한 데이터베이스 스키마 생성(CREATE TABLE 문)</li>
<li>TestBook과 TestBookCategory 객체에 접근하기 위한 Python 데이터베이스 접근 API를 생성</li>
</ul>
<p>위 작업을 하기 위해 먼저 <strong>test_project</strong>프로젝트 내 <code>settings.py</code>파일의 <strong><code>INSTALLED_APPS</code></strong>에 앱의 구성 클래스에 대한 참조를 추가하여 <strong>test_app</strong>앱이 설치되어 있다는 것을 알려줍니다. 
이 구성 클래스는 앱이름 + Config이라는 이름으로 생성되어 있으며, <code>test_app/apps.py</code>파일 내에 존재합니다.
이 내용을 바탕으로 <strong><code>INSTALLED_APPS</code></strong>에 구성 클래스를 추가합니다. (이전 포스팅에서 <code>test_app</code> 패키지를 통으로 넣어줬다면 별도로 작업하지 않아도 됩니다.)</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &quot;test_app.apps.TestAppConfig&quot;,
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
]</code></pre>
<p>Django에 <strong>test_app</strong>앱이 포함된 것을 알려줬다면 다음 명령을 실행합니다.</p>
<pre><code class="language-bash">$ python manage.py makemigrations test_app</code></pre>
<p><code>makemigrations</code> 을 실행하게 되면 모델을 생성 혹은 변경시킨 사실과 이 변경사항을 <code>migration</code>으로 저장시키고 싶다는 것을 Django에게 알려주게 됩니다.</p>
<p><strong><code>Migration</code></strong>은 Django가 모델(즉, 데이터베이스 스키마)의 변경사항을 디스크에 저장하는 방법입니다.
<code>test_app/migrations/0001_initial.py</code>파일에 변경된 새 모델에 대한 migration을 읽어 볼 수 있습니다. 이 파일을 직접 수정할 수 있도록 설계가 되어 있어 Django의 변경점을 수동으로 수정할 수도 있습니다.</p>
<p>다음으로 <code>sqlmigrate</code> 명령을 통해 <strong><code>Migration</code></strong>이 내부적으로 어떤 SQL 문장을 실행하는지 살펴 볼 수 있습니다.</p>
<pre><code class="language-bash">$ python manage.py sqlmigrate test_app 0001</code></pre>
<p>저의 경우에는 MySQL을 연동하여 다음과 같이 쿼리가 표시되며 이 내용은 연동하는 DB마다 다를 수 있습니다.</p>
<p><code>sqlmigrate</code>은 실제로 DB에 마이그레이션이 되지 않고 화면에 출력해줍니다. 단지 마이그레이션에 사용할 SQL문을 보여주는 것이며, 내가 작성한 테이블 모델이 어떻게 db에 마이그레이션이 될지 확인하는 용도로 유용하게 사용할 수 있습니다.</p>
<pre><code class="language-sql">--
-- Create model TestBookCategory
--
CREATE TABLE `core_testbookcategory` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `categoryName` varchar(20) NOT NULL);
--
-- Create model TestBook
--
CREATE TABLE `core_testbook` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `title` varchar(200) NOT NULL, `price` integer NOT NULL, `reg_datetime` datetime(6) NOT NULL, `category_id` bigint NULL);
ALTER TABLE `core_testbook` ADD CONSTRAINT `core_testbook_category_id_11be1c0d_fk_core_testbookcategory_id` FOREIGN KEY (`category_id`) REFERENCES `core_testbookcategory` (`id`);</code></pre>
<p><code>sqlmigrate</code>로 출력된 쿼리를 실제로 적용하려면 <code>migrate</code> 명령을 사용합니다.</p>
<pre><code class="language-bash">$ python manage.py migrate</code></pre>
<p><code>migrate</code> 명령은 아직 적용되지 않은 마이그레이션을 모두 수집해서 실행하며, 이 과정을 통해 모델에서의 변경 사항들과 데이터베이스의 스키마의 동기화가 이루어집니다.</p>
<p>django의 이 강력한 기능은 DB나 테이블을 거의 직접 다루지 않고도 모델을 생성하고 반복적인 변경도 가능하게 해줍니다. 게다가 동작 중인 데이터베이스를 자료 손실 없이 업그레이드하는데 최적화 되어 있습니다.</p>
<p>이 일련의 작업들이 복잡해 보일 수도 있지만 이 단계만 기억하면 됩니다.</p>
<ol>
<li><strong>models.py</strong> 에서 모델을 변경합니다.</li>
<li><code>makemigrations</code>을 통해 이 변경사항에 대한 마이그레이션을 생성합니다.</li>
<li><code>migrate</code>명령을 통해 변경사항을 데이터베이스에 적용합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(6) - 데이터베이스 데이터 삽입하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B06-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B06-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 09 Jul 2023 14:33:53 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터-삽입하기">데이터 삽입하기</h2>
<p>지난 번 포스팅에 이어 API를 만들어 테이블에 데이터를 삽입(insert)을 진행하도록 하겠습니다.</p>
<p><strong>test_app/views.py</strong>에 다음 코드를 추가합니다.</p>
<pre><code class="language-python">...
from rest_framework import status



@api_view([&#39;POST&#39;])
def setTestMember(request):
    reqData = request.data
    serializer = TestMemberSerializer(data=reqData)
    if serializer.is_valid():
        serializer.save()
        return Response(serializer.data)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

...</code></pre>
<p><code>@api_view([&#39;POST&#39;])</code> 어노테이션 지정하여 해당 view에 접근했을 때 <code>POST</code> 메소드로 데이터를 전송하는 화면이 뜨도록 합니다. 
그리고 데이터를 입력하여 POST버튼을 눌러 데이터가 전달되면 <code>is_valid()</code>함수로 데이터의 유효성을 확인한 뒤 성공하면 <code>save()</code>함수로 DB에 삽입하고, 유효하지 않으면 <code>400 Bad Request</code>라는 상태코드를 넘겨주도록 했습니다. 이번에는 실패시 응답에 <code>200 OK</code>가 아닌 <strong>HttpStatusCode</strong>를 넘겨주기 위해 <code>status</code>모듈을 import 시켜주었습니다.</p>
<p>그리고 <strong>test_app/urls.py</strong>에 해당 view가 호출될 url를 추가시켜 줍니다.</p>
<pre><code class="language-python">...
urlpatterns = [
    path(&quot;member&quot;, views.setTestMember, name=&quot;setMembers&quot;),
...
]
...</code></pre>
<p>이제 서버를 기동시켜 브라우저에서 <code>http://127.0.0.1:8000/test/member/</code>로 접근해봅시다.
정상적으로 작성됐다면 다음과 같은 화면이 뜨게 됩니다.
<img src="https://velog.velcdn.com/images/harry-jang/post/89893dbe-c561-4a15-940f-71efc3fd1073/image.png" alt="member 등록 화면">
해당 페이지 하단의 입력 항목들을 채워 넣고 POST 버튼을 누릅니다.
<strong>Media type</strong> : application/json
<strong>Content</strong> :</p>
<pre><code class="language-json">{
  &quot;name&quot; : &quot;peter&quot;,
  &quot;age&quot; : 17,
  &quot;created_at&quot; : &quot;2023-07-09 23:21:44&quot;
}</code></pre>
<p><strong>요청 성공</strong>
<img src="https://velog.velcdn.com/images/harry-jang/post/87cfef86-2a4c-4fcb-98dd-328ed1c5e2f9/image.png" alt="성공시">
위 그림과 같이 성공응답과 삽입된 데이터 내용이 표시되며, 실제 DB에서 조회시에도 데이터가 삽입된 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/harry-jang/post/3af0fd85-f0b1-42ff-aa48-6be1d8e00574/image.png" alt="mysql 워크벤치"></p>
<p><strong>요청 실패</strong>
<img src="https://velog.velcdn.com/images/harry-jang/post/0709be1b-5302-49d4-bbfc-93ce2b335c1a/image.png" alt="실패시">
요청 실패시에는 코드에서 작성한 것과 같이 <code>400 Bad Request</code> 상태코드가 전달되고, 어떤 부분에서 실패가 났는지에 대한 내용이 표시됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(5) - 데이터베이스 데이터 읽기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B05-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BD%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B05-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BD%EA%B8%B0</guid>
            <pubDate>Sat, 08 Jul 2023 10:09:54 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터-읽어오기">데이터 읽어오기</h2>
<h3 id="테이블-생성하기">테이블 생성하기</h3>
<p>Mysql 워크벤치를 통해서 사용할 테이블을 하나 생성합니다.</p>
<pre><code class="language-sql">CREATE TABLE testdb.test_member(
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    age INT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT NOW(), 
    updated_at DATETIME NULL DEFAULT NULL ON UPDATE NOW(),
    PRIMARY KEY (id)
);</code></pre>
<p>확인을 위해 생성된 테이블에 데이터도 몇개 삽입해줍니다.</p>
<pre><code class="language-sql">INSERT INTO `testdb`.`test_member`
(`name`,
`age`)
VALUES
(&#39;harry&#39;, 34),
(&#39;john&#39;, 42),
(&#39;rachel&#39;, 24);
</code></pre>
<h3 id="모델-생성하기">모델 생성하기</h3>
<p>모델이란 부가적인 메타데이터를 가진 데이터베이스의 구조(layout)를 말합니다.
<strong>Django</strong>의 <code>models.py</code>로 테이블을 생성할 수 있지만 반대로 기존에 존재하는 DB의 테이블을 가져올 수도 있습니다. 
이미 존재하는 테이블을 <code>model.py</code>로 만드려면 아래의 명령을 프롬프트에 입력합니다.</p>
<pre><code class="language-bash">$ python manage.py inspectdb &gt; models.py</code></pre>
<p>이렇게 입력하면 명령을 입력한 디렉토리에 default로 연결된 db에 대한 모델파일인 <code>models.py</code> 파일이 만들어집니다.
만약 default db외에 별도의 db를 연결한 상태라면 아래의 명령으로 해당 db의 모델파일을 생성할 수 있습니다.</p>
<pre><code class="language-bash">$ python manage.py inspectdb --datebase DB명 &gt; models.py</code></pre>
<p>만들어진 <code>models.py</code>를 <strong>test_app/</strong> 디렉토리 하위에 넣어서 덮어쓰기를 합니다.</p>
<h3 id="특정-테이블의-데이터-모두-읽어-들이기">특정 테이블의 데이터 모두 읽어 들이기</h3>
<p>데이터를 읽어들이기 전에 <strong>Django</strong>의 API 기능을 사용하기 위해 아래의 명령으로 패키지를 설치합니다.</p>
<pre><code class="language-bash">$ pip install djangorestframework</code></pre>
<p>설치한 패키지를 사용하기 위해 <code>settings.py</code>파일을 수정해보겠습니다. <code>rest_framework</code>와 우리가 만든 앱인 <code>test_app</code>도 같이 포함시켜 <code>INSTALLED_APPS</code> 항목에 추가해 줍니다.</p>
<pre><code class="language-python">...
INSTALLED_APPS = [
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,

    &#39;test_app&#39;,
    &#39;rest_framework&#39;,
]
...</code></pre>
<p>다음으로 모델을 직렬화하기 위해 <strong>test_app/</strong> 디렉토리 하위에 serializer.py 파일을 생성하고 다음과 같은 코드를 작성합니다.</p>
<pre><code class="language-python">from rest_framework.serializers import ModelSerializer
from models import TestMember

class TestMemberSerializer(ModelSerializer):
    class Meta:
        model = TestMember
        fields = &#39;__all__&#39;</code></pre>
<blockquote>
<p><strong>직렬화란?</strong>
객체를 바이트 스트림으로 변환하는 것, 즉 객체에 저장된 데이터 스트림에 쓰기 위해 연속적인 직렬 데이터로 변환하는 것이다.
쉬운 예로 DB에서 불러온 데이터를 json으로 보여주기 위해 가공하는 부분으로 이해하시면 될 것 같습니다.</p>
</blockquote>
<p>코드에 대해 간략히 설명하자면, 아까 설치했던 <code>rest_framework</code> 패키지내의 <code>ModelSerializer</code>와 <code>inspectdb</code> 명령을 통해 생성되었던 <code>models.py</code> 내의 TestMember 테이블 클래스을 가져와서 <strong>test_member</strong> 테이블의 데이터를 직렬화하여 TestMember 클래스로 변환해주는<code>TestMemberSerializer</code> 클래스를 생성하였습니다.
이제 TestMember 클래스 오브젝트로 db데이터를 읽어오고, TestMemberSerializer로 사용자가 볼 수 있게 데이터를 변환할 수 있게 되었습니다.</p>
<p>확인을 위해 인덱스 페이지를 수정해 봅시다. <code>views.py</code>파일을 열어 다음과 같이 수정합니다.</p>
<pre><code class="language-python">from .models import TestMember
from .serializer  import TestMemberSerializer
from django.http import HttpResponse

def index(request):
    datas = TestMember.objects.all()
    serializer = TestMemberSerializer(datas, many=True)

    return HttpResponse(serializer.data)</code></pre>
<p>서버를 실행하여 인덱스 페이지를 열어봅니다.</p>
<pre><code>$ python manage.py runserver</code></pre><p><img src="https://velog.velcdn.com/images/harry-jang/post/de6049b9-01ce-4488-b775-213a6e018558/image.png" alt="">
다음과 같은 내용이 노출된다면 DB에서 데이터를 정상적으로 가져온 것입니다.</p>
<p>rest_framework 패키지를 설치한 김에 API 형태로도 내용을 노출해봅시다.
<code>views.py</code>를 다음과 같이 작성합니다.</p>
<pre><code class="language-python">from rest_framework.response import Response
from rest_framework.decorators import api_view
from .models import TestMember
from .serializer  import TestMemberSerializer
from django.http import HttpResponse

@api_view([&#39;GET&#39;])
def getTestMembers(request):
    datas = TestMember.objects.all()
    serializer = TestMemberSerializer(datas, many=True)
    return Response(serializer.data)

@api_view([&#39;GET&#39;])
def getTestMember(request, name):
    data = TestMember.objects.get(name=name)
    serializer = TestMemberSerializer(data, many=False)
    return Response(serializer.data)

def index(request):
    datas = TestMember.objects.all()
    serializer = TestMemberSerializer(datas, many=True)

    return HttpResponse(serializer.data)</code></pre>
<p>그리고 <strong>test_app/</strong>내의 <code>urls.py</code> 파일에 추가한 api의 url를 등록해줍니다.</p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [
    path(&quot;members/&quot;, views.getTestMembers, name=&quot;getMembers&quot;),
    path(&quot;member/&lt;str:name&gt;&quot;, views.getTestMember, name=&quot;getMember&quot;),
    path(&quot;hello/&quot;, views.index, name=&quot;index&quot;),
]</code></pre>
<p><code>/members</code> API는 테이블 데이터 전체조회를 하는 API이고,
<code>/member/&lt;str:name&gt;</code> API는 테이블에서 해당되는 이름의 데이터만 검색하는 API입니다.  <code>&lt;str:name&gt;</code>은 <strong>member/</strong> 뒤에 붙는 <strong>Path Parameter</strong>로 검색할 이름을 저장하는 <code>String</code> 타입의 변수입니다.</p>
<p>예를 들어 &#39;harry&#39;라는 이름의 멤버를 검색하려면 <strong>/member/harry</strong>로 url을 호출하면 harry에 대한 정보만 검색이 됩니다. </p>
<p>작성을 마치고 브라우저를 통해 호출해보면 아래 그림과 같이 정상적으로 데이터를 읽어올 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/9e7c3ec8-e3df-4793-92d9-5c1926a6ba50/image.png" alt="멤버 전체 조회"></p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/ed9606ba-2325-436e-8259-953f1a8731bc/image.png" alt="멤버 검색"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(4) - 데이터베이스 연동하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B03-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B03-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 05 Jul 2023 14:22:37 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터베이스-설치">데이터베이스 설치</h2>
<p>데이터베이스를 연동하려면 <strong>test_project/settings.py</strong> 파일을 수정해야 합니다.
이 파일은 Django 설정을 모듈 변수로 표현한 Python 모듈입니다.
아래 코드와 같이 디폴트로 SQLite를 사용하도록 구성되어 있습니다.</p>
<pre><code class="language-python">...

DATABASES = {
    &#39;default&#39;: {
        &#39;ENGINE&#39;: &#39;django.db.backends.sqlite3&#39;,
        &#39;NAME&#39;: BASE_DIR / &#39;db.sqlite3&#39;,
    }
}
...</code></pre>
<p>실습용으로는 SQLite를 그대로 사용해도 무방하지만, 실제 프로젝트를 시작할 때에는 PostgreSQL, MariaDB, MySQL등의 좀 더 확장성 있는 데이터베이스를 사용하는 것이 좋습니다.
다른 데이터베이스를 사용해보고 싶다면 데이터베이스에 맞는 각 <a href="https://docs.djangoproject.com/ko/4.2/topics/install/#database-installation">데이터베이스 바인딩</a>을 설치하고, <code>settings.py</code>의 DATABASES &#39;default&#39; 항목의 값을 다음의 키 값으로 바꿔줍니다.</p>
<ul>
<li>ENGINE :  &#39;django.db.backends.sqlite3&#39;, &#39;django.db.backends.postgresql&#39;, &#39;django.db.backends.mysql&#39;, 또는 &#39;django.db.backends.oracle&#39;. 그외에 <a href="https://docs.djangoproject.com/ko/4.2/ref/databases/#third-party-notes">서드파티 백엔드</a> 참조</li>
<li>NAME : 데이터베이스의 이름으로 SQLite를 사용하는 경우에는 컴퓨터의 파일을 데이터베이스로 사용하므로, 이 경우에는 파일 이름을 포함한 해당 파일의 전체 절대 경로로 입력해줘야 합니다.</li>
</ul>
<p>SQLite 외의 DB를 사용하는 경우 아래 예시와 같이 USER, PASSWORD, HOST와 같은 추가 설정이 반드시 필요합니다. 
자세한 내용은 <a href="https://docs.djangoproject.com/ko/4.2/ref/settings/#std-setting-DATABASES">해당 페이지</a>를 참고바랍니다.</p>
<pre><code class="language-python"># postgresql 사용할 경우
DATABASES = {
    &quot;default&quot;: {
        &quot;ENGINE&quot;: &quot;django.db.backends.postgresql&quot;,
        &quot;NAME&quot;: &quot;mydatabase&quot;,
        &quot;USER&quot;: &quot;mydatabaseuser&quot;,
        &quot;PASSWORD&quot;: &quot;mypassword&quot;,
        &quot;HOST&quot;: &quot;127.0.0.1&quot;,
        &quot;PORT&quot;: &quot;5432&quot;,
    }
}</code></pre>
<blockquote>
<p><strong>스키마 생성</strong>
SQLite를 사용한다면 필요할 때마다 자동으로 데이터베이스 파일이 생성됩니다.
하지만 SQLite 이외의 데이터베이스를 사용한다면 별도로 데이터베이스를 미리 생성해 놓아야 합니다.
사용하는 데이터베이스에 접속하여 아래 명령으로 데이터베이스를 생성할 수 있습니다.</p>
<pre><code class="language-sql">CREATE DATABASE database_name;</code></pre>
<p>또한 <strong>test_project/settings.py</strong>에 설정된 데이터베이스 사용자가 <code>create database</code>권한이 있는지 확인해야 합니다. 
권한이 있어야 테스트 데이터베이스를 자동으로 생성할 수 있습니다.</p>
</blockquote>
<p>필자는 MySQL을 연동하여 개발을 진행해 보겠습니다.
먼저 MySQL 워크벤치 등의 프로그램을 통해 DB에 접속한 후 사용할 데이터베이스를 생성해 줍니다.</p>
<pre><code class="language-sql">CREATE SCHEMA `testdb` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ;</code></pre>
<p>다음으로 MySQL DB와 Django를 연결하려면 <code>mysqlclient</code>가 필요합니다. 다음 명령으로 mysqlclient를 설치해 줍니다.</p>
<pre><code class="language-bash">$ pip install mysqlclient</code></pre>
<blockquote>
<pre><code class="language-bash">Collecting mysqlclient
 Downloading mysqlclient-2.2.0.tar.gz (89 kB)
    &gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&gt;━━━━━━━━━━━ 89.5/89.5 kB 1.6 MB/s eta &gt;0:00:00
 Installing build dependencies ... done
 Getting requirements to build wheel ... error
 error: subprocess-exited-with-error

 × Getting requirements to build wheel did not &gt; run successfully.
 │ exit code: 1
 ╰─&gt; [24 lines of output]
     /bin/sh: 1: pkg-config: not found
     /bin/sh: 1: pkg-config: not found
     Trying pkg-config --exists mysqlclient
     Command &#39;pkg-config --exists mysqlclient&#39; &gt; returned non-zero exit status 127.
     Trying pkg-config --exists mariadb
     Command &#39;pkg-config --exists mariadb&#39; returned non-zero exit status 127.</code></pre>
<p>만약 이런 에러가 발생했다면 필요한 패키지들이 없어서 발생하는 것이므로 다음 패키지들을 설치해 줍니다.</p>
<pre><code>$ sudo apt-get install python3-dev libmysqlclient-dev build-essential pkg-config -y</code></pre></blockquote>
<p><code>mysqlclient</code> 설치가 완료되었다면 <code>settings.py</code>를 수정해줍니다.</p>
<pre><code class="language-python">DATABASES = {
    &#39;default&#39;: {
        &#39;ENGINE&#39;: &#39;django.db.backends.mysql&#39;,
        &#39;NAME&#39;: &#39;testdb&#39;,
        &#39;USER&#39;: &#39;root&#39;,
        &#39;PASSWORD&#39;: &#39;xxxx&#39;,
        &#39;HOST&#39;: &#39;localhost&#39;,
        &#39;PORT&#39;: &#39;3306&#39;,
    }
}</code></pre>
<p>수정을 마치고 연결이 잘 되는지 Django 서버를 실행해 봅시다.</p>
<pre><code class="language-bash">$ python manage.py runserver</code></pre>
<pre><code class="language-bash">❯ python manage.py runserver
 Watching for file changes with StatReloader
 Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run &#39;python manage.py migrate&#39; to apply them.
July 05, 2023 - 22:52:45
Django version 4.2.2, using settings &#39;test_project.settings&#39;
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
</code></pre>
<p>DB연결이 제대로 되지 않으면 <code>runserver</code> 명령어 실행시 에러가 발생하므로, 위와 같은 프롬프트가 뜨고 브라우저에서<code>http://127.0.0.1:8000/</code>로 접근 시 로켓 이미지가 표시된다면 정상적으로 연결이 된 것입니다.</p>
<p>데이터베이스 정상 연동이 확인되었으므로 계속해서 migrate 명령으로 DB 초기화 작업을 시켜줍니다.</p>
<pre><code class="language-bash">$ python manage.py migrate
</code></pre>
<p><code>migrate</code> 명령은 <strong>INSTALLED_APPS</strong> 설정을 탐색하여, <strong>test_app/settings.py</strong>의 데이터베이스의 설정과 app 과 함께 제공되는 database migrations에 따라, 필요한 데이터베이스 테이블을 생성합니다. </p>
<blockquote>
<p><strong>INSTALLED_APPS</strong>는
현재 Django 인스턴스에서 활성화된 모든 Django 앱(어플리케이션)의 이름을 담고 있습니다.
앱들은 다수의 프로젝트에서 사용될 수 있고, 다른 프로젝트에서 쉽게 사용될 수 있도록 패키징하여 배포할 수 있습니다.
현재 생성된 Django 프로젝트는 여러 기본 제공되는 앱이 등록되어 있으나 해당 앱들이 불필요하다면 <code>migrate</code> 명령을 실행하기 전에 <strong>settings.py</strong>내의 <strong>INSTALLED_APPS</strong>에서 제거할 앱을 주석처리하거나 삭제하시면 됩니다.</p>
</blockquote>
<p><code>migrate</code> 작업 완료 후 다시 서버를 실행하여 정상 연결 여부를 확인합니다. 마찬가지로 에러가 나지 않고 브라우저로 접속시에 로켓 이미지가 노출된다면 정상적으로 연결된 것입니다.</p>
<p>추가로 MySQL 워크벤치 등으로 MySQL DB에 접근하여 기본구성에 필요한 테이블들이 잘 생성되어 있는지 확인하실 수도 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/harry-jang/post/f0bed28c-8335-4152-8e63-46a48ba9cc32/image.png" alt="MySQL 워크벤치 화면"></p>
<p>다음 포스팅에서는 실제로 테이블에 데이터를 읽고 쓰는 방법에 대해 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(3) - Django 앱 생성하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B03-Django-%EC%95%B1-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B03-Django-%EC%95%B1-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 04 Jul 2023 14:24:14 GMT</pubDate>
            <description><![CDATA[<h2 id="앱-생성하기">앱 생성하기</h2>
<p>지난 번 포스팅에서 Django 프로젝트 생성을 통해 작업을 시작하기 위한 환경 세팅이 완료되었습니다.
<code>manage.py</code> 파일이 존재하는 디렉토리에서 다음 명령을 통해 해당 프로젝트 내에 작업할 앱을 하나 생성해봅시다.</p>
<pre><code class="language-bash">$ python manage.py startapp test_app</code></pre>
<blockquote>
<p><strong>프로젝트 vs 앱</strong>
프로젝트 : 특정 웹 사이트에 대한 구성 및 앱의 모음. 한 프로젝트에 여러 개의 앱이 포함될 수 있음.
앱 :  블로그 시스템, 공개 기록 데이터베이스 또는 소규모 의견조사 앱과 같은 작업을 수행하는 웹 애플리케이션. 앱은 여러 프로젝트에 있을 수 있음.</p>
</blockquote>
<p>생성된 앱은 아래와 같은 디렉터리 구조를 가집니다.</p>
<pre><code>test_app/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py</code></pre><h3 id="뷰-작성하기">뷰 작성하기</h3>
<p><code>test_app/views.py</code>를 수정하여 가장 간단한 형태를 뷰를 한번 작성해 봅시다.</p>
<pre><code class="language-python">from django.http import HttpResponse

def index(request):
    return HttpResponse(&quot;Hello, world!&quot;)</code></pre>
<p>뷰를 실제로 호출하여 화면에 띄우려면 이와 연결된 URL이 있어야 하는데, 이것은 URLconf를 통해 설정할 수 있습니다.
test_app 디렉토리에서 URLconf를 생성하기 위해 <code>urls.py</code>라는 파일을 만듭니다. 여기까지 작업하면 앱 디렉토리의 구조는 아래와 같을 것입니다.</p>
<pre><code>test_app/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    urls.py
    views.py</code></pre><p><code>urls.py</code>파일에는 다음과 같은 코드를 입력하여 라우팅할 path를 구성합니다.</p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [
    path(&quot;world/&quot;, views.index, name=&quot;index&quot;),
]</code></pre>
<blockquote>
<h3 id="path">path()</h3>
<p><code>path()</code> 함수의 파라미터는 아래와 같이 명시적으로도 작성할 수 있습니다. </p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [
path(route=&quot;world/&quot;, view=views.index, kwargs=None, name=&quot;index&quot;),
]</code></pre>
<p>각 파라미터를 하나하나 뜯어보면 다음과 같습니다.
<strong>route</strong> : URL 패턴을 가진 문자열입니다.
요청이 처리될 때, Django 는 urlpatterns 의 첫 번째 패턴부터 시작하여, 일치하는 패턴을 찾을 때 까지 요청된 URL 을 각 패턴과 리스트의 순서대로 비교합니다.
<strong>view</strong> : Django가 일치하는 URL 패턴을 찾으면, 첫번째 인수에는 <code>HttpRequest</code>객체가 담기고 경로로 부터 &#39;캡처된&#39; 값을 <code>kwargs</code> 인수로 하여 view에 지정한 함수를 호출합니다.
<strong>kwargs</strong> : 해당 경로로 들어왔을 때 호출되는 함수에 전달되는 임의의 키워드 인수들. 목표한 view 에 <code>dict</code> 형태으로 전달됩니다.
<strong>name</strong> : 템플릿을 포함한 Django 어디에서나 명확하게 참조할 수 있도록 지정하는 이름입니다.</p>
</blockquote>
<p>다음으로 <code>test_project/url.py</code> 파일을 열고, 아래 코드로 최상위 URLconf에서 <code>test_app.urls</code> 모듈을 바라보게 설정합니다.</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path(&quot;hello/&quot;, include(&quot;test_app.urls&quot;)),
    path(&quot;admin/&quot;, admin.site.urls),
]
</code></pre>
<blockquote>
<h3 id="include">include()</h3>
<p><code>include()</code> 함수는 다른 URLconf들을 참조할 수 있도록 도와주는 함수 입니다. 
Django가 <code>include()</code> 함수를 만나게 되면 URL의 그 시점까지 일치하는 부분을 잘라내고, 남은 문자열 부분을 후속 처리를 위해 include된 URLconf로 전달합니다.
admin.site.urls를 제외하고 다른 URL 패털을 포함할 때마다 항상 <code>include()</code>를 사용해야 합니다.</p>
</blockquote>
<p>이제 index 뷰가 URLconf에 연결되었으니 아래 명령으로 서버를 구동하여 정상적으로 작동하는지 확인해 봅시다.</p>
<pre><code class="language-python">$ python manage.py runserver</code></pre>
<p>지금까지 잘 따라왔다면 브라우저에서 <a href="http://localhost:8000/hello/world%EB%A5%BC">http://localhost:8000/hello/world를</a> 입력하면, views.py 내의 <code>index()</code>함수가 호출되어 &quot;hello, world&quot;라는 문구가 노출되게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(2) - Django 프로젝트 생성 및 구동하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B02-Django-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B02-Django-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 03 Jul 2023 14:15:54 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-만들기">프로젝트 만들기</h2>
<p>Django 프로젝트는 django-admin 명령을 통해 자동 생성할 수 있습니다. </p>
<pre><code class="language-bash">$ django-admin startproject myproject</code></pre>
<p><code>django-admin</code>은 Django 프로젝트의 관리 작업을 위한 커맨드 라인 유틸리티입니다. 자세한 내용은 기회가 되면 다뤄보겠습니다</p>
<p>돌아와서 위 명령을 통해 현재 디렉토리에서 myproject라는 디렉토리가 생성되며 startproject를 통해 생성된 프로젝트 구조는 다음과 같습니다.</p>
<pre><code>    manage.py
    myproject/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py</code></pre><ul>
<li><strong>manage.py</strong> : 각 Django 프로젝트에서 자동 생성되는 파일. <code>django-admin</code>과 동일한 작업을 수행하지만 작업할 프로젝트의 <code>setting.py</code>파일을 <code>DJANGO_SETTING_MODULE</code> 환경변수로 세팅하는 역할도 합니다.
일반적으로 단일 Django 프로젝트를 작업할 경우에는 <code>django-admin</code>보다 <code>manage.py</code> 파일을 사용하는 것이 더 쉽습니다.</li>
<li><strong>myproject/</strong> 디렉토리 내부에는 프로젝트를 위한 실제 Python 패키지들이 저장됩니다. 이 디렉토리 내의 이름을 이용하여, 프로젝트의 어디서나 Python 패키지들을 임포트 할 수 있습니다.</li>
<li><strong>myproject/_<em>init_</em>.py</strong> : 이 파일이 포함된 디렉토리를 패키지로 처리하도록 하기 위해 필요한 파일입니다. <strong>_<em>init_</em>.py</strong> 파일은 빈 파일일 수 있지만 패키지에 대한 초기화 코드를 실행하거나 <code>__all__</code>변수를 설정할 수도 있습니다.</li>
<li><strong>myproject/settings.py</strong> : <code>setting.py</code>파일에는 Django 설치의 모든 구성이 포함되어 있습니다. </li>
<li><strong>myproject/urls.py</strong> : Django 프로젝트의 URL 선언할 때 사용합니다. </li>
<li><strong>myproject/asgi.py</strong> : 현재 프로젝트를 서비스하기 위한 ASGI-호환 웹 서버의 엔드포인트입니다. <code>ASGI(Asynchronous Server Gateway Interface)</code>란 비동기 웹서버와 애플리케이션을 위한 파이썬 표준 인터페이스입니다. </li>
<li><strong>myproject/wsgi.py</strong> : 현재 프로젝트를 서비스하기 위한 WSGI-호환 웹 서버의 엔드포인트입니다. <code>WSGI(Web Server Gateway Interface)</code>란 동기 웹서버와 애플리케이션이 통신하기 위한 파이썬 표준 인터페이스입니다.</li>
</ul>
<p>ASGI와 WSGI에 대한 설명은 해당 <a href="https://www.itworld.co.kr/news/245062">아티클</a>를 참고하길 바랍니다.</p>
<h2 id="서버-구동하기">서버 구동하기</h2>
<p>myproject 디렉토리에서 아래 명령어를 통해 서버를 구동할 수 있습니다.</p>
<pre><code class="language-bash">$ python manage.py runserver</code></pre>
<p>구동에 성공하면 명령줄에서 다음과 같은 출력을 보게 됩니다.</p>
<pre><code class="language-bash">Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have unapplied migrations; your app may not work properly until they are applied.
Run &#39;python manage.py migrate&#39; to apply them.

July 03, 2023 - 14:03:22
Django version 4.2.2, using settings &#39;myproject.settings&#39;
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.</code></pre>
<blockquote>
<p><strong>참고</strong>
경고문구 You have unapplied migrations~ 부분은 아직 데이터베이스 적용되지 않은 변경사항들이 있을 때 출력되는 문구로 현재는 무시해도 됩니다. </p>
</blockquote>
<p>그리고 명령줄에 출력된 url을 웹브라우저 등에서 접속하면 해당 로켓 그림이 나오는 페이지가 뜨게 됩니다.
<img src="https://velog.velcdn.com/images/harry-jang/post/7970f5ce-aa67-4d05-9975-f4fa368ecd39/image.png" alt="main"></p>
<h3 id="포트-변경하기">포트 변경하기</h3>
<p>기본적으로, runserver 명령은 내부 IP의 8000번 포트로 개발서버를 띄우게 됩니다.
포트를 변경하려면 아래와 같이 명령어에 인수를 추가로 전달해주시면 됩니다.</p>
<pre><code class="language-bash">$ python manage.py runserver 8080</code></pre>
<p>서버의 IP도 다음과 같이 변경할 수 있습니다.</p>
<pre><code class="language-bash">$ python manage.py runserver 0.0.0.0:8000</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django : python으로 웹서비스 개발하기(1) - Django 설치하기]]></title>
            <link>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B01-Django-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@harry-jang/Django-python%EC%9C%BC%EB%A1%9C-WAS-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B01-Django-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 03 Jul 2023 09:41:01 GMT</pubDate>
            <description><![CDATA[<h2 id="django란">Django란?</h2>
<p>python으로 만들어진 무료 오픈소스 웹 애플리케이션 프레임워크</p>
<h2 id="설치하기">설치하기</h2>
<h3 id="1-가상환경-세팅">1. 가상환경 세팅</h3>
<p>각 어플리케이션 간의 설치하는 패키지 버전에 따른 Conflict를 방지하기 위해서 어플리케이션 별로 독립된 가상환경을 세팅을 진행합니다.</p>
<h4 id="1-프로젝트-디렉토리-생성">1) 프로젝트 디렉토리 생성</h4>
<pre><code class="language-bash">    $ mkdir django_webapp
    $ cd django_webapp</code></pre>
<h4 id="2-가상환경-생성">2) 가상환경 생성</h4>
<pre><code class="language-bash">    $ python3 -m venv myvenv</code></pre>
<p>여기에서 myvenv는 가상환경의 이름입니다. 
이름은 소문자에 공백이 없도록 작성해야 합니다.</p>
<blockquote>
<p>NOTE : 우분투 환경에서 다음과 같은 에러가 발생할 수 있습니다.</p>
</blockquote>
<pre><code class="language-bash">The virtual environment was not created successfully because ensurepip is not available.  On Debian/Ubuntu systems, you need to install the python3-venv package using the following command.
    apt install python3-venv
You may need to use sudo with that command.  After installing the python3-venv package, recreate your virtual environment.</code></pre>
<p>지시에 따라 아래 명령어를 입력하여 패키지를 설치해 줍니다.</p>
<pre><code class="language-bash">$ sudo apt install python3-venv</code></pre>
<h4 id="3-가상환경-활성화">3) 가상환경 활성화</h4>
<p>앞의 명령어로 생성된 가상환경은 다음 명령어를 통해 활성화할 수 있습니다.</p>
<pre><code class="language-bash">$ source myvenv/bin/activate</code></pre>
<blockquote>
<p>NOTE : source를 사용할 수 없는 경우 아래 명령어로 활성화할 수 있습니다.</p>
</blockquote>
<pre><code>$ . myvenv/bin/activate</code></pre><p>콘솔 프롬프트 앞에 (myvenv)접두어가 붙어있는 것이 확인된다면 virtualenv가 정상적으로 활성화가 된 상태입니다.</p>
<h3 id="2-django-설치">2. Django 설치</h3>
<h4 id="1-pip-최신버전으로-업그레이드">1) pip 최신버전으로 업그레이드</h4>
<pre><code class="language-bash">(myvenv) ~$ python3 -m pip install --upgrade pip</code></pre>
<h4 id="2-django-패키지-설치">2) Django 패키지 설치</h4>
<pre><code class="language-bash">$ python -m pip install Django</code></pre>
<h3 id="3-설치-확인">3. 설치 확인</h3>
<p>아래의 명령어로 버전을 확인할 수 있습니다.
버전이 정상적으로 출력된다면 설치가 완료된 것입니다.</p>
<pre><code class="language-bash">$ python -m django --version
4.2.3</code></pre>
<p>위 명령어 외에도 python 프롬프트로 들어가서 다음 코드를 실행해보면 설치된 버전이 출력됩니다.</p>
<pre><code class="language-python">&gt;&gt;&gt; import django
&gt;&gt;&gt; print(django.get_version())
4.2.3</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] maven property 주입]]></title>
            <link>https://velog.io/@harry-jang/SpringBoot-maven-property-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@harry-jang/SpringBoot-maven-property-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Fri, 26 Aug 2022 05:56:47 GMT</pubDate>
            <description><![CDATA[<p>로그에 프로젝트명, 프로젝트버전을 남기기 위해 pom.xml에 설정된 프로젝트명, 프로젝트 버전을 스프링으로 가져오는 방법을 알아보았다.</p>
<h3 id="pomxml">pom.xml</h3>
<ol>
<li><p>resource 필터링 활성화를 통해 <a href="http://application.properties">application.properties</a> 리소스 파일의 속성을 필터링(교체)</p>
<pre><code class="language-xml"> &lt;resources&gt;
     &lt;resource&gt;
         &lt;filtering&gt;true&lt;/filtering&gt;
         &lt;directory&gt;src/main/resources&lt;/directory&gt;
         &lt;includes&gt;
             &lt;include&gt;application.properties&lt;/include&gt;
         &lt;/includes&gt;
     &lt;/resource&gt;
 &lt;/resources&gt;</code></pre>
</li>
<li><p><a href="http://application.properties">application.properties</a> 파일에서 maven 프로퍼티를 ${} 형태로 주입받을 수 있도록 설정</p>
<ol>
<li><p>spring-boot-start-parent에서는 @property@로 프로터피를 주입받게 재정의되어 있으나useDefaultDelimiters : true로 주면 ${property}로 주입받을 수 있다.</p>
<pre><code class="language-xml">&lt;build&gt;
...
 &lt;plugins&gt;
   ...
     &lt;plugin&gt;
         &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
         &lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt;
         &lt;version&gt;2.7&lt;/version&gt;
         &lt;configuration&gt;
             &lt;useDefaultDelimiters&gt;true&lt;/useDefaultDelimiters&gt;
         &lt;/configuration&gt;
     &lt;/plugin&gt;
 &lt;/plugins&gt;
&lt;/build&gt;</code></pre>
</li>
</ol>
</li>
</ol>
<h3 id="applicationproperties">application.properties</h3>
<ol>
<li>아래와 같이 pom.xml에서 가져올 프로퍼티를 지정한다.</li>
</ol>
<pre><code>  application.name=${project.artifactId}
  build.version=${project.version}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[WSL 우분투에서 MongoDB 실행되지 않는 현상 해결 방법]]></title>
            <link>https://velog.io/@harry-jang/WSL-%EC%9A%B0%EB%B6%84%ED%88%AC%EC%97%90%EC%84%9C-MongoDB-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@harry-jang/WSL-%EC%9A%B0%EB%B6%84%ED%88%AC%EC%97%90%EC%84%9C-MongoDB-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 04 Aug 2022 06:12:39 GMT</pubDate>
            <description><![CDATA[<p>Spring MongoDB Integration 실습을 위해 WSL 우분투환경에서 MongoDB를 설치하고 실행하려고 하니 
다음과 같은 에러가 발생하였다.</p>
<pre><code class="language-bash">❯ systemctl start mongod
System has not been booted with systemd as init system (PID 1). Can&#39;t operate.
Failed to connect to bus: Host is down</code></pre>
<p>찾아보니 wsl2에서는 systemd를 지원하지 않기 때문에 공식적으로는 <code>systemctl</code> 명령어를 사용할 수 없다고 한다. 대신 <code>service</code> 명령은 지원하기 때문에 <code>service</code> 명령으로 실행해보라고 한다.</p>
<p><code>service</code> 라는 명령어는 /etc/init.d에 해당 파일이 존재할 때 사용할 수 있는 명령어 이다.</p>
<pre><code class="language-bash">❯ sudo service mongod start
mongod: unrecognized service</code></pre>
<p>apt-get으로 mongodb를 설치했을때 /etc/init.d에 mongod파일이 생성되지 않는 듯 하다.</p>
<p>수동으로 /etc/init.d에 mongod 파일을 만들어주자.</p>
<pre><code class="language-bash">cd /etc/init.d
sudo touch mongod
sudo chmod 755 mongod</code></pre>
<p><a href="https://github.com/mongodb/mongo/blob/master/debian/init.d">https://github.com/mongodb/mongo/blob/master/debian/init.d</a></p>
<p>찾아보니 해당 링크의 내용으로 mongod 파일을 만들면 되는 듯하다.</p>
<p>mongod 파일에 해당 내용을 붙여넣기하고 다시 실행해보았다.</p>
<pre><code class="language-bash">❯ sudo service mongod start
 * Starting database mongod                                                                             [ OK ]
❯ ps -ef | grep mongod
mongodb  23520 17530  4 14:19 ?        00:00:00 /usr/bin/mongod --config /etc/mongod.conf</code></pre>
<p>정상적으로 뜨는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[apt-get update 실패나는 이슈 해결 방법]]></title>
            <link>https://velog.io/@harry-jang/apt-get-update-%EC%8B%A4%ED%8C%A8%EB%82%98%EB%8A%94-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@harry-jang/apt-get-update-%EC%8B%A4%ED%8C%A8%EB%82%98%EB%8A%94-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 05 Jul 2022 09:28:41 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-bash">❯ sudo apt-get update
Ign:1 https://apt.kubernetes.io kubernetes-xenial InRelease
Err:2 https://apt.kubernetes.io kubernetes-xenial Release
  Certificate verification failed: The certificate is NOT trusted. The certificate issuer is unknown.  Could not handshake: Error in the certificate verification. 
Hit:3 https://download.docker.com/linux/ubuntu focal InRelease
Hit:4 http://security.ubuntu.com/ubuntu focal-security InRelease
Hit:5 http://archive.ubuntu.com/ubuntu focal InRelease
Hit:6 http://archive.ubuntu.com/ubuntu focal-updates InRelease
Hit:7 http://archive.ubuntu.com/ubuntu focal-backports InRelease
Reading package lists... Done
E: The repository &#39;https://apt.kubernetes.io kubernetes-xenial Release&#39; does not have a Release file.
N: Updating from such a repository can&#39;t be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.</code></pre>
<p>해당 에러가 발생하는 경우는 해당 서버의 인증서가 없어서 저장소에 릴리즈된 파일들을 읽어오지 못하는 경우이다.</p>
<ol>
<li><p>아래 명령어로 해당 서버의 인증서를 파일로 읽어온다.</p>
<pre><code class="language-bash">openssl s_client -connect apt.kubernetes.io:443 | tee cert_file</code></pre>
</li>
<li><p>파일의  BEGIN CERTIFICATE  부분부터  END CERTIFICATE까지가 인증서 부분이기 때문에 해당 부분만 남기고 나머지는 지우고 파일을 저장한다.</p>
</li>
<li><p>아래 명령어로 제대로 추출되었는지 확인한다.</p>
<pre><code class="language-bash">openssl x509 -inform PEM -text -in cert_file</code></pre>
</li>
<li><p>해당 인증서의 내용을 신뢰하는 인증 기관 목록(<strong>CA List; Certificate Authority List</strong>)에 추가한다.</p>
<pre><code class="language-bash">cat cert_file &gt;&gt; /etc/ssl/certs/ca-certificates.crt</code></pre>
</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>