<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>i-am-not-kangjik.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 24 Aug 2025 17:53:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>i-am-not-kangjik.log</title>
            <url>https://velog.velcdn.com/images/i-am-not-kangjik/profile/64b43c16-937a-4d2b-9e0d-c73da1e2a3ba/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. i-am-not-kangjik.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/i-am-not-kangjik" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[React.js 복습 기초]]></title>
            <link>https://velog.io/@i-am-not-kangjik/React.js-%EB%B3%B5%EC%8A%B5-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@i-am-not-kangjik/React.js-%EB%B3%B5%EC%8A%B5-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Sun, 24 Aug 2025 17:53:36 GMT</pubDate>
            <description><![CDATA[<h2 id="reactjs란-무엇이고-왜-쓰는가">React.js란 무엇이고 왜 쓰는가?</h2>
<p>공식 홈페이지 설명 - React는 UI를 만들어 주는 자바스크립트 라이브러리다.</p>
<p>간단한 웹 페이지 작업에는 필요하지 않지만,</p>
<p>인터랙티브한 웹 페이지를 만들 때에는 React를 사용하면 작성해야 할 코드량이 확 줄어 아주 유용하다.</p>
<p>기존 자바스크립트를 활용하면 명령적 방식을 사용하여 코드를 작성하지만,</p>
<p>리액트를 사용하면 선언형 방식으로 코드를 작성할 수 있다.</p>
<p>화면에 표시할 UI 코드를 작성하고,  이벤트 리스너를 추가하는 코드나 동적인 값을 HTML 코드에 넣어주고 리액트 기능을 더해준다. 예를 들어 상태를 정의하고, 어떤 상황에서 이 상태를 활성화할지 등의 코드를 추가한다.</p>
<p>그러면 React.js가 알아서 브라우저에 전달할 명령을 만들어 낸다. 그래서 개발자는 명령을 하나하나 작성할 필요가 없게 된다.</p>
<h2 id="react-프로젝트에-필요한-것">React 프로젝트에 필요한 것</h2>
<p>바닐라 자바스크립트를 사용할 때 프로젝트를 생성한다는 것은 단순히 폴더를 생성하고,
그 안에 HTML 파일과 js 파일, CSS파일을 넣기만 하는 것이었다.</p>
<p>리액트를 사용하여 웹 페이지를 생성할 때는 그렇게 간단하지만은 않은데,</p>
<p>리액트 프로젝트는 백그라운드에서 코드를 자동으로 변환해주는 작업을 포함해야 한다.</p>
<p>또한 개발자 경험을 위해 코드의 변경사항을 실시간으로 볼 수 있어야한다.</p>
<p>코드 변환 작업이 필요한 이유는 리액트는 하나의 자바스크립트 파일에서 작업을 하는데,
브라우저는 이 코드를 실행할수 없기 때문에 이 자바스크립트 코드는 유효하지 않다.</p>
<p>그래서 이 자바스크립트 코드를 브라우저에서 실행 가능한 클라이언트 코드로 변환(컴파일) 하는 작업이 필요하다.</p>
<h2 id="react-프로젝트를-생성하는-방법">React 프로젝트를 생성하는 방법</h2>
<p>React 프로젝트에 필요한 것들을 담은 프로젝트 생성 방법은 두가지가 있다.</p>
<ol>
<li><code>create react app</code> </li>
<li><code>vite</code></li>
</ol>
<p>어떤 도구를 사용하던 간에 두 가지 모두 Node.js 가 필요하다.</p>
<p>Node.js가 필요한 이유는 프로젝트 생성 도구 두가지 모두 Node.js를 이용하기 때문이다.</p>
<p>Node.js를 설치한 이후 새 프로젝트를 생성하려면</p>
<pre><code class="language-jsx">npx create-react-app</code></pre>
<pre><code class="language-jsx">npm create vite</code></pre>
<p>이렇게 생성할 수 있다.</p>
<p>지금은 vite를 사용해볼 예정이다.</p>
<p>vite를 사용해 아래와 같이 프로젝트를 생성했다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/2a84def2-9058-4b5c-9ade-c6452a45023a/image.png" alt=""></p>
<p>이후 create-react-app을 통해 프로젝트를 시작했다면 npm start 를</p>
<p>vite를 사용해 프로젝트를 시작했다면 npm run dev 를 입력해 프로젝트를 시작해준다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/7ab1443a-7351-4177-a691-9e83561dff58/image.png" alt=""></p>
<p>터미널에 표시된 localhost:5173을 방문하면 react 앱 미리보기를 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/1a0d5f50-eb46-4f87-8bfb-7a11b8d74e9b/image.png" alt=""></p>
<h2 id="react-구조-살펴보기">React 구조 살펴보기</h2>
<p>src 폴더 안에는 jsx 파일이 있는데, 이 안에 있는 HTML 코드를 지원한다.</p>
<p>브라우저에선 지원하지 않는 문법으로, 프로젝트 자체에서 자동으로 변환해준다.</p>
<pre><code class="language-jsx">function App() {
  return &lt;h1&gt;Hello World!&lt;/h1&gt;;
}

export default App;
</code></pre>
<p>위와 같이 HTML 코드를 자바스크립트에 포함시키는 것을 jsx라 한다.</p>
<p>또한 기본적인 css 스타일을 포함한 index.css 파일을 확인할 수 있는데,</p>
<p>이 파일도 결국엔 main.jsx에 import되어 사용된다.</p>
<p>이 방식도 브라우저에서 지원하지 않지만, 이 역시도 프로젝트가 알아서 변환해 적용시킨다.</p>
<p>변환된 결과를 보고 싶다면, 브라우저에서 개발자 도구를 실행해 확인할 수 있다.</p>
<p>head 섹션 안에 script와 style이 들어있는데, style 안에는 CSS 파일의 내용을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/c782550b-f068-4359-ae0f-77b4081550fd/image.png" alt=""></p>
<p>Source 탭을 보면 페이지가 로드될때 자바스크립트 파일도 로드되고 있는데,</p>
<p>main.jsx를 확인해 보면 변환된 js 코드만 존재하고, HTML 코드는 존재하지 않는걸 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/9fd6ca55-6199-45ba-806b-69c8b94a9ba7/image.png" alt=""></p>
<h3 id="mainjsx-파일의-역할">main.jsx 파일의 역할</h3>
<pre><code class="language-jsx">import React from &#39;react&#39;
import ReactDOM from &#39;react-dom/client&#39;
import App from &#39;./App&#39;
import &#39;./index.css&#39;

ReactDOM.createRoot(document.getElementById(&#39;root&#39;)).render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
)
</code></pre>
<p>이 파일은 진입점을 가지고 있는 전체 앱의 메인 파일이다.</p>
<p>브라우저에 웹사이트가 로딩될 때, 이 파일에 있는 코드가 제일 먼저 실행된다.</p>
<p>코드를 살펴보면, react와 react-dom을 import하는데</p>
<p>두 개의 패키지가 분리되어 있지만 같은 팀에서 생성한 것으로
두 패키지가 합쳐져서 리액트 라이브러리가 되는것이다.</p>
<p>main.jsx의 코드는 ReactDOM으로 시작되는데,
여기서 createRoot 라는 메소드를 호출한다.</p>
<p>이 메소드는 HTML 코드의 Element 포인터를 받는데,
그 부분은 document.getElementById라는 바닐라 자바스크립트 코드로 되어있다.</p>
<p>이 코드의 역할은 react에 시키는 작업을
root라는 id를 가진 요소 안에 이 리액트 코드를 render 하는 것이다. (화면에 뿌리는 것)</p>
<p>StrictMode가 하는 일은 리액트가 제공하는 특별한 기능인데,
우리가 작성한 코드가 최적인지 아닌지를 확인하여 알려주는 기능이다.
또한 추후에는 내가 작성한 코드가 호환되지 않거나 하는 상황에서 경고를 주는 역할을 한다.</p>
<p>여기서 가장 중요한 부분은 App 부분인데,
이 App은 import된 요소로, js 파일에서 HTML 요소처럼 이용되고 있다.
이 App은 App.jsx 파일을 의미한다. </p>
<h3 id="appjsx">App.jsx</h3>
<pre><code class="language-jsx">function App() {
  return &lt;h1&gt;Hello World!&lt;/h1&gt;;
}

export default App;
</code></pre>
<p>이 함수는 간단하게 jsx 코드를 반환하기만 한다.
h1으로 둘러쌓인 HTML 요소를 반환하고 있는데,
이런 걸 바로 리액트 컴포넌트라고 한다.</p>
<p>리액트 앱을 빌드한다는 것은 결국 컴포넌트를 빌드하는 것이고,
컴포넌트는 JSX 코드를 반환하는 함수이다.</p>
<p>이론적으로 그 외의 것도 반환할 수 있지만,
보통은 JSX 코드를 반환한다.
따라서 JSX 코드를 반환하는 함수는 리액트 컴포넌트라 보면 된다.</p>
<p>리액트 컴포넌트는 다른 JSX에서도 사용될 수 있는데,
이 App이라는 함수는 main.jsx에서 HTML 요소처럼 활용되는 것이 그 예시이다.
render 함수는 HTML 코드를 받아야 하는데, JSX를 받아 화면에 출력하고,
App 컴포넌트를 받아 화면에 출력한 것이다.</p>
<h2 id="커스텀-컴포넌트-만들어-보기">커스텀 컴포넌트 만들어 보기</h2>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/f381f8d3-8663-4099-8db3-fc5085e551c6/image.png" alt=""></p>
<p>src 디렉토리 안에 components 디렉토리를 만들고, Post.jsx 파일을 생성한다.
create-react-app을 사용했을 경우 .js 확장자를 사용해도 되지만 vite를 사용한 경우 .jsx 확장자를 이용해야 한다.</p>
<pre><code class="language-jsx">function Post() {
  return (
    &lt;div&gt;
      &lt;p&gt;Kangjik Kim&lt;/p&gt;
      &lt;p&gt;React.js is awesome!&lt;/p&gt;
    &lt;/div&gt;
  );
}

export default Post;
</code></pre>
<p>이렇게 Page.jsx에 Post 함수를 만들고 HTML 요소를 반환한 다음 export 해주면 커스텀 컴포넌트가 완성이 된다.</p>
<p>보통 React 프로젝트를 만들 때에는 root 컴포넌트를 하나만 두는데,
main.jsx에서 출력되는 컴포넌트를 하나만 둔다는 의미이다.</p>
<p>현재 main.tsx에서는 App 컴포넌트 하나를 다루고 있으므로,
Post 컴포넌트는 App 컴포넌트의 jsx 코드에 넣으려 한다.</p>
<p>아래와 같이 Post 컴포넌트를 import 하고, Hello World 대신 Post 컴포넌트를 반환하게 만든다.</p>
<pre><code class="language-jsx">import Post from &quot;./components/Post&quot;;

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

export default App;
</code></pre>
<p>컴포넌트를 사용할 때에는 일반 js 함수처럼 Post() 와 같이 사용하는 것이 아니라,
HTML 요소처럼 <Post /> 와 같이 사용한다.</p>
<p>컴포넌트를 선언하는 곳에서는 소문자로 선언을 해도 무방하지만 일반적으로 앞글자를 대문자로 선언하고 있고,
컴포넌트를 사용하는 곳에서는 무조건 대문자로 사용을 해야한다. (<Post />;)
컴포넌트를 사용하는 곳에서 소문자로 시작을 할 경우, 리액트에서 기본 HTML 요소로 인식하기 때문이다.</p>
<p>바뀐 화면을 확인해 보면, 아래와 같이 새로 만든 Post.jsx 컴포넌트의 내용이 표시되는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/7f53bf3f-3797-4966-aa0c-811c07eed522/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS 기초]]></title>
            <link>https://velog.io/@i-am-not-kangjik/NextJS-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@i-am-not-kangjik/NextJS-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Fri, 22 Aug 2025 15:51:11 GMT</pubDate>
            <description><![CDATA[<h2 id="nextjs란-무엇이며-왜-사용되는-걸까">NextJS란 무엇이며, 왜 사용되는 걸까?</h2>
<p>react 프레임워크이며 react에 빌드된다.</p>
<h2 id="react도-라이브러리인데-nextjs가-왜-필요할까">React도 라이브러리인데 NextJS가 왜 필요할까?</h2>
<p>NextJS는 react의 풀스택 프레임워크이며, 리액트로 풀스택 애플리케이션을 구축하는 과정을 단순화한다.</p>
<p>또한 최근에는 클라이언트 측 단일 페이지 애플리케이션이 아닌 풀스택 애플리케이션을 구축하는 추세이다.</p>
<p>리액트 라이브러리 자체에도 서버에서 리액트 사용을 더 쉽게 하기 위해 기능을 점점 추가하고 있고,</p>
<p>특히 서버에서 컴포넌트를 렌더링 할 수 있게 업데이트하고 있다.</p>
<p>그러나 풀스택 애플리케이션을 구축하려면 더 필요한 것들이 많은데,</p>
<ul>
<li>form 제출 처리</li>
<li>데이터 가저오기</li>
<li>사용자인증</li>
</ul>
<p>과 같은 것들을 NextJS가 제공한다.</p>
<p>따라서 리액트를 기반으로 구축한 다음 리액트로 풀스택 애플리케이션을 구축하는 과정을 대폭 간소화한다.</p>
<h2 id="nextjs-주요기능-및-장점">NextJS 주요기능 및 장점</h2>
<p>NextJS의 가장 큰 장점은 풀스택 앱을 구축하는 것이지만 장점은 이 뿐만이 아니다.</p>
<p>NextJS의 또 다른 장점은 파일 시스템을 사용하여 경로를 설정할 수 있다는 것이다.</p>
<p>일반적으로 React Router 같은 바닐라 자바스크립트에서 하는 것처럼 코드를 사용하여 환경설정하는 대신</p>
<p>NextJS에서는 파일 시스템으로 환경설정한다. 즉, 다양한 폴더와 파일을 설치하여 사용자가 방문할 수 있는 경로로 매핑한다. 이 접근 방식의 장점은 코드 기반 환경설정 또는 패키지가 NextJS에 내장되어 추가로 필요하지 않다는 것이다.</p>
<p>또한, NextJS가 페이지에 보이는 모든 내용을 렌더링한다는 것이다. 이는 모든 페이지, 서버의 컴포넌트 등을 포함한다. 이것은 결국 유선으로 전송되는 컨텐츠인 HTML 문서에 화면에 표시되어야 할 모든 컨텐츠가 포함된다는 것을 의미한다. 대부분 비어있고 클라이언트에 채워진 바닐라 자바스크립트와 비교할 수 있다.</p>
<p>여기서의 장점은 예를 들어 검색 엔진 크롤러도 완성된 컨텐츠를 볼 수 있다는 것이다.</p>
<h2 id="첫-번째-nextjs-앱-만들기">첫 번째 NextJS 앱 만들기</h2>
<p>NextJS 공식 홈페이지인 nextjs.org에 들어가면 새로운 NextJS 프로젝트를 생성하는 명령어가 있다.</p>
<pre><code class="language-html">npx create-next-app@latest</code></pre>
<p>이 명령어를 실행하면 아래와 같이 나오게 되는데,</p>
<p>나는 단순히 첫 NextJS앱을 만들어 보기 위해 아래와 같이 선택했다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/54a8bb9d-5a23-40a2-a482-ba43fffea48f/image.png" alt=""></p>
<p>그 다음 생성된 디렉토리로 이동해 개발 서버를 실행해보자.</p>
<pre><code class="language-html">cd my-app
npm run dev</code></pre>
<p>실행이 될 경우 아래와 같은 내용이 터미널에 표시되게 된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/48551c13-332b-498e-8d76-92ffc6753334/image.png" alt=""></p>
<p>터미널에 표시된 주소로 접속을 해보면 아래와 같은 페이지가 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/c9d337af-f947-42ab-85e1-b7c985b038a2/image.png" alt=""></p>
<h2 id="파일-시스템을-통해-새로운-페이지-추가하기">파일 시스템을 통해 새로운 페이지 추가하기</h2>
<p>app/ 폴더 아래 새로운 폴더 생성</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/b17d94d3-4285-4d5c-b143-72d27b338b93/image.png" alt=""></p>
<p>awesome 폴더아래 page.js 파일 생성</p>
<p>폴더명과 page.js 파일이 합쳐져야 NextJS에서 신규 경로가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/3706b713-8287-4dac-abcd-74de87a7231e/image.png" alt=""></p>
<p>그 다음 page.js에 표준 리액트 컴포넌트 함수를 export 한다.</p>
<p>NextJS에서 페이지는 단순히 리액트 함수이다.</p>
<pre><code class="language-jsx">export default function AwesomePage() {
  return (
    &lt;main&gt;
      &lt;h1&gt;NextJS is Awesome!&lt;/h1&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>그 다음 NextJS 애플리케이션으로 돌아가 <a href="http://localhost:3000/awesome">localhost:3000/awesome</a>  으로 접속하면</p>
<p>아래와 같은 화면이 나오게 된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/2b63aa67-69cd-4beb-a65c-9c25157de621/image.png" alt=""></p>
<h2 id="pages-router-vs-app-router">Pages Router vs App Router</h2>
<p>NextJS 앱을 만들기 위해 두 가지 방식이 존재한다.</p>
<p>어떤 방식을 선택해도 풀스택 애플리케이션을 만드는 것이기 때문에 페이지가 서버에 렌더링 되고, 파일 시스템으로 경로를 설정하게 된다.</p>
<p>Pages Router는 더 오래된 방식이다.</p>
<p>다년간 사용된 방식으로 아주 안정적인 방법이다.</p>
<p>App Router는 NextJS 13 버전에 등장한 비교적 새로운 방식이다.</p>
<p>안정적이게 되고 있으나 아직 부분적으로 버그가 발생하는 경향이 있다.</p>
<p>리액트 서버 컴포넌트 또는 Server Actions 과 같은 다양한 최신 기능을 사용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python의 urlparse: URL 파싱 알아보기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Python%EC%9D%98-urlparse-URL-%ED%8C%8C%EC%8B%B1-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Python%EC%9D%98-urlparse-URL-%ED%8C%8C%EC%8B%B1-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 06 Jul 2025 07:46:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-urlparse-소개">1. urlparse 소개</h2>
<p><code>urlparse</code>는 Python의 <code>urllib.parse</code> 모듈에서 제공하는 URL 파싱 도구입니다. 
이 함수는 URL 문자열을 의미 있는 컴포넌트로 분해하여 분석할 수 있게 해줍니다.</p>
<h2 id="2-urlparse의-기본-사용법">2. urlparse의 기본 사용법</h2>
<pre><code class="language-python">from urllib.parse import urlparse

# 기본 사용 예시
url = &quot;&lt;https://user:pass@example.com:8080/path/to/page?query=value#fragment&gt;&quot;
parsed = urlparse(url)
</code></pre>
<h2 id="3-parseresult-객체-구조">3. ParseResult 객체 구조</h2>
<p><code>urlparse</code>는 <code>ParseResult</code> 객체를 반환합니다. 이 객체는 네임드 튜플(named tuple)의 형태로, 다음과 같은 6개의 컴포넌트를 가집니다:</p>
<pre><code class="language-python">ParseResult(
    scheme=&#39;https&#39;,      # URL 스키마 (프로토콜)
    netloc=&#39;example.com&#39;,# 네트워크 위치
    path=&#39;/path/name&#39;,   # 경로
    params=&#39;&#39;,           # 매개변수
    query=&#39;&#39;,           # 쿼리 문자열
    fragment=&#39;&#39;         # 프래그먼트
)
</code></pre>
<h2 id="4-다양한-url-예시와-파싱-결과">4. 다양한 URL 예시와 파싱 결과</h2>
<h3 id="41-https-url">4.1 HTTPS URL</h3>
<pre><code class="language-python">url = &quot;&lt;https://www.example.com/path/to/resource?name=value#section&gt;&quot;
parsed = urlparse(url)

print(f&quot;scheme  : {parsed.scheme}&quot;)   # https
print(f&quot;netloc  : {parsed.netloc}&quot;)   # www.example.com
print(f&quot;path    : {parsed.path}&quot;)     # /path/to/resource
print(f&quot;params  : {parsed.params}&quot;)   #
print(f&quot;query   : {parsed.query}&quot;)    # name=value
print(f&quot;fragment: {parsed.fragment}&quot;) # section
</code></pre>
<h3 id="42-s3-url">4.2 S3 URL</h3>
<pre><code class="language-python">url = &quot;s3://my-bucket/uploads/files/&quot;
parsed = urlparse(url)

print(f&quot;scheme  : {parsed.scheme}&quot;)   # s3
print(f&quot;netloc  : {parsed.netloc}&quot;)   # my-bucket
print(f&quot;path    : {parsed.path}&quot;)     # /uploads/files/
</code></pre>
<h3 id="43-ftp-url-인증-정보-포함">4.3 FTP URL (인증 정보 포함)</h3>
<pre><code class="language-python">url = &quot;&lt;ftp://user:password@ftp.example.com/files/&gt;&quot;
parsed = urlparse(url)

print(f&quot;scheme  : {parsed.scheme}&quot;)   # ftp
print(f&quot;netloc  : {parsed.netloc}&quot;)   # &lt;user:password@ftp.example.com&gt;
print(f&quot;path    : {parsed.path}&quot;)     # /files/
</code></pre>
<h2 id="5-유용한-메서드와-속성">5. 유용한 메서드와 속성</h2>
<h3 id="51-path-처리">5.1 path 처리</h3>
<pre><code class="language-python"># 경로에서 앞뒤 슬래시 제거
clean_path = parsed.path.strip(&#39;/&#39;)
print(clean_path)  # &#39;uploads/files&#39;

# 경로 구성요소 분리
path_components = parsed.path.split(&#39;/&#39;)
print(path_components)  # [&#39;&#39;, &#39;uploads&#39;, &#39;files&#39;, &#39;&#39;]
</code></pre>
<h3 id="52-netloc-구성요소-접근">5.2 netloc 구성요소 접근</h3>
<pre><code class="language-python">url = &quot;&lt;https://user:pass@example.com:8080&gt;&quot;
parsed = urlparse(url)

print(f&quot;username: {parsed.username}&quot;)  # user
print(f&quot;password: {parsed.password}&quot;)  # pass
print(f&quot;hostname: {parsed.hostname}&quot;)  # example.com
print(f&quot;port    : {parsed.port}&quot;)     # 8080
</code></pre>
<h2 id="6-실제-활용-예시-s3-파일-업로드-경로-생성">6. 실제 활용 예시: S3 파일 업로드 경로 생성</h2>
<pre><code class="language-python">from urllib.parse import urlparse
from datetime import datetime
import uuid

def create_s3_path(s3_base_url, filename):
    &quot;&quot;&quot;S3 업로드 경로 생성 함수&quot;&quot;&quot;
    parsed = urlparse(s3_base_url)

    # 버킷과 기본 경로 추출
    bucket_name = parsed.netloc
    base_path = parsed.path.strip(&#39;/&#39;)

    # UUID를 사용한 고유 파일명 생성
    file_extension = filename.split(&#39;.&#39;)[-1]
    unique_filename = f&quot;{uuid.uuid4()}.{file_extension}&quot;

    # 날짜 기반 경로 생성
    date_path = datetime.now().strftime(&#39;%Y/%m/%d&#39;)

    # 최종 S3 키 생성
    s3_key = f&quot;{base_path}/{date_path}/{unique_filename}&quot;

    return {
        &#39;bucket&#39;: bucket_name,
        &#39;s3_key&#39;: s3_key,
        &#39;full_path&#39;: f&quot;s3://{bucket_name}/{s3_key}&quot;
    }

# 사용 예시
result = create_s3_path(&quot;s3://my-bucket/uploads&quot;, &quot;document.pdf&quot;)
print(result)
</code></pre>
<p>출력 예시:</p>
<pre><code class="language-python">{
    &#39;bucket&#39;: &#39;my-bucket&#39;,
    &#39;s3_key&#39;: &#39;uploads/2024/03/15/550e8400-e29b-41d4-a716-446655440000.pdf&#39;,
    &#39;full_path&#39;: &#39;s3://my-bucket/uploads/2024/03/15/550e8400-e29b-41d4-a716-446655440000.pdf&#39;
}
</code></pre>
<h2 id="7-주의사항">7. 주의사항</h2>
<ol>
<li><code>urlparse</code>는 URL이 완벽하게 형식에 맞지 않아도 최선을 다해 파싱을 시도합니다.</li>
<li>스키마가 없는 URL은 기본적으로 경로로 해석됩니다.</li>
<li>네트워크 위치(netloc)와 경로(path)를 구분하는 &#39;//&#39;가 없으면 모든 것이 경로로 해석됩니다.</li>
</ol>
<h2 id="8-결론">8. 결론</h2>
<p><code>urlparse</code>는 URL 처리에 있어 매우 유용한 도구입니다. 
특히 웹 애플리케이션 개발, 파일 시스템 경로 처리, API 통합 등에서 URL을 분석하고 조작해야 할 때 필수적인 도구입니다. 
올바른 사용을 위해서는 URL의 구조를 이해하고, 각 컴포넌트의 의미와 역할을 파악하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django에서 검색어 하이라이트 구현하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django%EC%97%90%EC%84%9C-%EA%B2%80%EC%83%89%EC%96%B4-%ED%95%98%EC%9D%B4%EB%9D%BC%EC%9D%B4%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django%EC%97%90%EC%84%9C-%EA%B2%80%EC%83%89%EC%96%B4-%ED%95%98%EC%9D%B4%EB%9D%BC%EC%9D%B4%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 01 Jul 2025 14:10:46 GMT</pubDate>
            <description><![CDATA[<p>검색 기능을 구현할 때 사용자 경험을 향상시키는 중요한 요소 중 하나는 검색 결과에서 검색어를 시각적으로 강조하는 것입니다. 오늘은 Django에서 검색어 하이라이트 기능을 구현하는 방법을 알아보겠습니다.</p>
<h2 id="1-기본-검색-기능-구현">1. 기본 검색 기능 구현</h2>
<p>먼저 간단한 검색 폼과 결과를 보여주는 기본 구조를 만듭니다:</p>
<pre><code class="language-html">{# templates/search.html #}
&lt;form method=&quot;get&quot;&gt;
    &lt;select name=&quot;search_type&quot;&gt;
        &lt;option value=&quot;title&quot;&gt;제목&lt;/option&gt;
        &lt;option value=&quot;content&quot;&gt;내용&lt;/option&gt;
        &lt;option value=&quot;all&quot;&gt;제목+내용&lt;/option&gt;
    &lt;/select&gt;
    &lt;input type=&quot;text&quot; name=&quot;search_keyword&quot; value=&quot;{{ search_keyword }}&quot; placeholder=&quot;검색어&quot;&gt;
    &lt;button type=&quot;submit&quot;&gt;검색&lt;/button&gt;
&lt;/form&gt;

&lt;div class=&quot;search-results&quot;&gt;
    {% for item in results %}
        &lt;div class=&quot;result-item&quot;&gt;
            &lt;h3&gt;{{ item.title }}&lt;/h3&gt;
            &lt;p&gt;{{ item.content }}&lt;/p&gt;
        &lt;/div&gt;
    {% endfor %}
&lt;/div&gt;
</code></pre>
<h2 id="2-검색어-하이라이트-필터-만들기">2. 검색어 하이라이트 필터 만들기</h2>
<p><code>templatetags</code> 디렉토리를 생성하고 하이라이트 필터를 구현합니다:</p>
<pre><code class="language-python"># myapp/templatetags/highlight.py
from django import template
from django.utils.safestring import mark_safe
import re

register = template.Library()

@register.filter
def highlight(text, search):
    &quot;&quot;&quot;
    텍스트 내의 검색어를 하이라이트 처리하는 필터

    Args:
        text: 원본 텍스트
        search: 하이라이트할 검색어
    &quot;&quot;&quot;
    if not search or not text:
        return text

    # 특수문자가 포함된 검색어 처리를 위한 이스케이프
    search = re.escape(search)
    # 대소문자 구분 없이 검색어 매칭
    pattern = re.compile(f&#39;({search})&#39;, re.IGNORECASE)
    # 매칭된 부분을 &lt;strong&gt; 태그로 감싸기
    highlighted = pattern.sub(r&#39;&lt;strong&gt;\\1&lt;/strong&gt;&#39;, str(text))

    return mark_safe(highlighted)
</code></pre>
<h2 id="3-템플릿에-하이라이트-적용하기">3. 템플릿에 하이라이트 적용하기</h2>
<pre><code class="language-html">{% extends &#39;base.html&#39; %}
{% load highlight %}  {# 커스텀 필터 로드 #}

{% block content %}
    &lt;form method=&quot;get&quot;&gt;
        &lt;select name=&quot;search_type&quot;&gt;
            &lt;option value=&quot;title&quot; {% if search_type == &#39;title&#39; %}selected{% endif %}&gt;제목&lt;/option&gt;
            &lt;option value=&quot;content&quot; {% if search_type == &#39;content&#39; %}selected{% endif %}&gt;내용&lt;/option&gt;
            &lt;option value=&quot;all&quot; {% if search_type == &#39;all&#39; %}selected{% endif %}&gt;제목+내용&lt;/option&gt;
        &lt;/select&gt;
        &lt;input type=&quot;text&quot; name=&quot;search_keyword&quot; value=&quot;{{ search_keyword }}&quot; placeholder=&quot;검색어&quot;&gt;
        &lt;button type=&quot;submit&quot;&gt;검색&lt;/button&gt;
    &lt;/form&gt;

    &lt;div class=&quot;search-results&quot;&gt;
        {% for item in results %}
            &lt;div class=&quot;result-item&quot;&gt;
                {# 검색 타입에 따라 하이라이트 적용 #}
                {% if search_type == &#39;title&#39; or search_type == &#39;all&#39; %}
                    &lt;h3&gt;{{ item.title|highlight:search_keyword }}&lt;/h3&gt;
                {% else %}
                    &lt;h3&gt;{{ item.title }}&lt;/h3&gt;
                {% endif %}

                {% if search_type == &#39;content&#39; or search_type == &#39;all&#39; %}
                    &lt;p&gt;{{ item.content|highlight:search_keyword }}&lt;/p&gt;
                {% else %}
                    &lt;p&gt;{{ item.content }}&lt;/p&gt;
                {% endif %}
            &lt;/div&gt;
        {% endfor %}
    &lt;/div&gt;
{% endblock %}
</code></pre>
<h2 id="4-스타일-적용하기">4. 스타일 적용하기</h2>
<p>검색어 하이라이트를 시각적으로 돋보이게 만듭니다:</p>
<pre><code class="language-css">/* static/css/style.css */
.search-results .result-item strong {
    background-color: #fff3cd;  /* 연한 노란색 배경 */
    padding: 2px 4px;
    border-radius: 2px;
    font-weight: bold;
    color: #856404;  /* 진한 갈색 텍스트 */
}

/* 또는 다른 스타일 옵션 */
.search-results .result-item strong {
    background: linear-gradient(transparent 60%, #ffd700 60%);  /* 밑줄 형태의 하이라이트 */
    padding: 0 2px;
}
</code></pre>
<h2 id="5-view-구현">5. View 구현</h2>
<pre><code class="language-python">from django.views.generic import ListView
from django.db.models import Q

class SearchView(ListView):
    template_name = &#39;search.html&#39;
    context_object_name = &#39;results&#39;

    def get_queryset(self):
        queryset = YourModel.objects.all()
        search_type = self.request.GET.get(&#39;search_type&#39;, &#39;&#39;)
        search_keyword = self.request.GET.get(&#39;search_keyword&#39;, &#39;&#39;)

        if search_keyword:
            if search_type == &#39;title&#39;:
                queryset = queryset.filter(title__icontains=search_keyword)
            elif search_type == &#39;content&#39;:
                queryset = queryset.filter(content__icontains=search_keyword)
            elif search_type == &#39;all&#39;:
                queryset = queryset.filter(
                    Q(title__icontains=search_keyword) |
                    Q(content__icontains=search_keyword)
                )

        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context[&#39;search_type&#39;] = self.request.GET.get(&#39;search_type&#39;, &#39;&#39;)
        context[&#39;search_keyword&#39;] = self.request.GET.get(&#39;search_keyword&#39;, &#39;&#39;)
        return context
</code></pre>
<h2 id="6-주요-특징">6. 주요 특징</h2>
<ol>
<li><strong>대소문자 구분 없음</strong><ul>
<li><code>re.IGNORECASE</code> 플래그를 사용하여 대소문자 구분 없이 검색어 매칭</li>
</ul>
</li>
<li><strong>특수문자 처리</strong><ul>
<li><code>re.escape()</code>를 사용하여 특수문자가 포함된 검색어도 안전하게 처리</li>
</ul>
</li>
<li><strong>HTML 안전성</strong><ul>
<li><code>mark_safe()</code>를 사용하여 HTML 태그가 이스케이프되지 않도록 처리</li>
<li>검색어만 하이라이트되고 다른 HTML 태그는 그대로 유지</li>
</ul>
</li>
<li><strong>조건부 하이라이트</strong><ul>
<li>검색 타입에 따라 제목 또는 내용에만 하이라이트 적용</li>
</ul>
</li>
</ol>
<h2 id="7-성능-고려사항">7. 성능 고려사항</h2>
<ol>
<li><strong>데이터베이스 쿼리</strong><ul>
<li>검색은 데이터베이스 단에서 처리</li>
<li>하이라이트는 템플릿 렌더링 시점에서 처리</li>
</ul>
</li>
<li><strong>캐싱</strong><ul>
<li>검색 결과가 자주 바뀌지 않는다면 캐싱 고려</li>
<li>하이라이트 처리된 결과를 캐시할 수 있음</li>
</ul>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>검색어 하이라이트는 작은 기능이지만 사용자 경험을 크게 향상시킬 수 있습니다. Django의 템플릿 필터 시스템을 활용하면 이러한 기능을 깔끔하게 구현할 수 있습니다.</p>
<p>더 나아가 다음과 같은 개선사항을 고려해볼 수 있습니다:</p>
<ul>
<li>여러 검색어 동시 하이라이트</li>
<li>검색어 주변 컨텍스트만 표시</li>
<li>정규식을 사용한 더 복잡한 검색어 매칭</li>
</ul>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://docs.djangoproject.com/en/stable/howto/custom-template-tags/">Django 템플릿 필터 문서</a></li>
<li><a href="https://docs.python.org/3/library/re.html">Python 정규표현식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django Custom Template Filter 만들기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-Custom-Template-Filter-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-Custom-Template-Filter-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 17 Jun 2025 08:22:40 GMT</pubDate>
            <description><![CDATA[<p>Django 템플릿에서 기본 제공하는 필터 외에도 우리만의 커스텀 필터가 필요할 때가 있습니다. </p>
<p>오늘은 검색어 하이라이트 기능을 예시로 커스텀 템플릿 필터를 만드는 방법을 알아보겠습니다.</p>
<h2 id="1-기본-구조-만들기">1. 기본 구조 만들기</h2>
<p>먼저 Django 앱 디렉토리 안에 <code>templatetags</code> 패키지를 생성해야 합니다:</p>
<pre><code>myapp/
├── __init__.py
├── models.py
├── views.py
├── templatetags/      # 새로 생성
│   ├── __init__.py   # 빈 파일
│   └── custom_filters.py
└── templates/</code></pre><h2 id="2-커스텀-필터-작성하기">2. 커스텀 필터 작성하기</h2>
<p><code>custom_filters.py</code> 파일에 커스텀 필터를 작성합니다:</p>
<pre><code class="language-python">
from django import template
from django.utils.safestring import mark_safe
import re

# 템플릿 라이브러리 생성
register = template.Library()

@register.filter
def highlight(text, search):
    &quot;&quot;&quot;
    텍스트 내의 검색어를 하이라이트 처리하는 필터

    Args:
        text: 원본 텍스트
        search: 검색어

    Returns:
        검색어가 &lt;strong&gt; 태그로 감싸진 텍스트
    &quot;&quot;&quot;
    if not search or not text:
        return text

    # 특수문자 이스케이프 처리
    search = re.escape(search)
    # 대소문자 구분 없이 검색어 찾기
    pattern = re.compile(f&#39;({search})&#39;, re.IGNORECASE)
    # 검색어를 &lt;strong&gt; 태그로 감싸기
    highlighted = pattern.sub(r&#39;&lt;strong&gt;\\1&lt;/strong&gt;&#39;, str(text))

    # HTML 안전하게 반환
    return mark_safe(highlighted)</code></pre>
<h2 id="3-앱-등록-확인하기">3. 앱 등록 확인하기</h2>
<p>커스텀 필터를 사용하려면 해당 앱이 <code>INSTALLED_APPS</code>에 등록되어 있어야 합니다:</p>
<pre><code class="language-python"># settings.py

INSTALLED_APPS = [
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    ...
    &#39;myapp&#39;,  # 여기에 앱이 등록되어 있어야 함
]
</code></pre>
<h2 id="4-템플릿에서-사용하기">4. 템플릿에서 사용하기</h2>
<p>이제 템플릿에서 새로운 커스텀 필터를 사용할 수 있습니다:</p>
<pre><code class="language-html">{% extends &#39;base.html&#39; %}
{% load custom_filters %}  {# 커스텀 필터 로드 #}

{% block content %}
    &lt;h1&gt;{{ title|highlight:search_keyword }}&lt;/h1&gt;
    &lt;p&gt;{{ content|highlight:search_keyword }}&lt;/p&gt;
{% endblock %}
</code></pre>
<h2 id="5-다양한-필터-유형">5. 다양한 필터 유형</h2>
<h3 id="단순-필터">단순 필터</h3>
<pre><code class="language-python">@register.filter
def lower_with_underline(value):
    return f&quot;_{str(value).lower()}_&quot;
</code></pre>
<h3 id="인자를-받는-필터">인자를 받는 필터</h3>
<pre><code class="language-python">@register.filter
def replace_with(value, arg):
    return str(value).replace(arg, &#39;***&#39;)
</code></pre>
<h3 id="불리언-필터">불리언 필터</h3>
<pre><code class="language-python">@register.filter
def is_even(value):
    return int(value) % 2 == 0
</code></pre>
<h2 id="6-템플릿-태그와-필터의-차이">6. 템플릿 태그와 필터의 차이</h2>
<ul>
<li><strong>필터</strong>: 값을 변환하는 간단한 함수 (<code>{{ value|filter }}</code>)</li>
<li><strong>태그</strong>: 더 복잡한 로직 수행 가능 (<code>{% tag %}</code>)</li>
</ul>
<h2 id="7-주의사항">7. 주의사항</h2>
<ol>
<li><strong>보안</strong><ul>
<li><code>mark_safe()</code>는 신중하게 사용해야 함</li>
<li>XSS 공격 방지를 위해 사용자 입력은 항상 이스케이프 처리</li>
</ul>
</li>
<li><strong>성능</strong><ul>
<li>복잡한 연산은 뷰에서 처리하는 것이 좋음</li>
<li>템플릿 필터는 간단한 텍스트 처리에 적합</li>
</ul>
</li>
<li><strong>재사용성</strong><ul>
<li>여러 앱에서 공통으로 사용될 필터는 별도 유틸리티 앱으로 분리</li>
</ul>
</li>
</ol>
<h2 id="8-실제-사용-예시">8. 실제 사용 예시</h2>
<p>검색 결과 페이지에서 검색어 하이라이트 적용:</p>
<pre><code class="language-html">{# search_results.html #}
{% load custom_filters %}

&lt;div class=&quot;search-results&quot;&gt;
    {% for result in results %}
        &lt;div class=&quot;result-item&quot;&gt;
            &lt;h3&gt;{{ result.title|highlight:search_query }}&lt;/h3&gt;
            &lt;p&gt;{{ result.description|highlight:search_query }}&lt;/p&gt;
        &lt;/div&gt;
    {% endfor %}
&lt;/div&gt;
</code></pre>
<h2 id="마무리">마무리</h2>
<p>커스텀 템플릿 필터를 사용하면 템플릿에서 반복되는 로직을 깔끔하게 처리할 수 있습니다. </p>
<p>하지만 너무 복잡한 로직은 뷰나 모델에서 처리하는 것이 좋습니다. 
필터는 간단한 텍스트 변환이나 포맷팅에 가장 적합합니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://docs.djangoproject.com/en/stable/howto/custom-template-tags/">Django 공식 문서 - Custom template tags and filters</a></li>
<li><a href="https://docs.djangoproject.com/en/stable/ref/templates/builtins/">Django 템플릿 필터 목록</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django에서 카테고리 필터 구현하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django%EC%97%90%EC%84%9C-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%ED%95%84%ED%84%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django%EC%97%90%EC%84%9C-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%ED%95%84%ED%84%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 29 Apr 2025 13:53:41 GMT</pubDate>
            <description><![CDATA[<p>게시판이나 상품 목록과 같은 페이지에서 카테고리별 필터링은 매우 흔한 요구사항입니다. </p>
<p>오늘은 Django에서 간단하게 카테고리 필터를 구현하는 방법을 알아보겠습니다.</p>
<h2 id="1-모델-설정">1. 모델 설정</h2>
<p>먼저 카테고리를 가진 기본적인 모델을 정의합니다:</p>
<pre><code class="language-python">class Post(models.Model):
    CATEGORY_CHOICES = [
        (&#39;TECH&#39;, &#39;기술&#39;),
        (&#39;LIFE&#39;, &#39;일상&#39;),
        (&#39;HOBBY&#39;, &#39;취미&#39;),
        (&#39;OTHER&#39;, &#39;기타&#39;),
    ]

    title = models.CharField(max_length=200)
    content = models.TextField()
    category = models.CharField(
        max_length=20,
        choices=CATEGORY_CHOICES
    )
    created_at = models.DateTimeField(auto_now_add=True)
</code></pre>
<h2 id="2-view-구현">2. View 구현</h2>
<p>카테고리 필터링을 처리할 View를 작성합니다:</p>
<pre><code class="language-python">from django.views.generic import TemplateView
from django.shortcuts import redirect

class PostListView(TemplateView):
    template_name = &#39;posts/list.html&#39;

    def get(self, request, *args, **kwargs):
        # category 파라미터가 없으면 ALL로 리다이렉트
        if &#39;category&#39; not in request.GET:
            return redirect(f&quot;{request.path}?category=ALL&quot;)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # 선택된 카테고리 가져오기
        selected_category = self.request.GET.get(&#39;category&#39;, &#39;ALL&#39;)

        # 게시글 필터링
        posts = Post.objects.all().order_by(&#39;-created_at&#39;)
        if selected_category != &#39;ALL&#39;:
            posts = posts.filter(category=selected_category)

        context.update({
            &#39;posts&#39;: posts,
            &#39;category_choices&#39;: Post.CATEGORY_CHOICES,
            &#39;selected_category&#39;: selected_category,
        })
        return context
</code></pre>
<h2 id="3-템플릿-구현">3. 템플릿 구현</h2>
<p>카테고리 필터 UI를 구현합니다:</p>
<pre><code class="language-html">{% extends &#39;base.html&#39; %}

{% block content %}
    {# 카테고리 필터 버튼 #}
    &lt;div class=&quot;category-filter&quot;&gt;
        &lt;a href=&quot;?category=ALL&quot;
           class=&quot;category-btn {% if selected_category == &#39;ALL&#39; %}active{% endif %}&quot;&gt;
            전체
        &lt;/a&gt;
        {% for category_code, category_name in category_choices %}
            &lt;a href=&quot;?category={{ category_code }}&quot;
               class=&quot;category-btn {% if selected_category == category_code %}active{% endif %}&quot;&gt;
                {{ category_name }}
            &lt;/a&gt;
        {% endfor %}
    &lt;/div&gt;

    {# 게시글 목록 #}
    &lt;div class=&quot;post-list&quot;&gt;
        {% for post in posts %}
            &lt;div class=&quot;post-item&quot;&gt;
                &lt;h2&gt;{{ post.title }}&lt;/h2&gt;
                &lt;p&gt;카테고리: {{ post.get_category_display }}&lt;/p&gt;
                &lt;p&gt;작성일: {{ post.created_at|date:&quot;Y-m-d&quot; }}&lt;/p&gt;
            &lt;/div&gt;
        {% endfor %}
    &lt;/div&gt;
{% endblock %}

{% block extra_css %}
&lt;style&gt;
    .category-filter {
        margin: 20px 0;
    }
    .category-btn {
        display: inline-block;
        padding: 8px 16px;
        margin-right: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        text-decoration: none;
        color: #333;
    }
    .category-btn.active {
        background-color: #007bff;
        color: white;
        border-color: #007bff;
    }
    .post-item {
        margin-bottom: 20px;
        padding: 15px;
        border: 1px solid #eee;
        border-radius: 4px;
    }
&lt;/style&gt;
{% endblock %}
</code></pre>
<h2 id="주요-기능">주요 기능</h2>
<ol>
<li><strong>카테고리 없는 접속 처리</strong>:<ul>
<li>처음 페이지 접속 시 자동으로 <code>?category=ALL</code>로 리다이렉트</li>
</ul>
</li>
<li><strong>카테고리 필터링</strong>:<ul>
<li>&#39;ALL&#39; 선택 시 모든 게시글 표시</li>
<li>특정 카테고리 선택 시 해당 카테고리의 게시글만 표시</li>
</ul>
</li>
<li><strong>UI/UX</strong>:<ul>
<li>현재 선택된 카테고리 버튼 강조 표시</li>
<li>깔끔한 버튼 스타일링</li>
</ul>
</li>
</ol>
<h2 id="확장-가능성">확장 가능성</h2>
<ol>
<li><strong>페이지네이션 추가</strong>:<ul>
<li>카테고리 필터와 함께 페이지네이션 구현 가능</li>
</ul>
</li>
<li><strong>다중 필터</strong>:<ul>
<li>날짜, 태그 등 추가 필터 조건 구현 가능</li>
</ul>
</li>
<li><strong>정렬 기능</strong>:<ul>
<li>최신순, 조회순 등 정렬 옵션 추가 가능</li>
</ul>
</li>
</ol>
<p>이렇게 간단하게 Django에서 카테고리 필터를 구현할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django ORM의 기본 동작 이해하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-ORM%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EB%8F%99%EC%9E%91-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-ORM%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EB%8F%99%EC%9E%91-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 14 Apr 2025 14:21:28 GMT</pubDate>
            <description><![CDATA[<h2 id="objects-매니저의-동작-방식">objects 매니저의 동작 방식</h2>
<p>Django 모델의 <code>objects</code> 매니저를 사용할 때, <code>.all()</code>이 없어도 기본적으로 모든 객체를 대상으로 동작합니다.</p>
<p>예를 들어, 다음 두 쿼리는 동일합니다:</p>
<pre><code class="language-python"># 방법 1
Post.objects.all().annotate(comment_count=Count(&#39;comments&#39;))

# 방법 2
Post.objects.annotate(comment_count=Count(&#39;comments&#39;))
</code></pre>
<h2 id="왜-이렇게-동작하나요">왜 이렇게 동작하나요?</h2>
<ol>
<li><code>objects</code> 매니저는 기본적으로 모델의 전체 QuerySet을 가리킵니다.</li>
<li>모든 QuerySet 메서드는 새로운 QuerySet을 반환합니다.</li>
<li><code>.all()</code>은 사실상 현재 QuerySet의 복사본을 반환하는 것이므로 생략 가능합니다.</li>
</ol>
<h2 id="실제-sql-비교">실제 SQL 비교</h2>
<p>두 방법 모두 동일한 SQL을 생성합니다:</p>
<pre><code class="language-sql">SELECT
    &quot;post&quot;.*,
    COUNT(&quot;comments&quot;.&quot;id&quot;) as &quot;comment_count&quot;
FROM &quot;post&quot;
LEFT OUTER JOIN &quot;comments&quot; ON &quot;post&quot;.&quot;id&quot; = &quot;comments&quot;.&quot;post_id&quot;
GROUP BY &quot;post&quot;.&quot;id&quot;;
</code></pre>
<h2 id="다른-메서드들과의-비교">다른 메서드들과의 비교</h2>
<pre><code class="language-python"># 전체 객체 대상
Post.objects.annotate(...)  # 모든 Post 대상
Post.objects.all().annotate(...)  # 위와 동일

# 필터링된 객체 대상
Post.objects.filter(status=&#39;ACTIVE&#39;).annotate(...)  # 활성 Post만 대상
</code></pre>
<h2 id="예제">예제</h2>
<pre><code class="language-python"># 불필요한 .all() 사용
posts = (
    Post.objects
        .all()  # 불필요
        .annotate(comment_count=Count(&#39;comments&#39;))
        .filter(comment_count__gt=5)
)

# 최적화된 코드
posts = (
    Post.objects
        .annotate(comment_count=Count(&#39;comments&#39;))
        .filter(comment_count__gt=5)
)
</code></pre>
<h2 id="주의사항">주의사항</h2>
<p><code>.all()</code>이 실제로 필요한 경우가 있습니다:</p>
<ol>
<li>QuerySet을 복사하고 싶을 때</li>
</ol>
<pre><code class="language-python">original_qs = Post.objects.filter(status=&#39;ACTIVE&#39;)
new_qs = original_qs.all()  # 새로운 QuerySet 생성
</code></pre>
<ol>
<li>캐시된 QuerySet을 초기화하고 싶을 때</li>
</ol>
<pre><code class="language-python">posts = Post.objects.all()
# ... 일부 작업 수행 ...
posts = posts.all()  # QuerySet 초기화
</code></pre>
<p>이처럼 Django ORM에서는 <code>objects</code> 매니저 자체가 이미 전체 객체에 대한 QuerySet을 의미하므로, <code>.all()</code>은 대부분의 경우 생략 가능합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django annotate 심층 이해하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-annotate-%EC%8B%AC%EC%B8%B5-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-annotate-%EC%8B%AC%EC%B8%B5-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 07 Apr 2025 03:56:48 GMT</pubDate>
            <description><![CDATA[<h2 id="annotate의-동작-방식">annotate의 동작 방식</h2>
<p><code>annotate()</code>는 새로운 QuerySet을 반환하며, 다음과 같은 특징이 있습니다:</p>
<ol>
<li>새로운 필드를 추가한 QuerySet을 반환</li>
<li>실제 쿼리는 데이터가 필요한 시점에 실행 (Lazy Evaluation)</li>
<li>체이닝(chaining) 가능</li>
</ol>
<h2 id="예제로-이해하기">예제로 이해하기</h2>
<pre><code class="language-python"># 기본적인 annotate 사용
posts = Post.objects.annotate(
    comment_count=Count(&#39;comments&#39;)
)
# 이 시점에서는 아직 쿼리가 실행되지 않음

# 실제 데이터가 필요한 시점에 쿼리 실행
for post in posts:  # 이 시점에 쿼리 실행
    print(post.comment_count)
</code></pre>
<h2 id="체이닝-예제">체이닝 예제</h2>
<pre><code class="language-python">posts = Post.objects.annotate(
    comment_count=Count(&#39;comments&#39;)
).filter(
    comment_count__gt=5
).order_by(&#39;-comment_count&#39;)
</code></pre>
<h2 id="queryset의-특성-유지">QuerySet의 특성 유지</h2>
<ol>
<li>Lazy Evaluation (지연 평가)</li>
</ol>
<pre><code class="language-python"># 쿼리가 실행되지 않는 시점
posts = Post.objects.all()  # 쿼리 미실행
posts = posts.annotate(comment_count=Count(&#39;comments&#39;))  # 여전히 미실행

# 쿼리가 실행되는 시점
post_list = list(posts)  # 이때 실행
</code></pre>
<ol>
<li>체이닝 가능</li>
</ol>
<pre><code class="language-python">posts = Post.objects.annotate(
    comment_count=Count(&#39;comments&#39;)
).annotate(
    like_count=Count(&#39;likes&#39;)
).filter(
    comment_count__gt=5
)
</code></pre>
<h2 id="주의사항">주의사항</h2>
<ol>
<li>메모리 사용</li>
</ol>
<pre><code class="language-python"># 좋은 예제
for post in Post.objects.annotate(comment_count=Count(&#39;comments&#39;)):
    # 한 번에 하나의 레코드만 메모리에 로드
    print(post.comment_count)

# 주의가 필요한 예제
posts = list(Post.objects.annotate(comment_count=Count(&#39;comments&#39;)))
# 모든 레코드를 한 번에 메모리에 로드
</code></pre>
<ol>
<li>쿼리 최적화</li>
</ol>
<pre><code class="language-python"># 비효율적인 쿼리
posts = Post.objects.all()  # 첫 번째 쿼리
count = posts.annotate(comment_count=Count(&#39;comments&#39;))  # 두 번째 쿼리

# 최적화된 쿼리
posts = Post.objects.annotate(comment_count=Count(&#39;comments&#39;))  # 단일 쿼리
</code></pre>
<h2 id="실제-sql로의-변환">실제 SQL로의 변환</h2>
<p>다음 Django ORM 코드는:</p>
<pre><code class="language-python">Post.objects.annotate(comment_count=Count(&#39;comments&#39;))
</code></pre>
<p>이런 SQL로 변환됩니다:</p>
<pre><code class="language-sql">SELECT
    &quot;post&quot;.*,
    COUNT(&quot;comments&quot;.&quot;id&quot;) as &quot;comment_count&quot;
FROM &quot;post&quot;
LEFT OUTER JOIN &quot;comments&quot; ON &quot;post&quot;.&quot;id&quot; = &quot;comments&quot;.&quot;post_id&quot;
GROUP BY &quot;post&quot;.&quot;id&quot;;
</code></pre>
<h2 id="결론">결론</h2>
<p><code>annotate()</code>는 단순히 필드를 추가하는 것이 아니라, QuerySet의 모든 특성을 유지하면서 새로운 계산 필드를 추가하는 강력한 도구입니다. Lazy Evaluation 덕분에 효율적인 쿼리 실행이 가능하며, 체이닝을 통해 복잡한 쿼리도 깔끔하게 작성할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django ORM 고급 기능 이해하기: annotate, Window, F 객체]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-ORM-%EA%B3%A0%EA%B8%89-%EA%B8%B0%EB%8A%A5-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-annotate-Window-F-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-ORM-%EA%B3%A0%EA%B8%89-%EA%B8%B0%EB%8A%A5-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-annotate-Window-F-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Tue, 01 Apr 2025 03:56:13 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Django ORM에서 제공하는 고급 기능들을 활용하면 복잡한 데이터베이스 쿼리를 파이썬 코드로 쉽게 작성할 수 있습니다.
오늘은 <code>annotate</code>, <code>Window</code>, <code>F</code> 객체의 사용법과 실제 활용 사례를 살펴보겠습니다.</p>
<h2 id="1-f-객체-데이터베이스-필드-참조하기">1. F 객체: 데이터베이스 필드 참조하기</h2>
<h3 id="f-객체란">F 객체란?</h3>
<p><code>F()</code> 객체는 데이터베이스의 필드 값을 직접 참조할 수 있게 해주는 도구입니다. 실제 값을 파이썬으로 가져오지 않고 데이터베이스 수준에서 처리할 수 있게 해줍니다.</p>
<h3 id="기본-사용-예제">기본 사용 예제</h3>
<pre><code class="language-python">from django.db.models import F

# 조회수 증가 예제
Post.objects.filter(id=1).update(views=F(&#39;views&#39;) + 1)
</code></pre>
<p>이 코드는 다음 SQL과 유사합니다:</p>
<pre><code class="language-sql">UPDATE post SET views = views + 1 WHERE id = 1;
</code></pre>
<h2 id="2-window-함수-데이터베이스-윈도우-함수-사용하기">2. Window 함수: 데이터베이스 윈도우 함수 사용하기</h2>
<h3 id="window-함수란">Window 함수란?</h3>
<p>윈도우 함수는 행들의 집합(윈도우)에 대해 계산을 수행하는 SQL 함수입니다. Django에서는 <code>Window</code> 클래스를 통해 이를 구현할 수 있습니다.</p>
<h3 id="실제-활용-예제">실제 활용 예제</h3>
<p>게시글에 행 번호를 부여하는 예제를 살펴보겠습니다:</p>
<pre><code class="language-python">from django.db.models import F
from django.db.models.expressions import Window
from django.db.models.functions import RowNumber

posts = Post.objects.annotate(
    row_num=Window(
        expression=RowNumber(),
        order_by=F(&#39;id&#39;).desc()
    )
)
</code></pre>
<p>이는 다음 SQL과 유사합니다:</p>
<pre><code class="language-sql">SELECT *,
    ROW_NUMBER() OVER (ORDER BY id DESC) as row_num
FROM post;
</code></pre>
<h2 id="3-annotate-쿼리셋에-필드-추가하기">3. annotate: 쿼리셋에 필드 추가하기</h2>
<h3 id="annotate란">annotate란?</h3>
<p><code>annotate()</code>는 쿼리셋에 임시 필드를 추가하는 메서드입니다. 계산된 필드나 집계 결과를 추가할 때 사용합니다.</p>
<h3 id="기본-사용-예제-1">기본 사용 예제</h3>
<pre><code class="language-python"># 댓글 수 계산
from django.db.models import Count

posts = Post.objects.annotate(
    comment_count=Count(&#39;comments&#39;)
)
</code></pre>
<h3 id="복합-예제">복합 예제</h3>
<p>위의 모든 기능을 조합한 실제 활용 예제를 살펴보겠습니다:</p>
<pre><code class="language-python">from django.db.models import F, Window
from django.db.models.functions import RowNumber

class PostView(TemplateView):
    def get_context_data(self, **kwargs):
        # 게시글 목록 가져오기
        posts = (
            Post.objects.annotate(
                # 행 번호 부여
                row_num=Window(
                    expression=RowNumber(),
                    order_by=[F(&#39;id&#39;)]
                )
            )
            .exclude(status=&#39;DELETED&#39;)  # 삭제된 게시글 제외
            .order_by(&#39;-id&#39;)  # 최신순 정렬
        )
        return {&#39;posts&#39;: posts}
</code></pre>
<h2 id="각-기능의-활용-시-장점">각 기능의 활용 시 장점</h2>
<ol>
<li>F 객체<ul>
<li>레이스 컨디션 방지</li>
<li>데이터베이스 수준의 연산 수행</li>
<li>메모리 사용 최적화</li>
</ul>
</li>
<li>Window 함수<ul>
<li>복잡한 행 번호 부여</li>
<li>순위 계산</li>
<li>누적 합계 계산</li>
</ul>
</li>
<li>annotate<ul>
<li>동적 필드 추가</li>
<li>계산된 값을 쿼리셋에 포함</li>
<li>재사용 가능한 필드 생성</li>
</ul>
</li>
</ol>
<h2 id="주의사항">주의사항</h2>
<ol>
<li>성능 고려사항<ul>
<li>Window 함수는 데이터베이스 부하를 증가시킬 수 있음</li>
<li>대량의 데이터에 대해서는 실행 계획 확인 필요</li>
</ul>
</li>
<li>데이터베이스 호환성<ul>
<li>Window 함수는 SQLite에서 제한적으로 지원</li>
<li>PostgreSQL에서 가장 잘 동작</li>
</ul>
</li>
</ol>
<h2 id="결론">결론</h2>
<p>Django ORM의 고급 기능들을 활용하면 복잡한 데이터베이스 작업을 파이썬 코드로 깔끔하게 표현할 수 있습니다. 
하지만 각 기능의 특성과 성능 영향을 이해하고 적절히 사용하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django ORM: values() 메서드 사용 시 get_field_display() 동작하지 않는 문제]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-ORM-values-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%82%AC%EC%9A%A9-%EC%8B%9C-getfielddisplay-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-ORM-values-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%82%AC%EC%9A%A9-%EC%8B%9C-getfielddisplay-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sun, 30 Mar 2025 07:29:13 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>Django에서 모델의 choices 필드를 사용할 때, <code>get_[field]_display()</code> 메서드를 통해 쉽게 표시값을 가져올 수 있습니다. 하지만 <code>.values()</code>를 사용하면 이 메서드가 동작하지 않는 상황이 발생합니다.</p>
<p>예를 들어, 다음과 같은 모델이 있다고 가정해보겠습니다:</p>
<pre><code class="language-python">class Notice(models.Model):
    CATEGORY_CHOICES = (
        (&#39;NOTICE&#39;, &#39;공지&#39;),
        (&#39;EVENT&#39;, &#39;이벤트&#39;),
        (&#39;NEWS&#39;, &#39;뉴스&#39;),
    )
    category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)
</code></pre>
<p>일반적으로는 다음과 같이 사용할 수 있습니다:</p>
<pre><code class="language-python">notice = Notice.objects.first()
print(notice.get_category_display())  # &#39;공지&#39; 출력
</code></pre>
<p>하지만 <code>.values()</code>를 사용하면:</p>
<pre><code class="language-python">notice = Notice.objects.values(&#39;category&#39;).first()
# notice.get_category_display()  # AttributeError 발생!
</code></pre>
<h2 id="원인">원인</h2>
<p><code>.values()</code>는 모델 인스턴스가 아닌 딕셔너리를 반환합니다. 따라서 모델 인스턴스의 메서드인 <code>get_[field]_display()</code>를 사용할 수 없게 됩니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="1-values-사용하지-않기">1. values() 사용하지 않기</h3>
<p>가장 간단한 방법은 <code>.values()</code>를 사용하지 않고 모델 인스턴스를 직접 사용하는 것입니다:</p>
<pre><code class="language-python">notices = Notice.objects.all()
for notice in notices:
    print(notice.get_category_display())
</code></pre>
<h3 id="2-annotate로-display-필드-추가하기">2. annotate()로 display 필드 추가하기</h3>
<p>성능상의 이유로 <code>.values()</code>가 필요한 경우, <code>annotate()</code>를 사용하여 display 값을 직접 추가할 수 있습니다:</p>
<pre><code class="language-python">from django.db.models import Case, When, Value, CharField

notices = Notice.objects.annotate(
    category_display=Case(
        *[When(category=k, then=Value(v)) for k, v in Notice.CATEGORY_CHOICES],
        output_field=CharField(),
    )
).values(&#39;category&#39;, &#39;category_display&#39;)
</code></pre>
<h2 id="각-방법의-장단점">각 방법의 장단점</h2>
<h3 id="values를-사용하지-않는-방법">values()를 사용하지 않는 방법</h3>
<p>장점:</p>
<ul>
<li>코드가 간단하고 직관적</li>
<li>모델의 모든 기능을 사용 가능</li>
</ul>
<p>단점:</p>
<ul>
<li>모델 인스턴스 전체를 메모리에 로드</li>
<li>대량의 데이터 처리 시 성능 저하 가능성</li>
</ul>
<h3 id="annotate를-사용하는-방법">annotate()를 사용하는 방법</h3>
<p>장점:</p>
<ul>
<li>필요한 필드만 선택적으로 가져올 수 있음</li>
<li>데이터베이스 레벨에서 처리되어 성능상 이점</li>
</ul>
<p>단점:</p>
<ul>
<li>코드가 다소 복잡</li>
<li>choices 정의가 변경될 경우 annotate 로직도 수정 필요</li>
</ul>
<h2 id="결론">결론</h2>
<p><code>.values()</code>의 사용은 성능 최적화에 도움이 될 수 있지만, <code>get_[field]_display()</code>와 같은 모델 메서드를 사용할 수 없다는 제한이 있습니다. 프로젝트의 요구사항과 성능 특성을 고려하여 적절한 방법을 선택하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[과거 커밋에 잘못 포함된 파일 제외하기(git cherry-pick)]]></title>
            <link>https://velog.io/@i-am-not-kangjik/%EA%B3%BC%EA%B1%B0-%EC%BB%A4%EB%B0%8B%EC%97%90-%EC%9E%98%EB%AA%BB-%ED%8F%AC%ED%95%A8%EB%90%9C-%ED%8C%8C%EC%9D%BC-%EC%A0%9C%EC%99%B8%ED%95%98%EA%B8%B0git-cherry-pick</link>
            <guid>https://velog.io/@i-am-not-kangjik/%EA%B3%BC%EA%B1%B0-%EC%BB%A4%EB%B0%8B%EC%97%90-%EC%9E%98%EB%AA%BB-%ED%8F%AC%ED%95%A8%EB%90%9C-%ED%8C%8C%EC%9D%BC-%EC%A0%9C%EC%99%B8%ED%95%98%EA%B8%B0git-cherry-pick</guid>
            <pubDate>Thu, 27 Mar 2025 02:36:34 GMT</pubDate>
            <description><![CDATA[<p>작업을 하던 도중 HEAD보다 세 개 전의 커밋에서</p>
<p>버전 관리가 필요하지 않은 파일이 포함됐다.</p>
<p>미리 gitignore에 등록을 했다면 발생하지 않았을 문제이지만,</p>
<p>저질러졌을 경우 해결하는 방법을 기록하고자 한다.</p>
<ol>
<li>먼저 현재 상태를 백업한다. (혹시 모를 상황에 대비)</li>
</ol>
<pre><code class="language-bash">git branch backup-branch
</code></pre>
<ol start="2">
<li>제외하고 싶은 파일이 있는 커밋 이전으로 돌아갑니다 (문제가 발생한 3개 전의 커밋 이전인 4개 전의 커밋으로 이동)</li>
</ol>
<pre><code class="language-bash">git reset --hard HEAD~4
</code></pre>
<ol start="3">
<li>이제 문제의 커밋부터 현재까지 모든 커밋을 하나씩 cherry-pick하면서 진행한다.</li>
</ol>
<pre><code class="language-bash">git cherry-pick -n 문제_커밋_해시
</code></pre>
<p>(-n 옵션은 자동으로 커밋하지 않고 변경사항만 스테이징 영역에 추가한다.)</p>
<ol start="4">
<li>스테이징 영역에서 제외하고 싶은 파일을 제외한다.</li>
</ol>
<pre><code class="language-bash">git reset HEAD 제외할_파일_이름
</code></pre>
<ol start="5">
<li>나머지 파일을 커밋한다.</li>
</ol>
<pre><code class="language-bash">git commit -m &quot;원래 커밋 메시지&quot;
</code></pre>
<ol start="6">
<li>나머지 커밋들도 순서대로 cherry-pick한다.</li>
</ol>
<pre><code class="language-bash">git cherry-pick 다음_커밋_해시
git cherry-pick 그다음_커밋_해시
...
</code></pre>
<ol start="7">
<li>(선택) 이미 push를 했던 경우 모든 작업이 끝나면 변경된 히스토리를 원격 저장소에 강제로 푸시한다.</li>
</ol>
<pre><code class="language-bash">git push --force
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 태깅 기능 추가하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%ED%83%9C%EA%B9%85-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%ED%83%9C%EA%B9%85-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 09 Feb 2025 12:05:39 GMT</pubDate>
            <description><![CDATA[<p>블로그의 일반적인 기능으로 태그를 사용해 게시물을 분류하는 기능이 있다.
태그를 사용하면 간단한 키워드를 사용해 컨텐츠를 비계층적으로 분류할 수 있다.
태그는 단순히 게시글에 할당할 수 있는 레이블 또는 키워드다. 
서드파티 쟝고 태깅 애플리케이션을 프로젝트에 통합해 태깅 시스템을 만들어보자.</p>
<p><code>django-taggit</code> 은 주로 <code>Tag</code> 모델과 관리자를 제공해 모든 모델에 태그를 쉽게 추가할 수 있게 해 주는 재사용 가능한 애플리케이션이다.</p>
<p>먼저 아래의 명령어를 통해 <code>django-taggit</code> 을 설치해보자.</p>
<pre><code class="language-python">pip install django-taggit==3.0.0</code></pre>
<p>그 다음 settings.py파일의 INSTALLED_APPS 설정에 아래와 같이 <code>taggit</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;blog.apps.BlogConfig&#39;,
    &#39;taggit&#39;,
]</code></pre>
<p>blog의 models.py파일을 열고 다음 코드와 같이 django-taggit에서 제공하는 <code>TaggableManager</code> 관리자를 <code>Post</code> 모델에 추가한다.</p>
<pre><code class="language-python">from taggit.managers import TaggableManager

class Post(models.Model):
        ...
    tags = TaggableManager()</code></pre>
<p>이제 <code>tags</code> 관리자를 사용해 태그를 추가, 검색 및 제거할 수 있다.</p>
<p><code>Tag</code> 모델은 태그를 저장하는 데 사용된다. 이 모델은 <code>name</code> 과 <code>slug</code> 필드를 가지고 있다.</p>
<p><code>TaggedItem</code> 모델은 관련 태깅된 객체를 저장하는 데 사용되는데, 관련 <code>Tag</code> 객체의 <code>Foreign Key</code> 필드가 있다. 또 <code>ContentType</code> 객체의 <code>ForeignKey</code> 와 태그가 지정된 객체의 id를 저장하기 위한 <code>IntegerField</code> 가 있다. <code>content_type</code> 과 <code>object_id</code> 필드를 결합해 프로젝트의 모든 모델과 일반화한 관계를 형성할 수 있는데, 이렇게 하면 Tag 인스턴스와 애플리케이션의 다른 모델의 인스턴스 관계를 만들 수 있다.</p>
<p>마이그레이션을 진행한 후, 이제 태그 관리자를 사용하는 방법을 살펴보자.</p>
<pre><code class="language-python">python manage.py makemigrations
python manage.py migrate
python manage.py shell</code></pre>
<p>다음 코드를 실행해 id가 1인 게시글 하나를 조회한다.</p>
<pre><code class="language-python">&gt;&gt;&gt; from blog.models import Post
&gt;&gt;&gt; post = Post.objects.get(id=1)</code></pre>
<p>그 다음 일부 태그를 추가하고 태그를 검색해 성공적으로 추가되었는지 확인한다.</p>
<pre><code class="language-python">&gt;&gt;&gt; post.tags.add(&#39;music&#39;, &#39;jazz&#39;, &#39;django&#39;)
&gt;&gt;&gt; post.tags.all()
&lt;QuerySet [&lt;Tag: django&gt;, &lt;Tag: music&gt;, &lt;Tag: jazz&gt;]&gt;</code></pre>
<p>끝으로 태그를 제거하고 태그 목록을 다시 확인한다.</p>
<pre><code class="language-python">&gt;&gt;&gt; post.tags.remove(&#39;django&#39;)
&gt;&gt;&gt; post.tags.all()
&lt;QuerySet [&lt;Tag: music&gt;, &lt;Tag: jazz&gt;]&gt;</code></pre>
<p>post.tags 관리자를 사용해 모델에서 태그를 추가, 조회, 제거하는 과정은 위와 같이 간단하다.</p>
<p>다음으로 개발 서버를 시작해 브라우저에서 관리페이지의 Tags 객체들을 살펴보자.</p>
<p><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/i-am-not-kangjik/post/4bfad931-56de-4185-8d41-abdc5abe298a/image.png" alt=""></p>
<p>Tags에 들어가면 아래와 같은 태그의 목록을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/24a356b7-5cce-4d3b-be7f-6180bb15c299/image.png" alt=""></p>
<p>jazz 태그를 눌러 들어가보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/afc27151-64a1-4602-8caf-852715d0e8bc/image.png" alt=""></p>
<p>Tagged Item에서 blog의 post의 2번 게시글이 이 태그를 사용중인 것을 확인할 수 있다.</p>
<p>이제 post모델의 2번 게시글로 들어가 Tags필드를 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/32f15e28-ab4d-4b61-af1b-4441f822eee0/image.png" alt=""></p>
<p>이제 태그를 표시하도록 블로그 게시글을 편집해보자.
<code>blog/post/list.html</code> 을 편집해 tag 관련 HTML 코드를 추가한다.</p>
<pre><code class="language-python">&lt;p class=&quot;tags&quot;&gt;Tags: {{ post.tags.all|join:&quot;, &quot; }}&lt;/p&gt;</code></pre>
<p><code>join</code> 템플릿 필터는 파이썬 문자열 <code>join()</code> 메서드와 동일하게 동작하는데, 요소들을 지정된 문자열로 연결한다.</p>
<p>브라우저에서 blog 메인 페이지로 이동해보면 아래와 같이 제목 아래에 태그 목록이 나타나게 된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/415046b9-9328-4d67-8814-5159aa773240/image.png" alt=""></p>
<p>다음으로 사용자가 특정 태그로 태깅된 게시글들을 조회할 수 있도록 post_list 뷰를 편집해보자.</p>
<p>blog의 views.py파일을 열어 django-taggit에서 Tag 모델을 불러오고 필요에 따라 게시글을 태그별로 필터링하도록 post_list 뷰를 편집해보자.</p>
<pre><code class="language-python">from taggit.models import Tag

def post_list(request, tag_slug=None):
    post_list = Post.published.all()
    tag = None
    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        post_list = post_list.filter(tags__in=[tag])

    paginator = Paginator(post_list, 3)
    page_number = request.GET.get(&#39;page&#39;, 1)
    try:
        posts = paginator.page(page_number)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request, &#39;blog/post/list.html&#39;, {&#39;posts&#39;: posts, &#39;tag&#39;: tag}</code></pre>
<p>post_list 뷰의 동작 방식은 아래와 같다.</p>
<ol>
<li>기본 값이 <code>None</code>인 <code>tag_slug</code> 매개 변수를 사용한다.
이 매개 변수는 URL로 전달된다.</li>
<li>뷰에서 기본 QuerySet을 만들어 게시된 모든 게시글을 조회하고 지정된 태그 슬러그가 있는 경우 <code>get_object_or_404()</code> 함수를 사용해 지정된 슬러그로 Tag 객체를 찾는다.</li>
<li>주어진 태그가 포함된 게시글을 기준으로 게시글 목록을 필터링한다.
다대다 관계이기 때문에 주어진 목록에 포함된 태그를 기준으로 게시글을 필터링해야 한다.
이 경우에는 목록에 하나의 태그만이 포함되어 있다.
<code>__in</code> 필드 검색을 사용한다.</li>
<li><code>render()</code> 함수에서 새로운 tag 변수를 템플릿에 전달한다.</li>
</ol>
<p>QuerySet은 필요시에 실행되므로, 게시글을 검색하기 위한 쿼리셋은 템플릿을 렌더링할 때나 게시글 목록을 반복할 때만 실행된다.</p>
<p>blog의 urls.py파일의 클래스 기반 PostListView URL패턴을 주석처리하고 기존 post_list 뷰의 주석 처리를 해제한다.</p>
<pre><code class="language-python"> path(&#39;&#39;, views.post_list, name=&#39;post_list&#39;),
    # path(&#39;&#39;, views.PostListView.as_view(), name=&#39;post_list&#39;),</code></pre>
<p>게시글을 태그별로 나열하기 위해 아래의 URL 패턴을 추가해준다.</p>
<pre><code class="language-python">path(&#39;tag/&lt;slug:tag_slug&gt;/&#39;, views.post_list, name=&#39;post_list_by_tag&#39;),</code></pre>
<p>두 패턴이 동일한 뷰를 가리키지만 이름이 다르다.
첫 번쨰 패턴은 매개 변수 없이 post_list 뷰를 호출하지만, 두 번째 패턴은 tag_slug 매개 변수를 사용해 뷰를 호출한다. slug path converter를 사용해 슬러그 타입 문자열로 매개 변수를 매칭시킨다.</p>
<p>post_list 뷰를 사용하고 있으니 게시글 목록 템플릿을 수정해 페이징에서 posts 객체를 사용하도록 바꿔주자.</p>
<pre><code class="language-python">{% include &quot;pagination.html&quot; with page=posts %}</code></pre>
<p>아래의 코드를 게시글 목록 페이지의 h1 아래에 추가한다.</p>
<p>특정 태그로 태깅된 게시글로 필터링할 경우 지정된 태그를 표시하기 위함이다.</p>
<pre><code class="language-python">{% if tag %}
    &lt;h2&gt;Posts tagged with &quot;{{ tag.name }}&quot;&lt;/h2&gt;
{% endif %}</code></pre>
<p>기존 태그 목록을 표시하던 부분을 아래와 같이 바꿔주자.</p>
<pre><code class="language-python">&lt;p class=&quot;tags&quot;&gt;
        Tags:
        {% for tag in post.tags.all %}
            &lt;a href=&quot;{% url &quot;blog:post_list_by_tag&quot; tag.slug %}&quot;&gt;
                {{ tag.name }}
            &lt;/a&gt;
            {% if not forloop.last %}, {% endif %}
        {% endfor %}
    &lt;/p&gt;</code></pre>
<p>게시글에 태깅된 모든 태그들을 반복하면서 해당 태그로 게시글들을 필터링 할 수 있는 커스텀 URL 링크를 표시하도록 했다. URL 이름과 같이 태그의 slug를 매개 변수로 <code>{% url &quot;blog:post_list_by_tag*&quot;* tag.slug %}</code> 와 같이 URL을 만든다.</p>
<p>게시글 목록에서 태그를 클릭해보면,
아래와 같이 클리된 태그로 필터링된 게시글 목록이 표시되게 된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/8eef93ea-9403-43b6-8882-6e5249fd3731/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 댓글 시스템 만들기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%EB%8C%93%EA%B8%80-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%EB%8C%93%EA%B8%80-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:24:05 GMT</pubDate>
            <description><![CDATA[<p>blog 애플리케이션을 사용자들이 게시글에 댓글을 달 수 있도록 확장해보자.
댓글 시스템을 만들기 위해선 다음과 같은 요소들이 필요하다.</p>
<ul>
<li>게시글의 사용자 댓글을 저장하는 모델</li>
<li>댓글을 작성해서 제출하고 데이터를 검증할 수 있는 폼</li>
<li>폼을 처리하고 DB에 새로운 댓글을 저장하는 뷰</li>
<li>게시글 상세 템플릿에 포함할 수 있는 댓글 목록과 새로운 댓글 추가를 위한 템플릿</li>
</ul>
<h2 id="댓글-모델-만들기">댓글 모델 만들기</h2>
<p>게시글의 사용자 댓글을 저장하는 모델을 만들어보자.</p>
<p>models.py에 아래의 모델을 추가했다.</p>
<pre><code class="language-python">class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name=&#39;comments&#39;)
    name = models.CharField(max_length=80)
    email = models.EmailField()
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=True)

    class Meta:
        ordering = [&#39;created&#39;]
        indexes = [
            models.Index(fields=[&#39;created&#39;]),
        ]

    def __str__(self):
        return f&quot;{self.post} 게시글의 {self.name}의 댓글&quot;
</code></pre>
<p>각 댓글을 단일 게시글과 연결하기 위해 <code>Foreign Key</code> 필드를 추가했다.
댓글은 하나의 게시글에 작성되고, 각 게시글은 여러 개의 댓글이 있을 수 있기 때문에 이러한 다대일 관계는 Comment 모델에서 정의된다.</p>
<p><code>related_name</code> 속성을 사용해 관련된 객체에서 다시 이 객체로의 관계에 사용되는 속성의 이름을 지정할 수 있다. <code>comment.post</code> 를 사용해서 댓글 객체의 해당 게시글을 조회하고, <code>post.comments.all()</code> 을 사용해 게시글과 관련된 모든 댓글을 조회할 수 있다.</p>
<p><code>related_name</code> 속성을 정의하지 않으면 쟝고는 소문자로 모델의 이름에 <code>_set</code> 을 붙여서 (<code>comment_set</code>) 관련 객체와 모댈 객체 관계의 이름으로 사용한다.</p>
<p>댓글 상태를 제어하기 위해 부울 필드 <code>active</code> 를 정의했다. 이 필드를 사용하면 관리 사이트에서 부적절한 댓글을 수동으로 비활성화 할 수 있다. 기본 값은 <code>default=True</code> 로 모든 댓글이 기본적으로 활성화됨을 나타낸다.</p>
<p>댓글이 생성된 날짜와 시간을 저장하기 위해 <code>create</code> 필드를 정의했는데, <code>auto_now_add</code>를 사용하면 객체를 생성할 때 날짜가 자동으로 저장된다. 모델의 <code>Meta</code> 클래스에 <code>ordering=[&#39;created&#39;]</code> 를 추가해 댓글을 기본적으로 시간순으로 정렬했다. 그리고 created 필드에 대한 인덱스를 오름차순으로 추가했다.
이렇게 하면 created 필드를 통해 DB 조회와 조회된 결과를 정렬하는 성능이 향상된다.</p>
<h2 id="관리-사이트에-댓글-추가">관리 사이트에 댓글 추가</h2>
<p>다음으로 관리 사이트에 새로운 모델을 추가해보자.</p>
<p>blog 앱의 admin.py파일을 열고 Comment 모델을 가져와 ModelAdmin 클래스를 추가하자.</p>
<pre><code class="language-python">from .models import Post, Comment

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = [&#39;name&#39;, &#39;email&#39;, &#39;post&#39;, &#39;created&#39;, &#39;active&#39;]
    list_filter = [&#39;active&#39;, &#39;created&#39;, &#39;updated&#39;]
    search_fields = [&#39;name&#39;, &#39;email&#39;, &#39;body&#39;]</code></pre>
<p>이제 관리사이트에 접속하면 아래와 같이 Comments 모델이 표시되고, 추가를 클릭할 경우 새로운 댓글을 추가하기 위한 폼이 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/dc5f8199-c895-46c5-afca-2401c91a31ef/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/51988716-da02-4579-bcc4-12bf2dc15d25/image.png" alt=""></p>
<p>이제 관리 사이트를 사용해서 Comment 인스턴스들을 관리할 수 있다.</p>
<h2 id="모델에서-폼-만들기">모델에서 폼 만들기</h2>
<p>사용자가 블로그 게시글에 댓글을 달 수 있도록 폼을 작서앻야 한다.
쟝고에는 폼을 만드는데 사용할 수 있는 두 가지 기본 클래스 <code>Form</code>과 <code>ModelForm</code>이 있다.
사용자가 이메일로 게시글을 공유할 수 있게 만들 때는 <code>Form</code> 클래스를 사용했었다.
이번에는 기존 Comment 모델을 활용하고 동적으로 폼을 만들기 위해 <code>ModelForm</code> 을 사용할 것이다.</p>
<p>forms.py파일을 편집해 아래의 코드를 추가한다.</p>
<pre><code class="language-python">from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = [&#39;name&#39;, &#39;email&#39;, &#39;body&#39;]</code></pre>
<p>모델에서 폼을 만들려면 <code>Meta</code> 클래스에서 폼을 빌드할 모델을 지정하기만 하면 된다.
<code>ModelForm</code> 을 사용하면 쟝고가 모델을 검사하고 해당 폼을 동적으로 만든다.
모델의 각 필드 유형에는 해당하는 기본 폼 필드 유형이 존재한다. 폼 유효성 검사를 위해 모델 필드의 속성을 고려할 필요가 있다. 기본적으로 쟝고는 모델에 포함된 각 필드에 대한 폼 필드를 만드는데 <code>fields</code> 속성을 사용해 폼에 포함할 필드를 쟝고에 명시적으로 알리거나 <code>exclude</code> 속성을 사용해 제외할 필드를 정의할 수 있다.</p>
<p><code>CommentForm</code> 에서는 <code>name, email, body</code> 필드를 명시적으로 포함하고 있다.</p>
<h2 id="뷰에서--modelforms-처리하기">뷰에서  ModelForms 처리하기</h2>
<p>이메일로 게시글을 공유할 때 HTTP 메서드로 구분해 동일한 뷰를 사용해 폼을 표시하고, 제출된 데이터를 처리했었다. 댓글의 경우 게시글 상세 페이젱 댓글 폼을 추가하고 폼 입력 값들을 처리하기 위한 별도의 뷰를 만들어야 한다. 폼을 처리하는 새로운 뷰는 댓글이 DB에 저장된 다음에야 사용자가 게시글 상세 뷰로 돌아갈 수 있도록 한다.</p>
<p>views.py파일에 아래의 코드를 추가해 만들어보자.</p>
<pre><code class="language-python">from django.views.decorators.http import require_POST

@require_POST
def post_comment(request, post_id):
    post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
    comment = None
    form = CommentForm(data=request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.post = post
        comment.save()
    return render(request, &#39;blog/post/comment.html&#39;, {&#39;post&#39;: post, &#39;form&#39;: form, &#39;comment&#39;: comment})</code></pre>
<p><code>request</code> 객체와 변수 <code>post_id</code> 를 매개 변수로 취하는 <code>post_comment</code> 뷰를 정의했다.
쟝고에서 제공하는 <code>require_POST</code> 데코레이터를 사용해 이 뷰는 <code>POST</code> 요청만 허용하게 한다.
쟝고에서는 뷰에 허용되는 HTTP 메서드를 제한할 수 있다.
다른 HTTP 메서드로 뷰에 접근하게 될 경우 쟝고에서 HTTP 405 오류가 발생한다.</p>
<p>이 뷰의 동작은 아래와 같다.</p>
<ol>
<li><code>get_object_or_404()</code> 함수를 사용해 id로 게시된 게시글을 조회한다.</li>
<li>초기값 None으로 <code>comment</code> 변수를 정의하고, 이 변수는 Comment 객체가 생성될 때 그 객체를 저장하는 데 사용된다.</li>
<li>수신된 POST 데이터를 사용해 폼을 인스턴스화하고 <code>is_valid()</code> 메서드를 사용해 유효성을 검사한다. 폼이 유효하지 않으면 유효성 검사 오류를 포함해 템플릿이 렌더링된다.</li>
<li>폼 값이 유효할 경우 폼의 <code>save()</code> 메서드를 호출해 새로운 Comment 객체를 만들고 이를 comment 변수에 할당한다. <code>comment = form.save(commit=False)</code></li>
<li><code>save()</code> 메서드는 폼이 연결된 모델의 인스턴스를 생성하고 DB에 저장한다.
<code>commit=False</code> 로 설정하고 호출하면 모델 인스턴스가 생성되지만, 데이터베이스에 저장되지 않는다. 이를 이용해 객체를 최종적으로 저장하기 전에 수정할 수 있다.<ol>
<li><code>save()</code> 메서드는 ModelForm에는 사용할 수 있지만, Form 인스턴스는 연결된 모델이 없어 사용할 수 없다.</li>
</ol>
</li>
<li>작성한 댓글의 게시글을 지정한다.
<code>comment.post = post</code></li>
<li><code>save()</code> 메서드를 호출해 새로운 댓글을 DB에 저장한다.
<code>comment.save()</code></li>
<li><code>blog/post/comment.html</code> 템플릿을 렌더링하고 템플릿 context에 <code>post, form, comment</code> 객체를 전달한다.</li>
</ol>
<p>이제 이 뷰의 URL 패턴을 추가해 보자.</p>
<pre><code class="language-python">path(&#39;&lt;int:post_id&gt;/comment/&#39;, views.post_comment, name=&#39;post_comment&#39;),</code></pre>
<h2 id="댓글-폼용-템플릿-만들기">댓글 폼용 템플릿 만들기</h2>
<p>댓글 폼의 템플릿은  아래의 두 곳에서 쓰이게 된다.</p>
<ol>
<li>사용자가 댓글을 게시할 수 있도록 하기 위한 <code>post_detail</code> 뷰와 연결된 게시글 상세 템플릿</li>
<li>폼에 오류가 있을 경우 폼을 다시 표시하기 위한 <code>post_comment</code> 뷰와 연결된 게시글 댓글 템플릿</li>
</ol>
<p>폼 템플릿을 만들고 나서 <code>{% include %}</code> 템플릿 태그를 사용해 다른 두 템플릿에서 불러들이자.
그 후 <code>templates/blog/post/</code> 디렉토리에 새로운 디렉토리 <code>include/</code> 를 만들자.
이 디렉토리에 <code>comment_form.html</code> 이라는 새로운 파일을 생성한다.</p>
<pre><code class="language-python">&lt;h2&gt; 새로운 댓글 작성하기 &lt;/h2&gt;
&lt;form action=&quot;{% url &#39;blog:post_comment&#39; post.id %}&quot; method=&quot;post&quot;&gt;
    {{ form.as_p }}
    {% csrf_token %}
    &lt;input type=&quot;submit&quot; value=&quot;댓글 작성하기&quot;&gt;
&lt;/form&gt;</code></pre>
<p>comment_form.html에 위와 같이 입력했다.</p>
<p>이 템플릿에서는 <code>{% url %}</code> 템플릿 태그를 사용해 동적으로 HTML <code>&lt;form&gt;</code> 엘리먼트를 처리하는 action URL을 만든다. 이 폼은 POST 메서드로 제출되기 때문에 CSRF 보호를 위해 <code>{% csrf_token %}</code> 을 포함시킨다.</p>
<p><code>templates/blog/post/</code> 디렉토리에 새로운 comment.html 파일을 만들어 보자.</p>
<pre><code class="language-python">{% extends &quot;blog/base.html&quot; %}
{% block title %}댓글 작성하기{% endblock %}
{% block content %}
{% if comment %}
    &lt;h2&gt; 댓글이 추가되었습니다. &lt;/h2&gt;
    &lt;p&gt;&lt;a href=&quot;{{ post.get_absolute_url }}&quot;&gt;게시글로 돌아가기&lt;/a&gt;&lt;/p&gt;
{% else %}
    {% include &quot;blog/post/includes/comment_form.html&quot;%}
{% endif %}
{% endblock %}
</code></pre>
<p>이 템플릿은 두 가지 다른 분기를 다룬다.</p>
<ul>
<li>제출된 폼 데이터가 유효할 경우 <code>comment</code> 변수에 생성된 댓글 객체가 담기고 성공 메시지가 표시된다.</li>
<li>제출된 폼 데이터가 유효하지 않을 경우 <code>comment</code> 변수는 None이 된다.이 경우 댓글 폼을 표시한다.
<code>{% include %}</code> 템플릿 태그를 사용해 이전에 생성한 <code>comment_form.html</code> 템플릿을 불러온다.</li>
</ul>
<h2 id="게시글-상세-뷰에-댓글-추가하기">게시글 상세 뷰에 댓글 추가하기</h2>
<p>views.py의 <code>post_detail</code> 뷰를 편집해 보자.</p>
<pre><code class="language-python">def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, status=Post.Status.PUBLISHED, slug=post,
                             publish__year=year, publish__month=month, publish__day=day)
    comments = post.comments.filter(active=True)
    form = CommentForm()

    return render(request, &#39;blog/post/detail.html&#39;, {&#39;post&#39;: post, &#39;comments&#39;: comments, &#39;form&#39;: form})</code></pre>
<p>post_detail 뷰에 추가한 코드는 다음과 같다.</p>
<ul>
<li>게시글에 대한 모든 활성 댓글을 조회하기 위해 QuerySet을 추가했다.
<code>comments = post.comments.filter(active=True)</code></li>
<li>이 쿼리셋은 post 객체를 사용해 작성되는데, Comment 모델에 대한 쿼리셋을 직접 작성하는 대신 post 객체를 활용해 관련 Comment 객체들을 조회한다.
Comment 모델에서 Post 모델을 향한 ForeignKey 필드를 정의할 때 <code>related_name</code> 속성을 사용해 게시글과 관련된 Comment 객체들을 comments로 명명했었다.</li>
<li><code>form = CommentForm()</code> 로 댓글 폼의 인스턴스도 만들었다.</li>
</ul>
<h2 id="게시글-상세-템플릿에-댓글-추가하기">게시글 상세 템플릿에 댓글 추가하기</h2>
<p><code>blog/post/detail.html</code> 을 편집해 게시글의 총 대슬 수, 댓글 목록, 댓글 추가 폼을 추가하자.</p>
<p>게시글의 총 댓글 수 표시 기능을 먼저 추가해보자.</p>
<pre><code class="language-python">{% with comments.count as total_comments %}
    &lt;h2&gt;
        {{total_comments}} comment{{total_comments|pluralize}}
    &lt;/h2&gt;
{% endwith %}</code></pre>
<p>템플릿에서 쟝고 ORM을 사용해 <code>comments.count()</code> 쿼리셋을 실행한다.
쟝고 템플릿 언어는 메서드 호출에 괄호를 사용하지 않는다.</p>
<p>그리고 <code>{% with %}</code> 태그를 사용하면 <code>{% endwith %}</code> 태그까지 템플릿에서 사용할 수 있는 새로운 변수에 값을 할당할 수 있다. with 템플릿 태그는 DB 사용이나 비용이 많이 드는 메서드를 여러 번 호출하는 것을 방지하는데 유용하다.</p>
<p><code>total_comments</code> 값에 따라 “comments” 라는 단어의 복수형 접미사를 표시하기 위해 <code>pluralizer</code> 템플릿 필터를 사용한다. 템플릿 필터는 적용되는 변수의 값을 입력으로 사용해 계산된 값을 반환한다.
<code>pluralize</code> 템플릿 필터는 값이 1이 아닌 경우 문자 “s”가 포함된 문자열을 반환한다.
코드 내에서 텍스트는 게시글의 활성화된 댓글의 수에 따라 0 comments, 1 comment, N comments로 렌더링된다.</p>
<p>이제 게시글 상세 템플릿에 활성화된 댓글의 목록을 추가해 보자.</p>
<pre><code class="language-python">{% for comment in comments %}
    &lt;div&gt;
        &lt;p&gt;
            Comment {{forloop.counter}} by {{comment.name}}
            {{comment.created}}
        &lt;/p&gt;
        {{comment.body|linebreaks}}
    &lt;/div&gt;
{% empty %}
    &lt;p&gt;댓글이 없습니다.&lt;/p&gt;
{% endfor %}</code></pre>
<p>게시글 댓글 관련 연산을 반복하기 위해 <code>{% for %}</code> 템플릿 태그를 추가했다.
댓글 목록이 비어 있으면 사용자에게 이 게시글에 달린 댓글이 없음을 알리는 메시지를 표시한다.
각 반복시 반복 횟수를 가지는 <code>{{ forloop.counter }}</code> 변수와 함께 댓글들을 나열하는데, 각 댓글에는 댓글을 단 사용자 이름, 날짜, 댓글 본문이 표시된다.</p>
<p>마지막으로 이 템플릿에 댓글 폼을 추가하자.</p>
<pre><code class="language-python">{% include &quot;blog/post/includes/comment_form.html&quot; %}</code></pre>
<p>브라우저에 접속에 아무 게시글의 상세 페이지에 들어가보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/93e02544-3829-4ce2-97e2-32a99788eb7e/image.png" alt=""></p>
<p>위와 같이 댓글 입력 폼을 확인할 수 있다.</p>
<p>유효한 데이터로 댓글 폼을 채우고 댓글 작성하기 버튼을 클릭하면 아래와 같은 페이지가 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/971754cc-1490-49de-b64f-c0b7e3ef01e2/image.png" alt=""></p>
<p>게시글로 돌아가기 링크를 누르면 게시글 세부 정보 페이지로 리디렉션되고 다음과 같이 방금 추가한 댓글을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/967d3949-3b0f-4abf-a138-e626160a0cfa/image.png" alt=""></p>
<p>게시글에 댓글을 하나 더 추가하면 아래와 같이 게시글 내용 아래에 시간순으로 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/c79b6ea5-1b9a-4f87-946b-8b26d13a9612/image.png" alt=""></p>
<p>관리자 페이지로 이동해 댓글 하나를 편집해 ACTIVE 상태를 체크 해제해보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/a676fcfb-ecb7-48f3-b7b1-5328200b4a7c/image.png" alt=""></p>
<p>그 후 게시글 상세 뷰로 돌아오면 비활성화된 댓글이 표시되지 않고, 게시글 총 댓글 수에도 포함되지 않는 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/e29c3737-17e0-44db-9f88-18cb5a3addce/image.png" alt="">
)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 폼을 활용해 이메일 보내기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%ED%8F%BC%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%ED%8F%BC%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:21:20 GMT</pubDate>
            <description><![CDATA[<p>사용자가 이메일을 통해 게시물 추천을 보내, 다른 사라들과 블로그 게시물을 공유할 수 있도록 해보자.</p>
<ul>
<li>사용자가 자신의 이름, 이메일 주소, 수신자 이메일 주소와 필요에 따라 코멘트를 남길 수 있는 폼을 만들어야 한다.</li>
<li>게시된 데이터를 처리하고 이메일을 보내는 뷰를 생성해야 한다.</li>
<li>새로운 뷰에 대한 URL 패턴을 추가해야 한다.</li>
<li>템플릿 폼을 만들어 폼을 표시해야 한다.</li>
</ul>
<h2 id="django로-폼-만들기">Django로 폼 만들기</h2>
<p>쟝고에는 양식을 쉽게 만들 수 있는 내장 폼 프레임워크가 있다.
폼 프레임워크를 사용하면 폼의 필드를 정의하고 표시 방법을 지정하고 입력 데이터의 유효성을 검사하는 방법을 간단하게 지정할 수 있다. 쟝고 폼 프레임워크는 HTML에서 폼을 렌더링하고 데이터를 처리하는 유연한 방법을 제공한다.</p>
<p>쟝고에는  폼을 작성하기 위한 두 가지 클래스가 있다.</p>
<ul>
<li><code>Form</code><ul>
<li>필드와 유혀성 검사를 정의해서 표준 폼을 작성할 수 있다.</li>
</ul>
</li>
<li><code>ModelForm</code><ul>
<li>모델 인스턴스에 연결된 폼을 작성할 수 있다.</li>
<li>기본 Form 클래스의 모든 기능을 제공하지만, 폼 필드들은 명시적으로 선언하거나 모델 필드에서 자동으로 생성할 수 있다.</li>
<li>폼은 모델 인스턴스를 생성하거나 수정하는데 사용할 수 있다.</li>
</ul>
</li>
</ul>
<p>blog 애플리케이션의 디렉토리 내에 forms.py 파일을 만들고 다음 코드를 추가하자.</p>
<pre><code class="language-python">from django import forms

class EmailPostForm(forms.Form):
    name = forms.CharField(max_length=25)
    email = forms.EmailField()
    to = forms.EmailField(
    comments=forms.CharField(required=False, widget=forms.Textarea)
</code></pre>
<p><code>EmailPostForm</code> 폼은 기본 Form 클래스를 상속하고, 서로 다른 필드 유형을 사용해 그에 따라 데이터를 검증한다.</p>
<p>폼에는 다음 필드들이 있다.</p>
<ul>
<li><code>name</code><ul>
<li>최대 길이 25자인 <code>CharField</code> 의 인스턴스이다. 게시글을 보내는 사람의 이름으로 사용한다.</li>
</ul>
</li>
<li><code>email</code><ul>
<li><code>EmailField</code> 의 인스턴스이다. 게시글 추천을 보내는 사람의 이메일로 사용한다.</li>
</ul>
</li>
<li><code>to</code><ul>
<li><code>EmailField</code> 의 인스턴스이다. 게시글 추천 이메일을 받을 수신자의 이메일로 사용한다.</li>
</ul>
</li>
<li><code>comments</code><ul>
<li><code>CharField</code> 의 인스턴스이다. 게시글 추천 이메일에 포함할 코멘트로 사용한다.</li>
<li><code>required</code> 를 False로 설정해 이 필드를 선택 사항으로 만들고 필드를 렌더링할 커스텀 위젯을 지정했다.</li>
</ul>
</li>
</ul>
<p>각 필드 유형에는 필드가 HTML에서 렌더링되는 방식을 결정하는 기본 위젯이 있다.
<code>name</code> 필드는 <code>CharField</code> 의 인스턴스로 HTML의 <code>&lt;input type=&quot;text&quot;&gt;</code> 엘리먼트로 렌더링된다.
위젯 속성으로 기본 위젯을 재정의할 수 있다.
<code>comment</code> 필드에서 <code>Textarea</code> 위젯을 사용해 기본 <code>&lt;input&gt;</code> 엘리먼트 대신 <code>&lt;textarea&gt;</code> 엘리먼트로 표시한다.</p>
<p>필드 유효성 검사는 필드의 유형에 따라 다르다. <code>email</code> 과 <code>to</code> 필드는 <code>EmailField</code> 이므로 두 필드 모두 유효한 이메일 주소가 필요하다. 그렇지 않으면 <code>forms.ValidationError</code> 예외가 발생하고 폼이 유효하지 않게 된다. 다른 필드들도 폼 필드 유효성 검사를 수행하는데, <code>name 필드의 최대 길이는 25</code> 또는 <code>comment 필드는 선택 사항</code> 과 같은 것들이다.</p>
<h2 id="view에서-form-처리하기">View에서 Form 처리하기</h2>
<p>이메일을 통해 게시글을 추천하는 폼을 정의했으니,
이제 폼의 인스턴스를 생성하고 전송된 폼의 값들을 처리하기 위한 뷰가 필요하다.</p>
<p>views.py에 아래의 코드를 추가하자.</p>
<pre><code class="language-python">from .forms import EmailPostForm

def post_share(request, post_id):
    post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
    if request.method == &#39;POST&#39;:
        form = EmailPostForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
    else:
        form = EmailPostForm()
    return render(request, &#39;blog/post/share.html&#39;, {&#39;post&#39;: post, &#39;form&#39;: form})</code></pre>
<p><code>request</code> 객체와 <code>post_id</code> 를 매개 변수로 사용하는 <code>post_share</code> 뷰를 정의했다.
<code>get_object_or_404()</code> 함수를 사용해 id로 게시된 게시글을 조회한다.
폼을 표시하고 수신된 데이터를 처리하는데 모두 동일한 뷰를 사용한다.
HTTP 요청 메서드를 사용하면 폼의 값들이 수신된 것인지 구분할 수 있다.</p>
<p><code>GET</code> 요청은 빈 폼이 사용자에게 표시되어야 함을 나타내고, <code>POST</code> 요청은 폼의 값들이 수신되었음을 나타낸다.
<code>request.method == ‘POST’</code> 를 사용해 두 조건을 구분한다.</p>
<p>폼을 표시하고 수신된 폼 값을 처리하는 프로세스는 아래와 같다.</p>
<ol>
<li>페이지가 처음 로드되면 뷰는 <code>GET</code> 요청을 받는다. 이 경우 새로운 <code>EmailPostForm</code> 인스턴스가 생성되어 forms 변수에 저장된다. 이 폼 인스턴스틑 템플릿에 비어 있는 폼을 표시하는데 사용된다.
<code>form = EmailPostForm()</code></li>
<li>사용자가 폼을 작성하고 <code>POST</code> 를 통해 전송하면 <code>request.POST</code> 에 포함된수신 데이터를 사용해 폼 인스턴스가 생성된다.
<code>form = EmailPostForm(request.POST)</code></li>
<li>그 다음 수신된 데이터를 폼의 <code>is_valid()</code> 메서드를 사용해 유효성 검사를 수행한다.
이 메서드는 폼의 각 필드들의 데이터를 검사하고 모든 필드에 유효한 데이터가 들어있을 경우 True를 반환한다. 잘못된 데이터가 있으면 <code>is_valid()</code> 는 False를 반환한다.
유효성 검사 에러 목록은 <code>forms.errors</code> 로 얻을 수 있다.</li>
<li>폼이 유효하지 않은 경우 제출된 데이터를 가지고 템플릿에 다시 렌더링된다.
유효성 검사의 오류가 템플릿에 표시되게 된다.</li>
<li>폼이 유효하면 유효한 데이터는 <code>form.cleaned_data</code> 로 조회할 수 있다.
이 속성은 폼 필드와 해당 값을 담은 딕셔너리이다.
만약 폼 데이터가 유효하지 않다면 cleaned_data에는 유효한 필드들만 포함된다.</li>
</ol>
<p>폼을 표시하고 전송된 폼 데이터를 처리하는 뷰를 구현했으니, 이제 쟝고에서 이메일을 보내는 방법을 알아보고 그 기능을 <code>post_share</code> 뷰에 추가하자.</p>
<h2 id="쟝고로-이메일-보내기">쟝고로 이메일 보내기</h2>
<p>쟝고로 이메일을 보내려면 로컬 SMTP 서버가 있거나, 이메일 서비스 공급자와 같은 외부 SMTP 서버에 액세스해야 한다. 다음 설정을 사용하면 쟝고로 이메일을 보내는 SMTP 구성을 정의할 수 있다.</p>
<ul>
<li><code>EMAIL_HOST</code><ul>
<li>SMTP 서버 호스트, 기본 값은 localhost</li>
</ul>
</li>
<li><code>EMAIL_PORT</code><ul>
<li>SMTP 포트, 기본 값은 25</li>
</ul>
</li>
<li><code>EMAIL_HOST_USER</code><ul>
<li>SMTP 서버의 사용자 이름</li>
</ul>
</li>
<li><code>EMAIL_HOST_PASSWORD</code><ul>
<li>SMTP 서버의 패스워드</li>
</ul>
</li>
<li><code>EMAIL_USE_TLS</code><ul>
<li>TLS(Transport Layer Security) 보안 연결 사용 여부</li>
</ul>
</li>
<li><code>EMAIL_USE_SSL</code><ul>
<li>SSL 연결 사용 여부</li>
</ul>
</li>
</ul>
<p>나는 GMAIL 계정과 함께 Google의 SMTP 서버를 사용할 것이다.</p>
<p>GMAIL 계정이 있다면, settings.py 파일을 열고 아래 코드를 추가하자.</p>
<pre><code class="language-python">EMAIL_HOST = &#39;smtp.gmail.com&#39;
EMAIL_HOST_USER = &#39;gmail 주소&#39;
EMAIL_HOST_PASSWORD = &#39;gmail 앱 비밀번호&#39;
EMAIL_PORT = 587
EMAIL_USE_TLS = True</code></pre>
<p>Gmail 대신 SendGrid 또는 Amazon Simple Email Service와 같이 사용자 고유의 도메인을 사용해 SMTP를 통해 이메일을 보낼 수 있는 전문 이메일 서비스를 사용할 수도 있다.
두 서비스 모두 도메인과 보낸 사람의 이메일 계정을 검증하고 이메일을 보낼 수 있는 SMTP 자격 증명을 제공한다. 쟝고 애플리케이션 <code>django-sendgrid</code> 와 <code>django-ses</code> 는 프로젝트에 SendGrid, Amazone SES를 추가하는 작업을 단순화한다.</p>
<p>만약 SMTP 서버를 사용할 수 없다면, settings.py 파일에 아래의 설정을 추가해 쟝고가 콘솔에 이메일을 출력하도록 지시할 수 있다.</p>
<pre><code class="language-python">EMAIL_BACKEND = &#39;django.core.mail.backends.console.EmailBackend&#39;</code></pre>
<p>이 설정을 사용하면 쟝고는 이메일을 보내는 대신 콘솔에 출력한다.
이는 aSMTP 서버 없이 애플리케이션을 테스트하는데 유용하다.</p>
<p>설정이 완료됐으면 파이썬 쉘에서 아래의 코드를 통해 테스트해보자.</p>
<pre><code class="language-python">&gt;&gt;&gt; from django.core.mail import send_mail
&gt;&gt;&gt; send_mail(&#39;Django mail&#39;,
... &#39;쟝고로 보낸 이메일&#39;,
... &#39;gmail 주소@gmail.com&#39;,
... [&#39;gmail 주소@gmail.com&#39;],
... fail_silently=False)</code></pre>
<p><code>send_mail()</code> 함수는 <code>subject, message, sender, recipients</code> 목록을 필수 매개 변수로 사용한다.
선택 매개 변수 <code>fail_silently</code> 는 False로 설정해 이메일을 보낼 수 없는 경우 예외를 발생시키도록 지시한다.
표시되는 출력이 1이면 이메일이 성공적으로 전송된 것이다.</p>
<p>받은 편지함을 확인해보면 아래와 같은 메일이 온 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/628cdc8d-4109-4c17-9f38-b65793deb94a/image.png" alt=""></p>
<h2 id="뷰에서-이메일-보내기">뷰에서 이메일 보내기</h2>
<p>views.py 파일의 post_share 뷰를 편집해보자.</p>
<pre><code class="language-python">from django.core.mail import send_mail

def post_share(request, post_id):
    post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
    sent = False
    if request.method == &#39;POST&#39;:
        form = EmailPostForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            post_url = request.build_absolute_uri(post.get_absolute_url())
            subject = f&quot;{cd[&#39;name&#39;]} recommends you read {post.title}&quot;
            message = f&quot;Read {post.title} at {post_url}\n\n{cd[&#39;name&#39;]}\&#39;s comments: {cd[&#39;comments&#39;]}&quot;
            send_mail(subject, message, &#39;쥐메일주소 @gmail.com&#39;, [cd[&#39;to&#39;]])
            sent = True
    else:
        form = EmailPostForm()
    return render(request, &#39;blog/post/share.html&#39;, {&#39;post&#39;: post, &#39;form&#39;: form, &#39;sent&#39;: sent})</code></pre>
<p><code>console.EmailBackend</code> 대신 SMTP 서버를 사용하는 경우 이메일 주소를 실제 이메일 계정으로 변경해야 한다. 앞의 코드에서 초기 값이 False인 <code>sent</code> 변수를 선언했는데, 이메일이 전송된 후 이 변수를 True로 설정한다.</p>
<p>나중에 템플릿에서 보낸 변수를 사용해 폼이 성공적으로 전송되면 성공 메세지를 표시한다.</p>
<p>이메일에 게시글에 대한 링크를 포함해야 해서 <code>get_absolute_url()</code> 메서드를 사용해 게시글의 경로를 조회한다. 이 경로를 <code>request.build_absoulte_uri()</code> 의 입력으로 사용해 HTTP 스키마와 호스트 이름을 가진 완전한 URL을 만든다.</p>
<p>검증된 폼의 정리된 데이터를 사용해 이메일의 제목과 메시지 본문을 작성하고, 폼의 필드에 있는 이메일 주소로 메일을 보낸다.</p>
<p>뷰가 완료되었으니 새로운 URL 패턴을 추가해보자.
blog의 urls.py 파일을 열어 post_share URL 패턴을 추가하자.</p>
<pre><code class="language-python">path(&#39;&lt;int:post_id&gt;/share/&#39;, views.post_share, name=&#39;post_share&#39;),</code></pre>
<h2 id="템플릿에서-폼-렌더링하기">템플릿에서 폼 렌더링하기</h2>
<p>blog/template/blog/post/ 디렉터리에 새로운 파일 share.html을 만들자.</p>
<p>새로운 share.html 템플릿에 아래의 코드를 추가한다.</p>
<pre><code class="language-python">{% extends &quot;blog/base.html&quot; %}
{% block title %}Share a post{% endblock %}
{% block content %}
{% if sent %}
&lt;h1&gt;메일이 성공적으로 보내졌습니다.&lt;/h1&gt;
&lt;p&gt;
    &quot;{{ post.title }}&quot; 포스트가  {{ form.cleaned_data.to }} 에게 성공적으로 보내졌습니다.
&lt;/p&gt;
{% else %}
&lt;h1&gt;&quot;{{ post.title }}&quot; 포스트를 메일로 공유하기.&lt;/h1&gt;
&lt;form method=&quot;post&quot;&gt;
    {{ form.as_p }}
    {% csrf_token %}
    &lt;input type=&quot;submit&quot; value=&quot;메일 보내기&quot;&gt;
&lt;/form&gt;
{% endif %}
{% endblock content %}
</code></pre>
<p>이메일을 통해 게시글들을 공유하기 위한 폼을 표시하고 이메일이 전송되고 나면 성공 메시지를 표시하는 데 사용하는 템플릿이다. <code>{% if sent %}</code> 를 통해 두 가지 경우를 구분한다.</p>
<p>폼을 표시하기 위해 POST 메서드를 사용해 제출하도록 HTML form 엘리먼트를 정의했다.</p>
<pre><code class="language-python">&lt;form method=&quot;post&quot;&gt;</code></pre>
<p><code>{{ form.as_p }}</code> 로 폼 인스턴스를 포함시켰는데, <code>as_p</code> 메서드를 사용해 HTML 단락을 나타내는 <code>&lt;p&gt;</code> 엘리먼트를 사용해 폼 필드들을 렌더링하도록 한다. 또한 <code>as_ul</code> 을 사용해 정렬되지 않은 목록으로 폼을 렌더링하거나 <code>as_table</code> 을 사용해 HTML 테이블로 렌더링 할 수도 있다.</p>
<p>다른 방법으로는 아래와 같이 폼 필드를 반복해서 각 필드를 렌더링하는 방법도 있다.</p>
<pre><code class="language-python">{% for field in form %}
    &lt;div&gt;
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    &lt;/div&gt;
{% endfor %}</code></pre>
<p>템플릿 태그 <code>{% csrf_token %}</code> 를 추가했다.
이 태그는 CSRF(교차 사이트 요청 위조) 공격을 방지하기 위해 자동 생성된 토큰이 숨겨진 필드를 도입했다.
템플릿 태그 <code>{% csrf_token % }</code> 은 아래와 같이 렌더링 되는 숨겨진 필드를 생성한다.</p>
<pre><code class="language-python">&lt;input type=&#39;hidden&#39; name=&#39;csrfmiddlewaretoken&#39; value=&#39;1q2w3e4r5t6y7u8i9o0p&#39; /&gt;</code></pre>
<p>기본적으로 쟝고는 모든 POST 요청에서 CSRF 토큰을 확인한다. 따라서 POST를 통해 제출되는 모든 폼에 csrf_token 태그를 포함해야 한다.</p>
<p>게시글 상세 페이지에 공유 페이지로 가는 링크를 추가하자.</p>
<pre><code class="language-python">{{ post.body|linebreaks }}
&lt;p&gt;
    &lt;a href=&quot;{% url &#39;blog:post_share&#39; post.id %}&quot;&gt;
        메일로 공유하기
    &lt;/a&gt;
&lt;/p&gt;</code></pre>
<p><code>post_share</code> 의 URL 링크를 추가했다. URL은 쟝고에서 제공하는 템플릿 태그 <code>{% url %}</code> 을 사용해 동적으로 만들어진다.</p>
<p>이제 브라우저에서 사이트에 접속해 게시글의 상세 페이지로 이동해보자.</p>
<p>아래 사진과 같이 메일로 공유하기 링크가 생겼다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/e652015a-d548-4d71-b74f-38a58ec12d86/image.png" alt=""></p>
<p>메일로 공유하기를 클릭하면 아래와 같이 이 게시글을 이메일로 공유하기 위한 폼이 있는 페이지가 표시된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/3aeb368d-0578-4996-8160-0f4ef9a4130a/image.png" alt=""></p>
<p>필드에 유효한 데이터를 포함해 메일을 전송해보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/f940ff73-59d4-4007-9eca-93199fa21aa3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/ab0543ee-6835-4770-8d6a-3d3d776fa40a/image.png" alt=""></p>
<p>이렇게 성공했다는 화면을 확인하고, 받은 메일함에서 메일을 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/2016072d-05fb-40df-adc5-e5fa61e2a767/image.png" alt=""></p>
<p>메일이 성공적으로 도착한 것을 확인할 수 있다.</p>
<p>잘못된 데이터를 포함한 폼을 제출하게 되면 폼이 모든 유효성 검사 오류와 함께 다시 렌더링된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/9c7fa563-02c4-4788-8bc6-0f18c91c7678/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/00825bf6-771b-4299-a653-1e507a0ce983/image.png" alt=""></p>
<p>대부분의 최신 브라우저는 비어 있거나 잘못된 필드가 있는 폼을 제출하지 못하도록 한다.
이는 브라우저가 폼을 제출하기 전에 속성을 기반으로 필드의 유효성을 검사하기 때문이다.
이 경우 폼은 제출되지 않으며 브라우저는 잘못된 필드에 대한 오류 메시지를 표시한다.</p>
<p>최신 브라우저를 사용해 쟝고의 폼 유효성 검사를 테스트하려면 <form method=”post” novalidate>와 같이 <code>novalidate</code> 속성을 추가해 브라우저의 폼 유효성 검사를 건너뛸 수 있다.
이 속성을 추가해 자체적인 폼 유효성 검사를 테스트할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/7dc91de5-2fe2-4140-bf36-94d0ac4c7091/image.png" alt=""></p>
<p>테스트를 마친 후에는 novalidate 속성을 제거해 브라우저의 폼 유효성 검사를 원래대로 되돌려 놓자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 클래스 기반 뷰 만들기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%B7%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%B7%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:18:09 GMT</pubDate>
            <description><![CDATA[<p>지금까지 함수 기반 뷰를 사용해 블로그 애플리케이션을 만들었다.
함수 기반 뷰는 간단하지만 쟝고에서는 클래스를 사용해 뷰를 만들 수도 있다.
클래스 기반 뷰는 함수 대신 파이썬 객체로 뷰를 구현하는 방법이다. 뷰는 웹 요청을 받고 응답을 반환하는 함수이므로 뷰를 클래스 메서드로 정의할 수도 있다.
쟝고는 뷰를 구현하는데 사용할 수 있는 기본 뷰 클래스를 제공한다.
이들 모두 HTTP 메서드 디스패치 및 기타 공통 기능을 처리하는 View 클래스를 상속한다.</p>
<p>클래스 기반 뷰는 아래와 같은 장점을 제공한다.</p>
<ul>
<li>GET, POST, PUT 등 HTTP 메서드와 관련된 코드를 조건부 분기 대신 별도의 메서드로 구성</li>
<li>다중 상속을 사용해 재사용 가능한 뷰 클래스(믹스인) 생성</li>
</ul>
<h2 id="게시글-목록에-클래스-기반-뷰-사용하기">게시글 목록에 클래스 기반 뷰 사용하기</h2>
<p>클래스 기반 뷰를 작성하는 방법을 이해하기 위해 post_list 부와 동일한 새로운 클래스 기반 뷰를 생성해 보자.
쟝고에서 제공하는 <code>ListView</code> 뷰를 상속한 클래스를 만든다.
<code>ListView</code>를 사용하면 모든 유형의 객체들을 나열할 수 있다.</p>
<p>blog의 views.py 파일을 편집해 아래 코드를 추가해보자.</p>
<pre><code class="language-python">from django.views.generic import ListView

class PostListView(ListView):
    queryset = Post.published.all()    
    context_object_name = &#39;posts&#39;
    paginate_by = 3
    template_name = &#39;blog/post/list.html&#39;</code></pre>
<p><code>PostListView</code> 뷰는 이전에 만든 post_list와 유사하다.
<code>ListView</code> 클래스를 상속한 클래스 기반의 뷰를 구현했는데, 이 클래스 기반 뷰는 다음과 같은 속성을 가진다.</p>
<ul>
<li>모든 객체를 조회하지 않고 커스텀 쿼리셋을 사용하기 위한 <code>queryset</code> 이 있다.
queryset을 사용하지 않고 <code>model = Post</code> 를 사용해 모델을 지정하면 쟝고가 일반적인
<code>Post.objects.all()</code> 쿼리셋을 만든다.</li>
<li>쿼리 결과를 위한 context 변수 posts를 사용한다. <code>context_object_name</code> 을 지정하지 않을 경우 기본 변수는 <code>object_list</code> 이다.</li>
<li>페이지당 3개의 객체를 반환하도록 <code>paginated_by</code> 로 페이징을 정의한다.</li>
<li><code>template_name</code> 을 설정해 커스텀 템플릿을 사용한다.
기본 템플릿을 설정하지 않으면 ListView는 기본적으로 <code>blog/post_list.html</code> 을 사용한다.</li>
</ul>
<p>이제 아래와 같이 urls.py 파일을 편집해 이전의 post_list URL 패턴을 주석 처리하고, PostListView 클래스를 사용해 새로운 URL 패턴을 추가해보자.</p>
<pre><code class="language-python">path(&#39;&#39;, views.PostListView.as_view(), name=&#39;post_list&#39;),</code></pre>
<p>페이징 작업르 계속하려면 템플릿에 전달되는 올바른 페이지 객체를 사용해야 한다.
쟝고의 ListView는 <code>page_obj</code> 라는 변수에 요청된 페이지를 전달한다.
아래와 같이 올바른 변수를 사용해 paginator를 include 하도록 post/list.html을 편집해야 한다.</p>
<pre><code class="language-python">{% include &quot;pagination.html&quot; with page=page_obj %}</code></pre>
<p><code>HTTP 404</code> 상태 코드를 반환하는 예외 처리는 ListView에서 제공된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/6024d2e8-137a-4177-81c7-b6a9833ae072/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 페이징 추가하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:17:28 GMT</pubDate>
            <description><![CDATA[<p>쟝고에는 페이징 데이터를 쉽게 관리할 수 있는 페이징을 휘한 클래스가 내장되어 있다.
페이지당 반환할 객체 수를 정할 수 있고, 사용자가 요청한 페이지에 해당하는 게시글들을 조회할 수 있다.</p>
<h2 id="게시글-목록-뷰에-페이징-추가하기">게시글 목록 뷰에 페이징 추가하기</h2>
<p>blog의  views.py 파일을 편집해 <code>Paginator</code> 클래스를 불러오고 아래와 같이 수정해 보자.</p>
<pre><code class="language-python">def post_list(request):
    post_list = Post.published.all()

    paginator = Paginator(post_list, 3)
    page_number = request.GET.get(&#39;page&#39;, 1)
    posts = paginator.page(page_number)

    return render(request, &#39;blog/post/list.html&#39;, {&#39;posts&#39;: posts})</code></pre>
<ul>
<li><code>paginator = Paginator(post_list, 3)</code><ul>
<li>페이지당 반환할 객체 수와 함께 Paginator 클래스를 인스턴스화한다.</li>
<li>페이지당 3개의 게시글을 표시한다.</li>
</ul>
</li>
<li><code>page_number = request.GET.get(&#39;page&#39;, 1)</code><ul>
<li>HTTP GET에서 page 파라미터를 조회해 그 값을 page_number 매개 변수에 저장하는데,
page 매개 변수가 파라미터에 없을 경우 기본 값으로 1을 사용해 첫 페이지를 로드하게 했다.</li>
</ul>
</li>
<li><code>posts = paginator.page(page_number)</code><ul>
<li>Paginator의 page() 메서드를 호출해 원하는 페이지의 객체를 얻는다.</li>
<li>이 메서드는 posts 변수에 담을 Page 객체를 반환한다.</li>
</ul>
</li>
</ul>
<h2 id="페이징-템플릿-만들기">페이징 템플릿 만들기</h2>
<p>사용자가 다른 페이지들을 조회할 수  있도록 페이지 네비게이션을 만들어야 한다.
페이징 링크를 표시하는 템플릿을 생성하고, 웹사이트에서 다른 객체들의 페이징에서도 템플릿을 재사용할 수 있도록 일반화해 보자.</p>
<p>templates/ 디렉토리에 pagination.html 파일을 만들고 코드를 작성해보자.</p>
<pre><code class="language-html">&lt;div class=&quot;pagination&quot;&gt;
    &lt;span class=&quot;step-links&quot;&gt;
        {% if page.has_previous%}
        &lt;a href=&quot;?page={{page.previous_page_number}}&quot;&gt; Previous&lt;/a&gt;
        {% endif %}
        &lt;span class=&quot;current&quot;&gt;
            Page {{page.number}} of {{page.paginator.num_pages}}
        &lt;/span&gt;
        {% if page.has_next%}
        &lt;a href=&quot;?page={{page.next_page_number}}&quot;&gt; Next&lt;/a&gt;
        {% endif %}
    &lt;/span&gt;
&lt;/div&gt;</code></pre>
<p>이 템플릿은 페이징을 일반화한 템플릿으로,
템플릿은 이전과 다음 링크를 렌더링하고 현재 페이지와 결과의 총페이지를 표시하기 위한 Page 객체를 context에 가지고 있어야 한다.</p>
<p>아래와 같이 blog/post/list.html 템플릿으로 돌아가 content 블럭 하단에 pagenation.html 템플릿을 포함시키자.</p>
<pre><code class="language-html">{% include &quot;pagination.html&quot; with page=posts %}</code></pre>
<p><code>include</code> 템플릿 태그는 지정된 템플릿을 불러와 현재 템플릿 컨텍스트를 사용해 렌더링한다.
그리고 추가 context 변수를 전달하기 위해 with을 사용해 posts 객체를 넘겨 준다.</p>
<p>이제 브라우저에서 <a href="http://localhost:8000/blog/"><code>http://localhost:8000/blog</code></a> 경로를 확인하면 게시글 목록 하단에 내비게이션 링크가 표시되게 된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/a749f3ba-8139-4b93-934b-a73593afd8d1/image.png" alt=""></p>
<p>Next를 클릭하면 마지막 게시글이 표시되는데, 두 번째 page의 URL에는 ?page=2 처럼 GET 매개 변수가 포함되어 있다. 이 매개변수는 뷰에서 paginator를 통해 요청된 결과 페이지를 가져오는 데 사용된다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/3b358411-925b-4324-8b25-64aa4719dd65/image.png" alt=""></p>
<h2 id="페이징-오류-처리하기">페이징 오류 처리하기</h2>
<p>이제 뷰에서 페이징 오류에 대한 예외 처리를 추가해 보자.
page 파라미터는 지정된 페이지를 검색하기 위해 뷰에서 사용되는데, 존재하지 않는 페이지 번호라 페이지 번호로 쓸 수 없는 문자열과 같이 잘못된 값으로 인해 오류가 발생할 수 있다.
이런 경우 적절한 오류 처리를 구현해야 한다.</p>
<p>브라우저에 <a href="http://localhost:8000/blog/?page=9999%EB%A5%BC">http://localhost:8000/blog/?page=9999를</a> 열어보면 아래와 같은 오류 페이지가 표시될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/290d8df6-953b-41af-9cf0-8bfb8582ddea/image.png" alt=""></p>
<p>Paginator 객체는 9999페이지를 조회할 때 범위를 벗어날 때 <code>EmptyPage</code> 에러를 발생시킨다.
뷰에서 이 오류를 처리해 보자.</p>
<p>views.py 파일을 열어 <code>EmptyPage</code>를 import하고 post_list 뷰를 아래와 같이 수정해보자.</p>
<pre><code class="language-python">from django.core.paginator import Paginator, EmptyPage

def post_list(request):
    post_list = Post.published.all()

    paginator = Paginator(post_list, 3)
    page_number = request.GET.get(&#39;page&#39;, 1)
    try:
        posts = paginator.page(page_number)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request, &#39;blog/post/list.html&#39;, {&#39;posts&#39;: posts})</code></pre>
<p>페이지를 검색할 때 예외를 처리하기 위해 <code>try, except</code> 블록을 추가했다.
요청한 페이지가 범위를 벗어나면 <code>paginator.num_pages</code>로 총 페이지수를 얻어 마지막 페이지를 반환한다.
총 페이지 수는 마지막 페이지 번호와 동일하다.
이제 뷰에서 다시 <a href="http://localhost:8000/blog/?page=9999%EC%97%90">http://localhost:8000/blog/?page=9999에</a> 접속할 경우 예외를 처리해 페이지네이션 결과의 마지막 페이지를 반환한다.</p>
<p>page 매개 변수에 정수가 아닌 다른 것이 전달되는 경우에도 뷰가 처리해야 한다.
브라우저에서 <a href="http://localhost:8000/blog/?page=qwer">http://localhost:8000/blog/?page=qwer</a> 를 접속해 보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/7bf5acfe-508a-48f6-b0f8-8d0bf5576d8e/image.png" alt=""></p>
<p>이 경우 페이지 번호는 정수만 될 수 있기 때문에 <code>PageNotAnInteger</code> 예외가 발생하게 된다.
뷰에서 이 오류를 처리해 보자.</p>
<p>views.py 파일을 편집해 <code>PageNotAnInteger</code> 를 import하고 아래와 같이 post_list 뷰를 수정하자.</p>
<pre><code class="language-python">from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def post_list(request):
    post_list = Post.published.all()

    paginator = Paginator(post_list, 3)
    page_number = request.GET.get(&#39;page&#39;, 1)
    try:
        posts = paginator.page(page_number)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request, &#39;blog/post/list.html&#39;, {&#39;posts&#39;: posts})</code></pre>
<p>페이지를 조회할 때 <code>PageNotAnInteger</code> 에러를 처리하기 위해 새로운 except 블록을 추가했다.
요청한 페이지가 정수가 아닌 경우 결과의 첫 번째 페이지를 반환한다.</p>
<p>다시 <a href="http://localhost:8000/blog/?page=qwer">http://localhost:8000/blog/?page=qwer</a> 페이지를 방문하면,
뷰에서 예외를 처리해 결과의 첫 번째 페이지가 반환되게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SEO에 부합하는 게시글 URL 만들기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/SEO%EC%97%90-%EB%B6%80%ED%95%A9%ED%95%98%EB%8A%94-%EA%B2%8C%EC%8B%9C%EA%B8%80-URL-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/SEO%EC%97%90-%EB%B6%80%ED%95%A9%ED%95%98%EB%8A%94-%EA%B2%8C%EC%8B%9C%EA%B8%80-URL-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:15:46 GMT</pubDate>
            <description><![CDATA[<p>블로그 게시글 상세 페이지의 표준 URL을 /blog/1/ 처럼 사용하도록 만들었다.
SEO 친화적인 게시글 URL을 만들기 위해 URL 패턴을 변경해보자.
publish 날짜와 slug 값을 모두 사용해 단일 게시글에 대한 URL을 만들자.
날짜를 조합해 /blog/2025/1/test-post1/ 과 같은 게시글의 상세 페이지 URL을 만들 것이다.
이렇게 하면 게시글의 제목과 날짜를 모두 포함해 검섹 엔진에 색인을 생성할 수 있는 SEO 친화적인 URL을 제공하게 된다.</p>
<p>단일 게시글을 게시 날짜와 slug의 조합으로 조회하려면 기존 게시글과 동일한 slug와 publish 날짜를 가진 게시글이 존재하지 않도록 해야 한다.
게시글의 게시 날짜를 기준으로 슬러그를 고유하게 정의해 Post 모델이 중복 게시글을 저장하지 않도록 하자.</p>
<p>models.py 파일을 편집해 <code>unique_for_date</code> 매개 변수를 Post 모델의 slug 필드에 추가해보자.</p>
<pre><code class="language-python">slug = models.SlugField(max_length=250, unique_for_date=&#39;publish&#39;)</code></pre>
<p><code>unique_for_date</code>를 사용하면 이제 slug 필드가 게시 필드에 지정된 날짜의 중복을 허용하지 않게 된다.
publish 필드는 DateTimeField의 인스턴스지만 고유 값의 확인은 날짜(시간x)에만 수행된다.
쟝고는 주어진 게시 날짜에 새로운 게시글이 기존 게시글과 동일한 슬러그를 사용해 저장되는 것을 방지학 ㅔ된다.</p>
<p>모델을 변경했으니 마이그레이션을 만들어 보자.
unique_for_date는 DB 수준에서 적용되지 않으므로 DB 마이그레이션이 필요하지 않지만,
쟝고는 마이그레이션을 사용해 모든 모델 변경사항을 추적한다.
마이그레이션을 모델의 현재 상태와 일치시키기 위해 마이그레이션을 생성해보자.</p>
<pre><code class="language-bash">python manage.py makemigrations blog

Migrations for &#39;blog&#39;:
  blog/migrations/0002_alter_post_slug.py
    - Alter field slug on post</code></pre>
<p>쟝고는 블로그 애플리케이션의 마이그레이션 디렉터리 내에 0002_alter_post_slug.py 파일을 생성했다.
이제 마이그레이션을 적용해보자.</p>
<pre><code class="language-bash">python manage.py migrate   

Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0002_alter_post_slug... OK</code></pre>
<p>이렇게 하면 쟝고는 모든 마이그레이션이 적용되고 모델과 동기화된 것으로 간주한다.
<code>unique_for_date</code> 는 DB 수준에서 적용이 강제되지 않아 DB에서 수행되는 작업은 없다.</p>
<h2 id="url-패턴-수정하기">URL 패턴 수정하기</h2>
<p>게시글 상세 URL에 게시 날짜와 슬러그를 사용하도록 URL 패턴을 수정해 보자.
blog의 urls.py 파일의 게시글 상세 URL을 편집해 보자</p>
<pre><code class="language-python">path(&#39;&lt;int:year&gt;/&lt;int:month&gt;/&lt;int:day&gt;/&lt;slug:post&gt;/&#39;,
         views.post_detail, name=&#39;post_detail&#39;),</code></pre>
<p>post_detail 뷰의 URL 패턴은 다음 인수를 사용하게 된다.</p>
<ul>
<li>year</li>
<li>month</li>
<li>day</li>
<li>post</li>
</ul>
<h2 id="뷰-수정하기">뷰 수정하기</h2>
<p>이제 새로운 URL 매개 변수와 일치하도록 post_detail 뷰의 매개 변수를 변경하고 이를 사용해 해당하는 Post 객체를 조회해야 한다.</p>
<p>views.py 파일을 열어 아래와 같이 post_detail 뷰를 편집해 보자.</p>
<pre><code class="language-python">post = get_object_or_404(Post, status=Post.Status.PUBLISHED, slug=post,
                             publish__year=year, publish__month=month, publish__day=day)</code></pre>
<p>year, month, day와 post 인수를 받고 지정된 슬러그 및 게시 날짜로 게시된 게시글을 검색하도록 post_detail 뷰를 수정했다. 앞서 Post 모델의 slug 필드에 <code>unique_for_date = &#39;publish&#39;</code> 를 추가해 주어진 날짜에 해당 슬러그가 포함된 게시글이 하나만 있도록 했기에 단일 게시글을 조회할 수 있다.</p>
<h2 id="게시글의-표준-url-수정하기">게시글의 표준 URL 수정하기</h2>
<p>이제 새로운 URL 매개 변수와 일치하도록 블로그 게시글의 표준 URL 매개 변수를 수정해보자.
blog 애플리케이션의 models.py 파일을 열어 <code>get_absolute_url()</code> 메서드를 편집하자.</p>
<pre><code class="language-python">def get_absolute_url(self):
        return reverse(&quot;blog:post_detail&quot;, args=[self.publish.year, self.publish.month, self.publish.day, self.slug])</code></pre>
<p>그 다음 브라우저로 돌아가 게시글들 중 하나의 제목을 클릭해 게시글 상세 뷰를 볼 수 있다.
브라우저의 주소를 살펴보면
<code>http://localhost:8000/blog/2025/1/22/test3/</code> 와 같이 SEO 친화적인 블로그 게시글을 갖게 된 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 모델에 표준 URL 사용하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%EB%AA%A8%EB%8D%B8%EC%97%90-%ED%91%9C%EC%A4%80-URL-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%EB%AA%A8%EB%8D%B8%EC%97%90-%ED%91%9C%EC%A4%80-URL-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:14:01 GMT</pubDate>
            <description><![CDATA[<p>웹사이트에는 동일한 컨텐츠를 표시하는 다른 페이지가 있을 수 있다.</p>
<p>현재까지 만든 블로그에선 각 게시물의 일부 컨텐츠 정보는 게시글 목록 페이지와 게시글 상세 페이지 둘 다에서 표시된다.</p>
<p>표준 URL은 리소스에 대한 기본 URL로 특정 컨텐츠의 가장 대표적인 페이지 URL이라 생각하면 된다.</p>
<p>사이트에서 게시글을 표시하는 서로 다른 페이지가 존재할 수 있지만, 게시글의 기본 URL로 사용하는 URL은 하나이다.</p>
<p>표준 URL을 사용할 경우 페이지의 마스터 사본에 대한 URL을 지정할 수 있다.</p>
<p>쟝고의 경우 get_absolute_url() 메서드를 구현해 객체의 표준 URL을 반환할 수 있다.</p>
<p>애플리케이션의 URL 패턴에 정의된 post_detail URL을 사용해 Post 객체에 대한 표준 URL을 만들어 보자.</p>
<p>쟝고는 URL 이름과 필요한 매개 변수를 사용해 동적으로 URL을 작성할 수 있는 URL 해석 기능을 제공한다.</p>
<p>우리는 django.urls 모듈의 reverse() 함수를 사용할 것이다.</p>
<p>아래와 같이 blog 애플리케이션의 <a href="http://models.py">models.py</a> 파일을 편집해 <code>reverse()</code> 함수를 import하고 <code>get_absolute_url()</code> 메서드를 Post 모델에 추가한다.</p>
<pre><code class="language-python">from django.urls import reverse

class Post(models.Model):

    # ...

    def get_absolute_url(self):
        return reverse(&quot;blog:post_detail&quot;, args=[self.id])</code></pre>
<p><code>reverse()</code> 함수는 URL 패턴에 정의된 URL 이름을 사용해 URL을 동적으로 만든다.</p>
<p>blog 네임스페이스 뒤에 콜론과 URL 이름 post_details을 사용했다.</p>
<p>이 URL에는 조회할 블로그 게시글의 id인 필수 매개 변수가 있다.</p>
<p><code>args=[self.id]</code> 를 사용해서 Post 객체의 id를 위치 인자로 포함했다.</p>
<p>게시글 상세 템플릿의 URL을 get_absolute_url() 메서드로 교체해 보자.</p>
<p>blog/post/list.html 파일을 편집해 다음 줄을 변경해보자.</p>
<pre><code class="language-python">&lt;a href=&quot;{% url &#39;blog:post_detail&#39; post.id %}&quot;&gt;</code></pre>
<pre><code class="language-python">&lt;a href=&quot;{{post.get_absolute_url}}&quot;&gt;</code></pre>
<p>이제 템플릿에서 post.get_absolute_url을 호출하면 게시글의 표준 URL을 얻을 수 있다. 이렇게 표준 URL을 사용하면 코드의 가독성이 향상되고 URL 패턴이 변경되더라도 템플릿을 수정할 필요가 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django의 요청/응답 주기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django%EC%9D%98-%EC%9A%94%EC%B2%AD%EC%9D%91%EB%8B%B5-%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django%EC%9D%98-%EC%9A%94%EC%B2%AD%EC%9D%91%EB%8B%B5-%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:12:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/7c1ba534-9ed7-49a8-8893-56badbca636f/image.png" alt=""></p>
<p>이미지 출처 : <a href="https://www.technoarchsoftwares.com/blog/django-request-response-cycle/">https://www.technoarchsoftwares.com/blog/django-request-response-cycle/</a></p>
<ol>
<li>웹 브라우저는 URL(<a href="http://localhost:8000/blog/3/)%EB%A1%9C">http://localhost:8000/blog/3/)로</a> 페이지를 요청한다.
웹 서버가 HTTP 요청을 수신해 쟝고에 전달한다.</li>
<li>쟝고는 URL 패턴의 구성에 정의된 각 URL 패턴을 검사하고, 프레임워크는 주어진 URL 경로에 대해 각 패턴을 나타나는 순서대로 확인한고 요청된 URL과 매칭되는 첫번째 패턴에서 검사를 멈춘다.
/blog/<id>/ 패턴이 /blog/33/ 경로와 매칭되게 된다.</li>
<li>쟝고는 일치하는 URL 패턴의 뷰를 가져와 HttpRequest 클래스의 인스턴스와 키워드 또는 위치 인수를 전달해 실행한다.
뷰는 모델을 사용해 DB에서 정보를 조회하는데, 쟝고 ORM QuerySet을 사용하면 SQL로 변환되어 DB에서 실행된다.</li>
<li>뷰는 render() 함수를 사용해 context 변수로 Post를 전달해 HTML 템플릿을 렌더링한다.</li>
<li>렌더링된 내용은 컨텐트 타입이 기본적으로 text/html인 뷰에 의해 HttpResponse 객체로 반환된다.</li>
</ol>
<p>여기서는 설명의 단순화를 위해 미들웨어는 포함시키지 않았지만 추후에 미들웨어를 공부하며 추가할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 뷰에 맞는 템플릿 생성하기]]></title>
            <link>https://velog.io/@i-am-not-kangjik/Django-%EB%B7%B0%EC%97%90-%EB%A7%9E%EB%8A%94-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@i-am-not-kangjik/Django-%EB%B7%B0%EC%97%90-%EB%A7%9E%EB%8A%94-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 23 Jan 2025 23:11:42 GMT</pubDate>
            <description><![CDATA[<p>blog 애플리케이션에 대한 뷰와 URL 패턴을 작성했다.</p>
<p>URL 패턴은 URL을 뷰에 매핑하고 뷰는 사용자에게 반환되는 데이터를 결정한다.</p>
<p>템플릿은 데이터가 표시되는 방식을 정의하는데, 일반적으로 쟝고 템플릿 언어와 함께 HTML로 작성된다.</p>
<p>애플리케이션에 템플릿을 추가해보자.</p>
<p>blog 애플리케이션 디렉토리 안에 다음 디렉토리와 파일을 생성한다.</p>
<pre><code>templates/
    blog/
        base.html
        post/
            list.html
            detail.html</code></pre><p>base.html은 웹사이트의 기본 HTML과 구조를 가지고 컨텐츠를 기본 컨텐츠 영역과 사이드 바로 나눈다.</p>
<p>list.html과 detail.html은 블로그 게시글 목록 및 세부 정보 보기를 렌더링하기 위해 base.html에서 상속된다.</p>
<p>쟝고에는 데이터 표시 방법을 지정할 강력한 템플릿 언어가 있다.</p>
<p>템플릿 태그, 템플릿 변수, 템플릿 필터를 기반으로 한다.</p>
<ul>
<li>템플릿 태그는 템플릿의 렌더링을 제어하며 <code>{% 태그 %}</code> 처럼 쓴다.</li>
<li>템플릿 변수는 템플릿이 렌더링될 때 값으로 대체되며 <code>{{ 변수 }}</code> 처럼 쓴다.</li>
<li>템플릿 필터를 사용하면 표시할 변수를 수정하고 <code>{{ 변수|필터 }}</code> 와 같이 표시할 수 있다.</li>
</ul>
<h2 id="기본-템플릿-만들기">기본 템플릿 만들기</h2>
<p>base.html을 수정해 기본 템플릿을 만들어 보자.</p>
<pre><code class="language-html">{% load static %}
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;{% block title %}{% endblock %}&lt;/title&gt;
    &lt;link href=&quot;{% static &#39;css/blog.css&#39; %}&quot; rel=&quot;stylesheet&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;content&quot;&gt;{% block content %} {% endblock %}&lt;/div&gt;
    &lt;div id=&quot;sidebar&quot;&gt;
      &lt;h2&gt;My blog&lt;/h2&gt;
      &lt;p&gt;This is my blog.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p><code>{% load static %}</code> 은 INSTALLED_APPS 설정에 포함된 django.contrib.staticfiles 애플리케이션에서 제공하는 정적 템플릿 태그를 로드하도록 한다.
이것을 로드한 후에는 이 템플릿 전체에서 <code>{% static %}</code> 템플릿 테그를 사용할 수 있다.</p>
<p>위 코드에는 두 개의 <code>{% block %}</code> 태그가 있는 것을 볼 수 있는데,
이들은 쟝고에 해당 영역에 블록을 정의한다고 알려준다.</p>
<p>이 템플릿에서 상속된 템플릿은 컨텐츠로 블록을 채울 수 있다.
코드에서는 title이라는 블록과 content라는 블록을 정의했다.</p>
<h2 id="게시물-목록-템플릿-만들기">게시물 목록 템플릿 만들기</h2>
<p>post/list.html 파일을 편집해 아래와 같이 만들자.</p>
<pre><code class="language-html">{% extends &quot;blog/base.html&quot;%}
{% block title %} My blog {% endblock %}
{% block content %}
&lt;h1&gt;My Blog&lt;/h1&gt;
{% for post in posts %}
&lt;h2&gt;
    &lt;a href=&quot;{% url &#39;blog:post_detail&#39; post.id %}&quot;&gt;
        {{post.title}}
    &lt;/a&gt;
&lt;/h2&gt;
&lt;p class=&quot;date&quot;&gt;
    Published {{ post.publish }} by {{ post.author }}
&lt;/p&gt;
{{ post.body|truncatewords:30|linebreaks}}
{% endfor %}
{% endblock %}
</code></pre>
<p><code>{% extends %}</code> 템플릿 태그를 사용하면 blog/base.html 템플릿을 상속받도록 할 수 있다.</p>
<p>그 다음 base.html의 title과 content 블록을 이 컨텐츠로 채운다.</p>
<p>게시물들을 순회하며 게시글들의 상세 페이지로 이동하기 위한 URL 링크를 포함한 정보들을 표시한다.</p>
<p>링크의 URL은 쟝고에서 제공하는 <code>{% url %}</code> 템플릿 태그를 사용해 구성했다.</p>
<p>이 템플릿 테그를 사용하면 URL을 이름을 가지고 동적으로 구성할 수 있다.</p>
<p>blog 네임스페이스에서 post_detail의 URL을 참조하기 위해서는 <code>blog:post_detail</code>과 같이 사용하면 된다.                                                                                                                                            </p>
<p>그리고 필수 매개변수인 post.id를 전달해 해당 게시물의 URL을 작성한다.</p>
<p>게시물 body에 두 가지의 템플릿 필터를 적용했는데,</p>
<p><code>truncatewords</code>는 값을 지정된 단어 수로 자르고 <code>linebreaks</code>는 출력을 HTML 줄 바꿈으로 변환한다.</p>
<p>이렇게 중첩해 템플릿 필터를 연결할 수 있는데, 각 템플릿 필터는 이전 필터에서 생성된 출력에 적용된다.</p>
<h2 id="게시글-목록-템플릿-확인하기">게시글 목록 템플릿 확인하기</h2>
<p>이제 <a href="http://localhost:8000/blog/">http://localhost:8000/blog/</a> 에 접속해보자.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/141376ed-af1a-4476-a3d7-bc38f377ae8d/image.png" alt=""></p>
<p>사진과 같이 Published인 게시글들이 목록에 표시되게 된다.</p>
<h2 id="게시글-상세-템플릿-만들기">게시글 상세 템플릿 만들기</h2>
<p>다음으로 post/detail.html을 작성해 게시글 상세 템플릿을 만들어 보자.</p>
<pre><code class="language-python">{% extends &quot;blog/base.html&quot; %} {% block title %}{{post.title}}{% endblock %}
{% block content %}
&lt;h1&gt;{{post.title}}&lt;/h1&gt;
&lt;p class=&quot;date&quot;&gt;
    Published {{post.publish}} by {{post.author}}
&lt;/p&gt;
{{post.body|linebreaks}}
{% endblock %}
</code></pre>
<p>작성한 후 목록의 게시글 제목 중 하나를 클릭하면 게시글의 상세 정보를 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/i-am-not-kangjik/post/faba2c02-65e8-431d-9776-a2f8d92aa21a/image.png" alt=""></p>
<p>URL을 유심히 살펴보면 <a href="http://localhost:8000/blog/3/">localhost:8000/blog/3/</a> 과 같이 자동으로 생성된 게시글 ID가 있다.</p>
]]></description>
        </item>
    </channel>
</rss>