<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>veggie.log</title>
        <link>https://velog.io/</link>
        <description>Strong belief in connecting the dots. 찬찬히 배우고 있는 학생 개발자입니다.</description>
        <lastBuildDate>Sun, 23 May 2021 07:47:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>veggie.log</title>
            <url>https://images.velog.io/images/infl_veggie/profile/14eefcf8-b54f-4183-8a17-65bd0aaed6e6/profile.img.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. veggie.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/infl_veggie" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AWS t4g 인스턴스 사용기 (with 도커)]]></title>
            <link>https://velog.io/@infl_veggie/AWS-t4g-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%82%AC%EC%9A%A9%EA%B8%B0-with-%EB%8F%84%EC%BB%A4</link>
            <guid>https://velog.io/@infl_veggie/AWS-t4g-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%82%AC%EC%9A%A9%EA%B8%B0-with-%EB%8F%84%EC%BB%A4</guid>
            <pubDate>Sun, 23 May 2021 07:47:11 GMT</pubDate>
            <description><![CDATA[<h2 id="도입">도입</h2>
<p>스타트업에서 어떤 서버를 구축할지 결정하는 일은 매우 중요합니다. 제한된 비용으로 더 많은 유저를 수용하기 위해 온갖 노력이 들어가곤 하는데, 애초에 같은 돈으로 더 좋은 컴퓨터를 대여할 수 있다면 얼마나 좋을까요?</p>
<p><a href="https://aws.amazon.com/ko/blogs/korea/amazon-ec2-t4g-instance-seoul-region/">Amazon EC2 T4g 인스턴스, 서울 리전 출시</a></p>
<p>지난 2월, 서울 리전에 t4g 인스턴스가 추가됐습니다. EC2의 Arm 기반 t4g 인스턴스는 서버 컴퓨터로 흔히 사용되는 동급 t3 모델보다 40%나 나은 성능을 자랑한다고 합니다. 아래 글에 따르면, 동등한 조건에서 t3 모델보다 3배 이상 나은 CPU 성능을 보였지만 오히려 20% 더 저렴했습니다.</p>
<p><a href="https://www.learnaws.org/2020/12/19/t3-t3a-t4g/">AWS EC2 Instance Comparison: T3 vs T3a vs T4g(영문)</a></p>
<p>하지만 관심이 생겨 검색을 해봐도 아직 참고할 만한 사례를 찾기 어려웠습니다. 가성비와 편의성 사이에서 고민한 끝에, 일단 도입해 써보기로 결심했습니다.</p>
<h2 id="가성비가-좋은-이유가-있어">가성비가 좋은 이유가 있어..</h2>
<p>t4g 인스턴스의 가장 큰 특징은, 로컬 컴퓨터에서 일반적으로 쓰이는 Amd64(x86-64) 아키텍처가 아닌 arm64 기반 프로세서를 사용한다는 사실입니다. 그 덕에 동일 비용으로 훨씬 뛰어난 성능을 발휘할 수 있지만, 여러 호환성 문제를 겪을 수도 있습니다. </p>
<p>사실 겉으로 보기엔 기존에 쓰던 인스턴스들과 큰 차이를 느낄 수는 없었는데요. 문제는 도커를 설치하려 할때부터 시작됐습니다.</p>
<h3 id="도커-설치하기">도커 설치하기</h3>
<p>리눅스 컴퓨터에 도커를 설치하실 때 보통 다음의 <a href="https://docs.docker.com/engine/install/ubuntu/">도커 설치 공식문서</a>를 참조하실 텐데요. 도커 설치를 위한 셋업 단계 마지막에, amd64용 커맨드를 입력하지 않도록 주의하셔야 합니다.
<img src="https://images.velog.io/images/infl_veggie/post/4b40b804-6cc4-467a-b0f0-b1d0faf5b9df/image.png" alt="">
저장소 세팅 시에 위의 arm64용 커맨드를 입력하셔야 합니다. 혹시라도 착각해 x86_64용 커맨드를 입력하신 경우 빠르게 인스턴스를 초기화하거나 재대여하시길 추천드립니다. 해당 커맨드를 되돌리기는 까다롭고, 그렇다고 다시 arm64 커맨드를 입력하면 에러가 발생하기 때문입니다.</p>
<h3 id="도커-컴포즈-설치문제">도커 컴포즈 설치문제</h3>
<p>특정 버전의 우분투에서 도커 컴포즈를 설치하는 경우 설치가 안되는 문제가 발생할 수 있습니다. 해당 문제를 겪으신다면 다음 이슈에서 해결법을 찾을 수 있습니다.
<a href="https://github.com/docker/compose/issues/6831">Release docker/compose image for armv7 / arm64v8 #6831
</a>
도커 컴포즈 이미지를 활용하라는 답변도 있지만 오버헤드가 있으므로 추천드리진 않습니다. <code>nemchik</code>님의 답변이 큰 도움이 됐고, 이 방식으로 해결했을때 테스트 단계에서 도커 컴포즈를 이용하는데 어떠한 문제도 발생하지 않았습니다.
<img src="https://images.velog.io/images/infl_veggie/post/d96123f9-965f-442e-b341-e1fbd58ac32b/image.png" alt=""></p>
<h3 id="도커-이미지-빌드-및-실행-문제">도커 이미지 빌드 및 실행 문제</h3>
<p>설치를 무사히 마치셨다면, 기존에 사용하던 이미지를 레지스트리에서 풀받아 실행하는 순간 아래와 같은 에러를 마주하게 됩니다.
<img src="https://images.velog.io/images/infl_veggie/post/95fd85d1-e8eb-4aa7-b809-fb9bdf63d993/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-05-23%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.14.18.png" alt="exec user process caused: exec format error">
<code>exec user process caused: exec format error</code></p>
<p>보통의 개발 컴퓨터는 amd64 아키텍처를 사용하므로, 기본 세팅으로 도커 이미지를 빌드하면 amd64에서만 동작하는 이미지가 빌드됩니다. 이를 arm 기반 t4g 인스턴스에서 실행하려하면 위와 같은 에러가 발생합니다. 따라서 이미지 빌드시에 arm64용으로 빌드해줄 필요가 있습니다.</p>
<p>다행히도 도커에서는 이와 같은 다중 플랫폼에 대응하고자 <code>docker buildx</code>를 제공합니다. 최신 버전의 경우 도커에 이미 내장돼있는 기능으로, 다양한 아키텍처에서 작동하는 도커 이미지를 빌드해줍니다. 사용법에 대해선 아래 링크들을 참조하시면 좋습니다.</p>
<p><a href="https://meetup.toast.com/posts/255">Docker Buildx로 Multi-Archtecture Image 빌드하기
</a>
<a href="https://judo0179.tistory.com/99">Docker buildx를 활용하여 Multi-Architecture 이미지 빌드
</a>
<a href="https://docs.docker.com/buildx/working-with-buildx/">Docker Buildx 공식문서</a></p>
<p>제 경우, 인텔 프로세서 맥북에서 도커 이미지를 빌드해 arm64 t4g 인스턴스에서 이미지를 실행하고 있습니다. 이 경우 제가 사용하는 커맨드는 다음과 같습니다(위 링크대로 기본 세팅을 하셔야 합니다).</p>
<pre><code class="language-bash">docker buildx build --platform linux/arm64 -t [image name]:$VER -f [Dockerfile 경로] --load . 
docker push [image name]:$VER</code></pre>
<p>보이듯이 멀티 아키텍처 빌드를 도커에서 아주 잘 지원해주고 있어서 사용하는 입장에서는 거의 차이가 없고, 빌드 커맨드만 다릅니다. 다만 같은 이유로 이 이미지를 로컬 컴퓨터에서 실행할 수는 없습니다. 또한 <code>docker buildx</code>에서 <code>--load</code>를 <code>--push</code>로 바꾸면 빌드한 이미지를 바로 레지스트리로 푸시합니다.</p>
<h3 id="arm을-지원하지-않는-퍼블릭-도커-이미지들">Arm을 지원하지 않는 퍼블릭 도커 이미지들</h3>
<p>이제 직접 빌드하신 이미지를 서버 상에 돌리는데에는 거의 문제가 없습니다. 다만 겪으실 수 있는 마지막 관문은 공개 이미지를 직접 사용할때 일어납니다. 대부분의 &#39;Awesome&#39;으로 지정된 서비스와 라이브러리들은 Arm 아키텍처도 지원합니다.</p>
<p><img src="https://images.velog.io/images/infl_veggie/post/87814e2f-9820-4698-99c0-be37465caade/image.png" alt=""></p>
<pre><code>위와 같이 redis는 ARM 64를 지원합니다. 사용에 문제가 없습니다.</code></pre><p>하지만 도커허브 상의 널리 사용되지 않는 일부 이미지들은 x86-64만 지원하는 경우도 있습니다. </p>
<p><img src="https://images.velog.io/images/infl_veggie/post/44058bb2-446b-45d2-98b7-9a712acac0d8/image.png" alt=""></p>
<pre><code>Arm을 지원하지 않는 celery 모니터링 서비스 flower</code></pre><p>이 경우 해당 이미지의 멀티 아키텍처 베이스 이미지와, 공개된 도커파일을 가져와 <code>docker buildx</code>로 직접 멀티 아키텍처 빌드해 사용하실 필요가 있습니다. 다만 커스텀 이미지만을 사용하는 경우가 많고, 이런 문제가 흔한 일은 아니어서 크게 걱정하진 않아도 괜찮다고 생각합니다. Arm64를 지원하는 이미지만을 찾고 싶으신 경우, 도커 허브의 <code>Architectures</code> 필터에서 Arm64를 체크해 검색하면 됩니다.</p>
<h2 id="그래서-써보니-어때">그래서 써보니 어때?</h2>
<p>앞서 t4g 인스턴스에서 도커를 사용할때 겪을 만한 어려움과 해결책을 다뤘습니다. 다만, 한번 세팅하고 나면 큰 문제가 될만한 내용은 아니라고 생각합니다.</p>
<p>실제로 저희는 초기 세팅과 안정성 테스트 후에는 t4g 인스턴스에 만족해 잘 사용하고 있습니다. 현재도 여러 대의 t4g 인스턴스 위에 도커 스웜을 올려 프로덕션 서버를 운용하고 있습니다. 다만 대규모 유저를 대상으로 하진 않아 저희가 겪지 못한 문제가 발생할 수 있으니 참고만 해주시면 감사하겠습니다.  </p>
<h2 id="세줄요약">세줄요약</h2>
<ol>
<li>AWS EC2의 t4g 인스턴스, 가성비는 동급 t3 인스턴스에 비해 매우 뛰어나다.</li>
<li>Arm 아키텍처 기반이라 깊이 있게 들어갔을 때의 호환성 문제가 있고, 아키텍처에 대한 이해가 필요한 것은 흠이다.</li>
<li>뛰어난 가성비를 요구하는 보조 서버 용으로는 만족스럽다고 생각합니다.</li>
</ol>
<blockquote>
<p>모든 종류의 지적에 대해 미리 감사드립니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 7: 튜토리얼 (4) - 테스팅]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-4</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-4</guid>
            <pubDate>Fri, 15 Jan 2021 18:16:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/tutorial/index.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="튜토리얼-파트-4-자동화된-테스팅">튜토리얼 파트 4: 자동화된 테스팅</h1>
<blockquote>
<p>튜토리얼 3에서 이어집니다. </p>
</blockquote>
<p>역자 주: <em>이 튜토리얼은 테스트 코드 작성법을 다루고 있습니다. 단순히 기능 구현만을 원하시는 분들은 컨슈머로 넘어가셔도 괜찮습니다.</em></p>
<h2 id="뷰-테스트하기">뷰 테스트하기</h2>
<p>채팅 서버가 계속 잘 동작할지 보장하려면 테스트 코드를 작성해야 할 필요가 있습니다.</p>
<p>엔드투엔드 테스트 케이스를 만들기 위해 크롬 웹 브라우저를 제어하는 <code>Selenium</code>을 사용할겁니다. 테스트해야 할 대상은 다음과 같습니다:</p>
<ul>
<li>채팅 메시지가 게시되면 같은 방 안의 모두가 해당 메시지를 볼 수 있어야 한다.</li>
<li>채팅 메시지가 게시되면 다른 방의 그 누구도 해당 메시지를 볼 수 없어야 한다.</li>
</ul>
<p>크롬 웹브라우저가 없으시면 지금 <a href="https://www.google.com/chrome/">설치</a>해주세요.</p>
<p>이어서 <a href="https://sites.google.com/a/chromium.org/chromedriver/getting-started">크롬드라이버</a>를 설치해주세요.</p>
<p>그 후, 다음 커맨드로 <code>Selenium</code>을 설치합니다:</p>
<pre><code class="language-bash">$ python3 -m pip install selenium</code></pre>
<p><code>chat/tests.py</code>라는 이름으로 파일을 하나 만듭니다. 이제 앱 디렉토리는 다음과 같습니다:</p>
<pre><code>chat/
    __init__.py
    consumers.py
    routing.py
    templates/
        chat/
            index.html
            room.html
    tests.py
    urls.py
    views.py</code></pre><p>다음 코드를 <code>chat/tests.py</code>에 붙여넣으세요:</p>
<pre><code class="language-python"># chat/tests.py
from channels.testing import ChannelsLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.wait import WebDriverWait

class ChatTests(ChannelsLiveServerTestCase):
    serve_static = True  # emulate StaticLiveServerTestCase

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        try:
            # NOTE: Requires &quot;chromedriver&quot; binary to be installed in $PATH
            cls.driver = webdriver.Chrome()
        except:
            super().tearDownClass()
            raise

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()
        super().tearDownClass()

    def test_when_chat_message_posted_then_seen_by_everyone_in_same_room(self):
        try:
            self._enter_chat_room(&#39;room_1&#39;)

            self._open_new_window()
            self._enter_chat_room(&#39;room_1&#39;)

            self._switch_to_window(0)
            self._post_message(&#39;hello&#39;)
            WebDriverWait(self.driver, 2).until(lambda _:
                &#39;hello&#39; in self._chat_log_value,
                &#39;Message was not received by window 1 from window 1&#39;)
            self._switch_to_window(1)
            WebDriverWait(self.driver, 2).until(lambda _:
                &#39;hello&#39; in self._chat_log_value,
                &#39;Message was not received by window 2 from window 1&#39;)
        finally:
            self._close_all_new_windows()

    def test_when_chat_message_posted_then_not_seen_by_anyone_in_different_room(self):
        try:
            self._enter_chat_room(&#39;room_1&#39;)

            self._open_new_window()
            self._enter_chat_room(&#39;room_2&#39;)

            self._switch_to_window(0)
            self._post_message(&#39;hello&#39;)
            WebDriverWait(self.driver, 2).until(lambda _:
                &#39;hello&#39; in self._chat_log_value,
                &#39;Message was not received by window 1 from window 1&#39;)

            self._switch_to_window(1)
            self._post_message(&#39;world&#39;)
            WebDriverWait(self.driver, 2).until(lambda _:
                &#39;world&#39; in self._chat_log_value,
                &#39;Message was not received by window 2 from window 2&#39;)
            self.assertTrue(&#39;hello&#39; not in self._chat_log_value,
                &#39;Message was improperly received by window 2 from window 1&#39;)
        finally:
            self._close_all_new_windows()

    # === Utility ===

    def _enter_chat_room(self, room_name):
        self.driver.get(self.live_server_url + &#39;/chat/&#39;)
        ActionChains(self.driver).send_keys(room_name + &#39;\n&#39;).perform()
        WebDriverWait(self.driver, 2).until(lambda _:
            room_name in self.driver.current_url)

    def _open_new_window(self):
        self.driver.execute_script(&#39;window.open(&quot;about:blank&quot;, &quot;_blank&quot;);&#39;)
        self.driver.switch_to_window(self.driver.window_handles[-1])

    def _close_all_new_windows(self):
        while len(self.driver.window_handles) &gt; 1:
            self.driver.switch_to_window(self.driver.window_handles[-1])
            self.driver.execute_script(&#39;window.close();&#39;)
        if len(self.driver.window_handles) == 1:
            self.driver.switch_to_window(self.driver.window_handles[0])

    def _switch_to_window(self, window_index):
        self.driver.switch_to_window(self.driver.window_handles[window_index])

    def _post_message(self, message):
        ActionChains(self.driver).send_keys(message + &#39;\n&#39;).perform()

    @property
    def _chat_log_value(self):
        return self.driver.find_element_by_css_selector(&#39;#chat-log&#39;).get_property(&#39;value&#39;)</code></pre>
<p>이 테스트 클래스는 장고에서 엔드투엔드 테스트용으로 보통 사용하는 <code>StaticLiveServerTestCase</code>나 <code>LiveServerTestCase</code>가 아니라 <code>ChannelsLiveServerTestCase</code>를 상속받아 채널 라우팅 설정 안의 <code>/ws/room/ROOM_NAME/</code>와 같은 URL이 잘 작동하도록 합니다.</p>
<hr>
<p>우리는 여기서 테스팅 시에 인메모리 DB로 작동하는 <code>sqlite3</code>를 사용하려 하므로 테스트가 제대로 동작하지 않을 것입니다. 따라서 본 프로젝트에서 <code>sqlite3</code> 데이터베이스가 테스트시에 인메모리 방식이 아니도록 설정해야 합니다. <code>mysite/settings.py</code>의 <code>DATABASES</code> 설정에 <code>TEST</code> 인자를 추가합니다:</p>
<pre><code class="language-python"># mysite/settings.py
DATABASES = {
    &#39;default&#39;: {
        &#39;ENGINE&#39;: &#39;django.db.backends.sqlite3&#39;,
        &#39;NAME&#39;: os.path.join(BASE_DIR, &#39;db.sqlite3&#39;),
        &#39;TEST&#39;: {
            &#39;NAME&#39;: os.path.join(BASE_DIR, &#39;db_test.sqlite3&#39;)
        }
    }
}</code></pre>
<p>테스트를 실행하려면 다음 커맨드를 입력합니다:</p>
<pre><code class="language-bash">$ python3 manage.py test chat.tests</code></pre>
<p>그러면 콘솔에 다음과 같이 출력됩니다:</p>
<pre><code class="language-bash">Creating test database for alias &#39;default&#39;...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 5.014s

OK
Destroying test database for alias &#39;default&#39;...</code></pre>
<p>채팅 서버 테스팅까지 성공하였습니다!</p>
<hr>
<h2 id="이-다음엔">이 다음엔?</h2>
<p>축하드립니다. 잘 동작하는 채팅 서버를 구현하고, 비동기적으로 작성해 성능을 높였고, 후에 실패하지 않도록 자동화된 테스트도 작성했습니다.</p>
<p>튜토리얼은 이것으로 끝입니다. 이제 본인의 앱에서 채널즈를 시작할 수 있을 정도가 되셨을 것입니다. 앞으로는 필요한 때에 이 다음에 이어지는 문서들로 돌아오시면 됩니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 6: 튜토리얼 (3) - 비동기 컨슈머]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-3</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-3</guid>
            <pubDate>Fri, 15 Jan 2021 17:59:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/tutorial/index.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="튜토리얼-파트-3-채팅-서버-비동기로-다시-작성하기">튜토리얼 파트 3: 채팅 서버, 비동기로 다시 작성하기</h1>
<blockquote>
<p>튜토리얼 2에서 이어집니다. </p>
</blockquote>
<p>이제 성능 향상을 위해 동기적으로 작성된 컨슈머 코드를 비동기적으로 다시 작성해 봅시다.</p>
<h2 id="컨슈머-비동기로-다시-작성하기">컨슈머, 비동기로 다시 작성하기</h2>
<p>우리가 만든 <code>ChatConsumer</code>는 동기적으로 작동합니다. 동기적인 컨슈머는 장고 모델 접근과 같은 일반적인 동기적 입출력 함수들을 별다른 처리 없이 사용할 수 있어서 편리합니다. 하지만 비동기적인 컨슈머들은 요청을 처리할 때 별도의 쓰레드를 만들 필요가 없어 훨씬 우수한 성능을 자랑합니다.</p>
<p><code>ChatConsumer</code>는 그 자체로 비동기적인 라이브러리(채널즈와 채널 레이어)만을 사용하고, 특히 장고 모델에 접근하지 않기 때문에 간단하게 비동기적으로 다시 작성할 수 있습니다.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p><code>ChatConsumer</code>가 장고 모델이나 다른 동기적인 코드에 접근한다 하더라도 비동기적으로 만들 수 있습니다. <code>asgiref.sync.sync_to_async</code>나 <code>channels.db.database_sync_to_async</code>와 같은 유틸 함수들은 비동기적인 컨슈머에서 동기적인 코드를 실행하기 위해 사용할 수 있습니다. 하지만 비동기적인 컨슈머에서 얻을 수 있는 성능 향상의 정도는 그 자체로 비동기적인 라이브러리만 사용할 때에 비해 다소 줄어듭니다.</p>
</blockquote>
<hr>
<p><code>ChatConsumer</code>를 비동기적으로 다시 작성해 봅시다. 다음 코드를 <code>chat/consumers.py</code>에 붙여넣습니다:</p>
<pre><code class="language-python"># chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope[&#39;url_route&#39;][&#39;kwargs&#39;][&#39;room_name&#39;]
        self.room_group_name = &#39;chat_%s&#39; % self.room_name

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json[&#39;message&#39;]

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                &#39;type&#39;: &#39;chat_message&#39;,
                &#39;message&#39;: message
            }
        )

    # Receive message from room group
    async def chat_message(self, event):
        message = event[&#39;message&#39;]

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            &#39;message&#39;: message
        }))</code></pre>
<p>위 코드의 <code>ChatConsumer</code>는 기존의 <code>ChatConsumer</code>와 다음 차이점만 제외하면 거의 비슷해 보입니다:</p>
<ul>
<li><code>ChatConsumer</code>는 이제 <code>WebsocketConsumer</code>가 아닌 <code>AsyncWebsocketConsumer</code>를 상속받습니다.</li>
<li>모든 메서드들이 그냥 <code>def</code>가 아니라 <code>async def</code>입니다.</li>
<li>입출력 작업을 수행하는 비동기 함수를 호출하기 위해 <code>await</code>가 사용되었습니다.</li>
<li>채널레이어 상에서 메서드를 호출할 때<code>async_to_sync</code>가 더이상 필요하지 않습니다.</li>
</ul>
<hr>
<p>이제 <code>/ws/chat/ROOM_NAME/</code>에 해당하는 새 컨슈머가 잘 동작하는지 확인해 봅시다. 채널즈 개발 서버 실행을 위해 다음 커맨드를 입력합니다:</p>
<pre><code class="language-bash">$ python3 manage.py runserver</code></pre>
<p>브라우저를 열고 채팅방 페이지 <code>http://127.0.0.1:8000/chat/lobby/</code>로 이동합니다. 다른 탭을 하나 더 열어 동일한 채팅방 페이지로 이동합니다.</p>
<p>이제 두번째 브라우저 탭에서, “비동기 안녕!”이라고 타이핑하고 엔터를 눌러보세요. 그러면 두번째 탭과 첫번째 탭 모두에서 &quot;비동기 안녕!&quot;이라는 메시지가 채팅 로그에 나타나는 것을 확인하실 수 있습니다.</p>
<p>이제 이 채팅 서버는 완전히 비동기적입니다!</p>
<hr>
<p>이 튜토리얼은 <a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-4">튜토리얼 4</a>로 이어집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 5: 튜토리얼 (2) - 채팅 서버 구현]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-2</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-2</guid>
            <pubDate>Fri, 15 Jan 2021 17:37:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/tutorial/index.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="튜토리얼-파트-2-채팅-서버-만들기">튜토리얼 파트 2: 채팅 서버 만들기</h1>
<blockquote>
<p>튜토리얼 1에서 이어집니다. </p>
</blockquote>
<p>이제 채팅방 페이지를 구현해 같은 방 안에 있는 사람들끼리 채팅할 수 있도록 만들어 봅시다.</p>
<h2 id="채팅방-뷰-추가">채팅방 뷰 추가</h2>
<p>이제 두번째 뷰인 채팅방 뷰(room view)를 만들어 특정 방에 게시된 메시지를 볼 수 있게 할겁니다.</p>
<p><code>chat/templates/chat/room.html</code> 파일을 만들어주세요. 그러면 앱 디렉토리는 다음과 같습니다:</p>
<pre><code>chat/
    __init__.py
    templates/
        chat/
            index.html
            room.html
    urls.py
    views.py</code></pre><p>채팅방 뷰의 템플릿을 만들어 줍니다. 다음 코드를 <code>chat/templates/chat/room.html</code>에 붙여넣어 주세요:</p>
<pre><code class="language-html">&lt;!-- chat/templates/chat/room.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;/&gt;
    &lt;title&gt;Chat Room&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;textarea id=&quot;chat-log&quot; cols=&quot;100&quot; rows=&quot;20&quot;&gt;&lt;/textarea&gt;&lt;br&gt;
    &lt;input id=&quot;chat-message-input&quot; type=&quot;text&quot; size=&quot;100&quot;&gt;&lt;br&gt;
    &lt;input id=&quot;chat-message-submit&quot; type=&quot;button&quot; value=&quot;Send&quot;&gt;
    {{ room_name|json_script:&quot;room-name&quot; }}
    &lt;script&gt;
        const roomName = JSON.parse(document.getElementById(&#39;room-name&#39;).textContent);

        const chatSocket = new WebSocket(
            &#39;ws://&#39;
            + window.location.host
            + &#39;/ws/chat/&#39;
            + roomName
            + &#39;/&#39;
        );

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector(&#39;#chat-log&#39;).value += (data.message + &#39;\n&#39;);
        };

        chatSocket.onclose = function(e) {
            console.error(&#39;Chat socket closed unexpectedly&#39;);
        };

        document.querySelector(&#39;#chat-message-input&#39;).focus();
        document.querySelector(&#39;#chat-message-input&#39;).onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector(&#39;#chat-message-submit&#39;).click();
            }
        };

        document.querySelector(&#39;#chat-message-submit&#39;).onclick = function(e) {
            const messageInputDom = document.querySelector(&#39;#chat-message-input&#39;);
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                &#39;message&#39;: message
            }));
            messageInputDom.value = &#39;&#39;;
        };
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<hr>
<p>이제 <code>chat/views.py</code>에서 채팅방 뷰를 만들어 줍니다:</p>
<pre><code class="language-python"># chat/views.py
from django.shortcuts import render

def index(request):
    return render(request, &#39;chat/index.html&#39;, {})

def room(request, room_name):
    return render(request, &#39;chat/room.html&#39;, {
        &#39;room_name&#39;: room_name
    })</code></pre>
<hr>
<p>그런 다음 <code>chat/urls.py</code>에서 채팅방 뷰를 위한 url 경로를 다음과 같이 지정해 줍니다:</p>
<pre><code class="language-python"># chat/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path(&#39;&#39;, views.index, name=&#39;index&#39;),
    path(&#39;&lt;str:room_name&gt;/&#39;, views.room, name=&#39;room&#39;),
]</code></pre>
<hr>
<p>다시 채널즈 개발 서버를 켜볼까요!</p>
<pre><code class="language-bash">$ python3 manage.py runserver</code></pre>
<p>브라우저를 열고 <code>http://127.0.0.1:8000/chat/</code>로 들어가 보세요. 초기 화면에서 &quot;lobby&quot;를 방 이름으로 타이핑하고 엔터를 칩니다. 그러면 <code>http://127.0.0.1:8000/chat/lobby/</code>로 리다이렉트 되고, 빈 채팅 로그가 나타납니다.</p>
<p>&quot;안녕!&quot;이라고 메시지를 타이핑하고 엔터를 눌러보세요. 아무 일도 일어나지 않습니다. 특히 방금 타이핑한 메시지도 채팅 로그에 나타나지 않습니다. 왜일까요?</p>
<p>엔터를 누르면 채팅방 뷰는 URL <code>ws://127.0.0.1:8000/ws/chat/lobby/</code>로 향하는 WebSocket을 열려 합니다. 하지만 해당하는 웹소켓 연결을 받아들이는 <code>consumer</code>를 아직 만들지 않았죠. 따라서 브라우저의 자바스크립트 콘솔을 열면 다음과 같은 에러가 나타납니다:</p>
<pre><code class="language-bash">WebSocket connection to &#39;ws://127.0.0.1:8000/ws/chat/lobby/&#39; failed: Unexpected response code: 500</code></pre>
<h2 id="컨슈머-만들기">컨슈머 만들기</h2>
<p>장고는 HTTP 요청을 받으면 최상단 URLconf를 확인해 대응하는 뷰 함수를 찾고, 이 뷰가 이 요청을 처리할 수 있도록 호출합니다. 마찬가지로, 채널즈가 웹소켓 요청을 받으면 최상단 라우팅 설정을 확인해 대응하는 컨슈머(Consumer)를 찾고, 이 컨슈머의 다양한 함수를 호출해 요청을 처리하도록 합니다.</p>
<p>한 번 <code>/ws/chat/ROOM_NAME/</code>로 들어오는 웹소켓 연결을 처리하는 기본적인 컨슈머를 하나 작성해 봅시다. 이 컨슈머는 들어오는 메시지를 받아들이고, 같은 웹소켓으로 다시 보내줘야 합니다.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>웹소켓 연결을 다른 일반적인 HTTP 연결과 구분하기 위해 웹소켓용 경로에는 앞에 <code>/ws/</code> 같은 것을 붙여주는 것이 좋습니다. 이를 통해 채널즈를 프로덕션 환경에 배포할 때 특정 설정을 더욱 쉽게 할 수 있습니다.</p>
<p>특히 대형 사이트 등에서 <code>nginx</code>와 같은 프로덕션 수준 HTTP 서버를 설정해 요청을 경로에 따라 나누어 (1) 일반 HTTP 요청은 <code>Gunicorn</code>+<code>Django</code>와 같은 프로덕션의 WSGI 서버로 전달하고 (2) 웹소켓 요청은 <code>Daphne</code>+<code>Channels</code>와 같은 프로덕션의 ASGI 서버로 전달해 처리하는 것이 가능합니다.</p>
<p>다만 작은 규모의 사이트에서는 더 단순하게 별도의 WSGI 서버를 두지 않고 <code>Daphne</code> 서버가 혼자 모든 요청-HTTP와 웹소켓 둘 다-을 처리하도록 할 수도 있습니다. 이런 배포환경에서는 <code>/ws/</code>와 같은 공통된 경로 앞에 붙는 단어가 필요하지 않습니다.</p>
</blockquote>
<p>컨슈머를 만들기 위해 <code>chat/consumers.py</code> 파일을 만들어 줍니다. 앱 디렉토리 구조는 다음과 같습니다:</p>
<pre><code class="language-bash">chat/
    __init__.py
    consumers.py
    templates/
        chat/
            index.html
            room.html
    urls.py
    views.py</code></pre>
<p>다음 코드를 <code>chat/consumers.py</code>에 붙여 넣으세요:</p>
<pre><code class="language-python"># chat/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json[&#39;message&#39;]

        self.send(text_data=json.dumps({
            &#39;message&#39;: message
        }))</code></pre>
<p>이 <code>ChatConsumer</code>는 동기적인 웹소켓 컨슈머로, 모든 연결을 승낙하고(accept), 클라이언트로부터 메시지를 받고(receive), 동일한 클라이언트에게 이 메시지들을 다시 돌려줍니다(send). 아직은 이 컨슈머가 같은 채팅방의 다른 클라이언트에게 메시지를 전파해주지는 못합니다.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>채널즈에서는 더 나은 성능을 위해 비동기적인 컨슈머를 만들 수 있습니다. 하지만 비동기적인 컨슈머를 사용할 때는 장고 모델에 접근하는 것과 같이 동작을 차단하는(blocking) 작업을 직접 수행하지 않도록 주의해야 합니다. 비동기적인 컨슈머를 작성하는 법을 더 알고 싶다면 <a href="https://channels.readthedocs.io/en/stable/topics/consumers.html">컨슈머</a>를 참조하세요.</p>
</blockquote>
<hr>
<p>이제 <code>chat</code> 앱을 위한 라우팅 설정을 만들어 요청을 이 컨슈머로 전달해줘야 합니다. <code>chat/routing.py</code> 파일을 만드세요. 앱 디렉토리 구조는 이제 다음과 같습니다:</p>
<pre><code>chat/
    __init__.py
    consumers.py
    routing.py
    templates/
        chat/
            index.html
            room.html
    urls.py
    views.py</code></pre><hr>
<p>다음 코드를 <code>chat/routing.py</code>에 붙여넣으세요:</p>
<pre><code class="language-python"># chat/routing.py
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r&#39;ws/chat/(?P&lt;room_name&gt;\w+)/$&#39;, consumers.ChatConsumer.as_asgi()),
]</code></pre>
<p>ASGI 애플리케이션을 얻기 위해 클래스 메서드인 <code>as_asgi()</code>를 호출했는데, 이 애플리케이션이 각 유저별 연결을 처리하는 컨슈머 인스턴스를 만들어줍니다. 이 메서드는 요청별로 장고 뷰 인스턴스를 만들어주는 장고의 <code>as_view()</code>와 유사합니다.</p>
<p>(여기선 URLRouter의 한계 때문에 <code>re_path()</code>를 사용했습니다.)</p>
<hr>
<p>다음 단계는 최상단 라우팅 설정이 <code>chat.routing</code> 모듈을 가리키도록 하는 것입니다. <code>mysite/asgi.py</code>에서, <code>AuthMiddlewareStack</code>, <code>URLRouter</code>, 그리고 <code>chat.routing</code>를 임포트하세요; 그리고 &#39;websocket&#39; 키를 <code>ProtocolTypeRouter</code> 리스트에 다음과 같은 형식으로 추가합니다:</p>
<pre><code class="language-python"># mysite/asgi.py
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing

os.environ.setdefault(&quot;DJANGO_SETTINGS_MODULE&quot;, &quot;mysite.settings&quot;)

application = ProtocolTypeRouter({
  &quot;http&quot;: get_asgi_application(),
  &quot;websocket&quot;: AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})</code></pre>
<blockquote>
<p><strong>주의사항</strong></p>
<p>장고 2.2에서는 <code>ProtocolTypeRouter</code>의 <code>http</code> 키가 채널즈의 <code>AsgiHandler</code>를 사용함에 주의하세요. 그 외엔 동일합니다. 웹소켓 키는 기존에 없는 것이어서 모든 버전에서 동일합니다.</p>
</blockquote>
<p>이 최상단 라우팅 설정은 채널즈 개발 서버에 연결이 구성될 때, <code>ProtocolTypeRouter</code>가 제일 먼저 연결 타입을 확인하도록 합니다. 만약 연결이 웹소켓 타입이라면 (<strong>ws</strong>://나 <strong>wss</strong>://), 이 연결은 <code>AuthMiddlewareStack</code>으로 전달됩니다.</p>
<p><code>AuthMiddlewareStack</code>은 현재 인증된 유저에 대한 참조를 연결의 <code>scope</code>에 추가하는데, 이는 장고에서 <code>AuthenticationMiddleware</code>가 뷰 함수의 <code>request</code> 객체에 현재 인증된 유저를 추가하는 방식과 비슷합니다. (<code>Scope</code>는 튜토리얼 후반부에 다룹니다.) 그 후 이 연결은 <code>URLRouter</code>에게 전달됩니다.</p>
<p><code>URLRouter</code>는 연결의 HTTP 경로를 확인해 적절한 컨슈머에게 연결해 줍니다.</p>
<hr>
<p>한 번 <code>/ws/chat/ROOM_NAME/</code>에 지정한 컨슈머가 잘 작동하는지 확인해 봅시다. DB 변경사항을 반영하기 위해 마이그레이션을 진행합니다(장고의 세션 프레임워크가 DB를 사용하기 때문입니다), 그리고 채널즈 개발 서버를 실행합니다:</p>
<pre><code class="language-python">$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK
$ python3 manage.py runserver</code></pre>
<p>브라우저를 열고 채팅방 페이지 <code>http://127.0.0.1:8000/chat/lobby/</code>로 가면 빈 채팅 로그가 나타납니다.</p>
<p>이제 &quot;안녕!&quot;이라고 타이핑하고 엔터를 치면, 채팅 로그에 다시 &quot;안녕!&quot;이라는 메시지가 나타나는걸 확인할 수 있습니다!</p>
<p>하지만 아직입니다. 또다른 브라우저 탭을 열어서 동일한 채팅방 페이지 <code>http://127.0.0.1:8000/chat/lobby/</code>로 이동해 메시지를 타이핑하면, 방금 연 두번째 탭의 채팅 로그에서는 메시지가 뜨지만 원래 첫번째 탭의 채팅 로그에서는 메시지가 나타나지 않습니다. 이것이 가능하려면, 우리는 동일한 <code>ChatConsumer</code>의 여러 인스턴스들이 서로 소통할 수 있게 해야합니다. 채널즈는 컨슈머 간에 이러한 종류의 통신이 가능하도록 추상화된 &#39;채널 레이어&#39;를 제공합니다.</p>
<p>터미널로 돌아가 <code>Control-C</code>로 서버를 중지하세요.</p>
<h2 id="채널-레이어-활성화하기">채널 레이어 활성화하기</h2>
<p>채널 레이어는 일종의 통신 체계입니다. 이를 통해 여러 컨슈머들끼리 서로 소통하고, 또한 장고의 다른 부분들과도 소통하게 할 수 있습니다.</p>
<p>채널 레이어는 다음과 같은 추상화된 대상을 제공합니다:</p>
<ul>
<li><code>채널</code>은 메시지가 전달되는 우편함입니다. 각각의 채널은 자기 이름을 갖고 있습니다. 채널 이름을 알고있는 누구든 해당 채널로 메시지를 보낼 수 있습니다.</li>
<li><code>그룹</code>은 관련있는 채널들의 모임입니다. 그룹은 이름을 갖고 있습니다. 그룹의 이름을 아는 누구든 새 채널을 그룹에 추가하거나, 그룹에서 제거하거나, 그룹에 등록된 모든 채널에 메시지를 보낼 수 있습니다. 하지만 특정 그룹에 있는 채널들을 쭉 나열하는 것은 불가능합니다.</li>
</ul>
<p>모든 컨슈머 인스턴스는 자동으로 생성된 유니크한 채널명을 갖고 있고, 따라서 채널 레이어를 통해 통신할 수 있습니다.</p>
<p>우리 채팅 앱에서 우리는 여러 개의 <code>ChatConsumer</code>가 서로 통신을 주고받게 하고 싶습니다. 그러기 위해선 각 <code>ChatConsumer</code>가 자신의 채널을 (채팅방 이름을 바탕으로 하는 이름의) 그룹에 등록해야합니다. 그러면 <code>ChatConsumers</code>가 같은 방 안의 모든 <code>ChatConsumers</code>들에게 메시지를 전파할 수 있습니다.</p>
<hr>
<p>우리는 <code>Redis</code>를 저장소로 하는 채널 레이어를 사용할 겁니다. 도커를 이용해 <code>Redis</code>를 포트 6379에서 실행하려면, 다음 커멘드를 입력합니다:</p>
<pre><code class="language-bash">$ docker run -p 6379:6379 -d redis:5</code></pre>
<p>채널즈가 레디스에 접근할 수 있게 하려면 다음 커맨드로 <code>channels_redis</code>를 설치해야 합니다:</p>
<pre><code class="language-bash">$ python3 -m pip install channels_redis</code></pre>
<p>채널 레이어를 사용하려면 먼저 몇 가지 설정을 해야 합니다. <code>mysite/settings.py</code> 파일 끝에 <code>CHANNEL_LAYERS</code> 설정을 다음과 같이 추가해주세요:</p>
<pre><code class="language-python"># mysite/settings.py
# Channels
ASGI_APPLICATION = &#39;mysite.asgi.application&#39;
CHANNEL_LAYERS = {
    &#39;default&#39;: {
        &#39;BACKEND&#39;: &#39;channels_redis.core.RedisChannelLayer&#39;,
        &#39;CONFIG&#39;: {
            &quot;hosts&quot;: [(&#39;127.0.0.1&#39;, 6379)],
        },
    },
}</code></pre>
<blockquote>
<p><strong>주의사항</strong></p>
<p>여러 개의 채널 레이어를 갖도록 설정할 수도 있습니다. 하지만 대부분의 프로젝트는 <code>default</code> 채널 레이어 하나만을 사용할 것입니다.</p>
</blockquote>
<hr>
<p>채널 레이어가 <code>Redis</code>와 통신할 수 있는지 확인해 봅시다. 장고 쉘을 열고 다음 커맨드를 실행합니다:</p>
<pre><code class="language-python">$ python3 manage.py shell
&gt;&gt;&gt; import channels.layers
&gt;&gt;&gt; channel_layer = channels.layers.get_channel_layer()
&gt;&gt;&gt; from asgiref.sync import async_to_sync
&gt;&gt;&gt; async_to_sync(channel_layer.send)(&#39;test_channel&#39;, {&#39;type&#39;: &#39;hello&#39;})
&gt;&gt;&gt; async_to_sync(channel_layer.receive)(&#39;test_channel&#39;)
{&#39;type&#39;: &#39;hello&#39;}</code></pre>
<p><code>Control-D</code>를 눌러 장고 쉘에서 나옵니다.</p>
<p>이제 채널 레이어가 준비됐으니, <code>ChatConsumer</code>에서 사용해봅시다. 다음 코드를 <code>chat/consumers.py</code> 파일에 붙여넣어 기존 코드를 덮어 씌웁니다:</p>
<pre><code class="language-python"># chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope[&#39;url_route&#39;][&#39;kwargs&#39;][&#39;room_name&#39;]
        self.room_group_name = &#39;chat_%s&#39; % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json[&#39;message&#39;]

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                &#39;type&#39;: &#39;chat_message&#39;,
                &#39;message&#39;: message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event[&#39;message&#39;]

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            &#39;message&#39;: message
        }))</code></pre>
<p>유저가 메시지를 전송하면, 자바스크립트 함수는 이 메시지를 웹소켓을 통해 <code>ChatConsumer</code>로 전송합니다. <code>ChatConsumer</code>는 메시지를 받아, 채팅방 이름에 해당하는 그룹으로 전파합니다. 같은 그룹안에 있는 (즉, 같은 채팅방 안에 있는) 모든 <code>ChatConsumer</code>는 그룹으로부터 메시지를 전달받아, 웹소켓을 통해 자바스크립트로 이를 돌려주고, 따라서 채팅 로그에 이 메시지가 추가됩니다.</p>
<p><code>ChatConsumer</code> 코드의 몇몇 부분은 추가적인 설명이 필요합니다:</p>
<ul>
<li><code>self.scope[&#39;url_route&#39;][&#39;kwargs&#39;][&#39;room_name&#39;]</code><ul>
<li>웹소켓 연결을 컨슈머에게 전달한 <code>chat/routing.py</code>의 URL route로부터<code>room_name</code> 인자를 얻습니다. </li>
<li>모든 컨슈머는 자신의 연결에 대한 정보가 담긴 <code>scope</code>를 갖습니다. <code>scope</code> 안에는 모든 URL route 인자들과, 만약 존재한다면 현재 인증된 유저 정보가 들어 있습니다.</li>
</ul>
</li>
</ul>
<ul>
<li><code>self.room_group_name = &#39;chat_%s&#39; % self.room_name</code><ul>
<li>채널즈 그룹명을 유저가 지정한 채팅방 이름에서 다른 처리 없이 직접 구성합니다.</li>
<li>그룹명은 알파벳, 숫자, 하이픈, 그리고 온점만으로 구성돼야 합니다. 따라서 이 예제 코드는 그 외 문자가 들어간 채팅방 이름 입력에 대해 실패함에 유의해야 합니다.</li>
</ul>
</li>
</ul>
<ul>
<li><code>async_to_sync(self.channel_layer.group_add)(...)</code><ul>
<li>그룹에 들어갑니다.</li>
<li><code>async_to_sync(…)</code>는 ChatConsumer가 동기적인 <code>WebsocketConsumer</code>이지만 채널 레이어의 비동기적인 메서드를 호출하고 있기 때문에 필요합니다(채널 레이어의 메서드들은 모두 비동기적입니다.)</li>
<li>그룹명은 아스키 영문자, 숫자, 하이픈, 온점으로 제한됩니다. 유효하지 않은 문자가 포함돼있다면 에러가 발생합니다.</li>
</ul>
</li>
</ul>
<ul>
<li><code>self.accept()</code><ul>
<li>웹소켓 연결을 승낙합니다(Accept).</li>
<li><code>connect()</code> 메서드 안에서 <code>accept()</code>를 호출하지 않는다면, 연결 요청은 거부되고 종료됩니다. 요청하는 유저가 해당 동작을 수행하기 위한 권한이 없는 등의 이유로 연결을 거부할 필요가 있을 수 있습니다.</li>
<li>연결을 승낙하려는 경우 <code>accept()</code>를 <code>connect()</code>의 마지막 동작으로 호출하도록 권장합니다.</li>
</ul>
</li>
</ul>
<ul>
<li><code>async_to_sync(self.channel_layer.group_discard)(...)</code><ul>
<li>그룹을 떠납니다.</li>
</ul>
</li>
</ul>
<ul>
<li><code>async_to_sync(self.channel_layer.group_send)</code><ul>
<li>그룹에 이벤트를 전송합니다.</li>
<li>이벤트는 메서드명에 대응되는 특별한 <code>type</code> 키를 갖고 있습니다. 이 이벤트를 전달받는 컨슈머들은 대응되는 메서드명의 메서드를 실행하게 됩니다.</li>
</ul>
</li>
</ul>
<hr>
<p>이제 <code>/ws/chat/ROOM_NAME/</code>에 해당하는 새 컨슈머가 잘 동작하는지 확인해 봅시다. 채널즈 개발 서버 실행을 위해 다음 커맨드를 입력합니다:</p>
<pre><code class="language-bash">$ python3 manage.py runserver</code></pre>
<p>브라우저를 열고 채팅방 페이지 <code>http://127.0.0.1:8000/chat/lobby/</code>로 이동합니다. 다른 탭을 하나 더 열어 동일한 채팅방 페이지로 이동합니다.</p>
<p>이제 두번째 브라우저 탭에서, “정말로 안녕!”이라고 타이핑하고 엔터를 눌러보세요. 그러면 두번째 탭과 첫번째 탭 모두에서 &quot;정말로 안녕!&quot;이라는 메시지가 채팅 로그에 나타나는 것을 확인하실 수 있습니다.</p>
<p>방금 기본적인 모든 기능이 잘 동작하는 채팅 서버를 만드신 겁니다!</p>
<hr>
<p>이 튜토리얼은 <a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-3">튜토리얼 3</a>으로 이어집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 4: 튜토리얼 (1) - 기본 설정]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1</guid>
            <pubDate>Fri, 15 Jan 2021 12:51:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/tutorial/index.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="튜토리얼-파트-1-기본-설정">튜토리얼 파트 1: 기본 설정</h1>
<p>튜토리얼에서 우리는 다음의 두 페이지로 구성된 간단한 채팅 서버를 만들어보려 합니다.</p>
<ul>
<li>입장할 채팅방 이름을 타이핑하는 첫 화면(index view)</li>
<li>채팅 메시지를 볼 수 있는 개별 채팅방 화면(room view)</li>
</ul>
<p>채팅방 화면에서 웹소켓을 이용해 장고 서버와 통신하고 게시된 메시지를 받아올 것입니다.</p>
<p>아래 내용은 장고 프로젝트를 구성하는데 필요한 기본 지식을 갖추고 있다고 가정하고 진행됩니다. 그렇지 않다면 장고 튜토리얼을 먼저 완료하고 돌아와주세요.</p>
<hr>
<p>먼저 장고를 설치해주세요. 쉘 프롬프트에서 다음 커맨드를 실행해 장고가 설치되었는지 확인하고 그 버전을 알 수 있습니다.</p>
<pre><code class="language-bash">$ python3 -m django --version</code></pre>
<p>아직 채널즈를 설치하지 않았다면, 다음 커맨드로 설치합니다.</p>
<pre><code class="language-bash">$ python -m pip install -U channels</code></pre>
<p>채널즈가 설치되었는지 확인하려면 다음 커맨드를 실행하세요:</p>
<pre><code class="language-bash">$ python3 -c &#39;import channels; print(channels.__version__)&#39;</code></pre>
<blockquote>
<p>이 튜토리얼은 파이썬 3.6 이상과 장고 2.2 이상에서 호환되는 채널즈 3.0을 기준으로 작성되었습니다.</p>
</blockquote>
<hr>
<p>이 튜토리얼에서는 또한 <code>Redis</code>를 설치하고 실행하기 위해 <code>Docker</code>를 사용합니다. 레디스는 채널 레이어의 저장소로 활용됩니다. 도커를 공식 홈페이지의 <a href="https://docs.docker.com/get-docker/">설치법</a>에 따라 설치해주세요.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>기본적인 장고 <code>runserver</code>를 실행하는 데에는 도커가 필요하지는 않지만, 뒷 파트에서 사용할 채널즈의 기능들을 위해선 <code>Redis</code>가 필요합니다. 우리는 <code>Redis</code>를 실행하기 위한 가장 쉬운 방법으로 도커를 권장합니다.</p>
</blockquote>
<h2 id="프로젝트-생성">프로젝트 생성</h2>
<p>아직 장고 프로젝트가 없다면 새 프로젝트를 만들어야 합니다.</p>
<p>커맨드 라인에서 프로젝트를 생성할 디렉토리로 이동한 후 다음 커맨드를 실행하세요:</p>
<pre><code class="language-bash">$ django-admin startproject mysite</code></pre>
<p>이 커맨드는 다음과 같이 구성된 <code>mysite</code> 디렉토리를 생성합니다:</p>
<pre><code class="language-bash">mysite/
    manage.py
    mysite/
        __init__.py
        asgi.py
        settings.py
        urls.py
        wsgi.py</code></pre>
<blockquote>
<p><strong>주의사항</strong></p>
<p>장고 2.2는 asgi.py 파일을 생성하지 않습니다. 하지만 일 분도 걸리지 않아 만들 수 있으니 걱정 마세요.</p>
</blockquote>
<h2 id="chat-앱-만들기">Chat 앱 만들기</h2>
<p><code>chat</code> 앱을 만들어 그 안에서 채팅서버 코드를 작성할 겁니다.</p>
<p><code>manage.py</code>가 있는 디렉토리에서 다음 커맨드를 실행하세요:</p>
<pre><code class="language-bash">$ python3 manage.py startapp chat</code></pre>
<p>이 커맨드는 다음과 같이 구성된 <code>chat</code> 디렉토리를 생성할 것입니다:</p>
<pre><code>chat/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py</code></pre><p>이 튜토리얼의 목표를 달성하기 위해선 <code>chat/views.py</code>와 <code>chat/__init__.py</code>만 필요하니 다른 파일들은 지우셔도 좋습니다.</p>
<p>불필요한 파일을 지우고 나면 <code>chat</code> 디렉토리는 다음과 같이 구성될 것입니다:</p>
<pre><code>chat/
    __init__.py
    views.py</code></pre><p>이제 프로젝트에 <code>chat</code> 앱이 생성됐음을 알려야 합니다. <code>mysite/settings.py</code> 파일에서 <code>INSTALLED_APPS</code> 값에 다음과 같이 &#39;chat&#39;을 추가하세요.</p>
<pre><code class="language-python"># mysite/settings.py
INSTALLED_APPS = [
    &#39;chat&#39;,
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
]</code></pre>
<h2 id="인덱스-뷰-추가">인덱스 뷰 추가</h2>
<p>이제 입장할 채팅방 이름을 입력할 인덱스 뷰를 만들어 봅시다.</p>
<p><code>chat</code> 디렉토리 안에 <code>templates</code> 폴더를 만들고, 그 안에 <code>chat</code> 폴더를 생성합니다. 그리고 그 안에 인덱스 뷰의 템플릿을 담을 <code>index.html</code> 파일을 만듭니다.</p>
<p>그러면 이제 <code>chat</code> 디렉토리는 다음과 같습니다:</p>
<pre><code>chat/
    __init__.py
    templates/
        chat/
            index.html
    views.py</code></pre><p>이제 다음 코드를 <code>chat/templates/chat/index.html</code>에 붙여 넣으세요:</p>
<pre><code class="language-html">&lt;!-- chat/templates/chat/index.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;/&gt;
    &lt;title&gt;Chat Rooms&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    What chat room would you like to enter?&lt;br&gt;
    &lt;input id=&quot;room-name-input&quot; type=&quot;text&quot; size=&quot;100&quot;&gt;&lt;br&gt;
    &lt;input id=&quot;room-name-submit&quot; type=&quot;button&quot; value=&quot;Enter&quot;&gt;

    &lt;script&gt;
        document.querySelector(&#39;#room-name-input&#39;).focus();
        document.querySelector(&#39;#room-name-input&#39;).onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector(&#39;#room-name-submit&#39;).click();
            }
        };

        document.querySelector(&#39;#room-name-submit&#39;).onclick = function(e) {
            var roomName = document.querySelector(&#39;#room-name-input&#39;).value;
            window.location.pathname = &#39;/chat/&#39; + roomName + &#39;/&#39;;
        };
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>그 다음 인덱스 뷰를 위한 뷰 함수를 만듭니다. 아래 코드를 <code>chat/views.py</code>에 붙여 넣으세요:</p>
<pre><code class="language-python"># chat/views.py
from django.shortcuts import render

def index(request):
    return render(request, &#39;chat/index.html&#39;)</code></pre>
<p>뷰를 호출하려면 뷰를 URL에 매핑하기 위해 <code>URLconf</code>가 필요합니다.</p>
<p><code>urls.py</code>라는 이름으로 <code>chat</code> 디렉토리에 파일을 생성하세요. 이제 앱 디렉토리는 다음과 같습니다:</p>
<pre><code class="language-bash">chat/
    __init__.py
    templates/
        chat/
            index.html
    urls.py
    views.py</code></pre>
<p><code>chat/urls.py</code>에 다음 코드를 붙여넣으세요.</p>
<pre><code class="language-python"># chat/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path(&#39;&#39;, views.index, name=&#39;index&#39;),
]</code></pre>
<p>이제 최상단(root) URLconf가 방금 만든 <code>chat.urls</code> 모듈을 가리키게 해야합니다. <code>mysite/urls.py</code> 에서 <code>django.conf.urls.include</code>를 <code>import</code>하고, <code>urlpatterns</code> 리스트에 <code>include()</code>를 추가하세요. 그 결과는 다음과 같습니다:</p>
<pre><code class="language-python"># mysite/urls.py
from django.conf.urls import include
from django.urls import path
from django.contrib import admin

urlpatterns = [
    path(&#39;chat/&#39;, include(&#39;chat.urls&#39;)),
    path(&#39;admin/&#39;, admin.site.urls),
]</code></pre>
<hr>
<p>이제 인덱스 뷰가 작동하는지 확인해 봅시다. 다음 커맨드를 실행하세요:</p>
<pre><code class="language-bash">$ python3 manage.py runserver</code></pre>
<p>그러면 커맨드라인에 다음과 같은 문구가 출력됩니다:</p>
<pre><code class="language-bash">Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run &#39;python manage.py migrate&#39; to apply them.
October 21, 2020 - 18:49:39
Django version 3.1.2, using settings &#39;mysite.settings&#39;
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.</code></pre>
<p>브라우저를 열고 <code>http://127.0.0.1:8000/chat/</code> 주소로 이동하면 “What chat room would you like to enter?”와 함께 입장할 채팅방 이름을 입력하는 입력창이 나타납니다.</p>
<p>방 이름으로 &quot;lobby&quot;를 입력하고 엔터를 눌러보세요. 그러면 <code>http://127.0.0.1:8000/chat/lobby/</code> 주소의 room 뷰로 이동하지만, 아직 우리가 room 뷰를 작성하지 않았기 때문에 “Page not found” 에러 페이지가 나타날 겁니다.</p>
<p>터미널로 돌아가 <code>Control-C</code>를 눌러 서버를 중지하세요.</p>
<h2 id="채널즈-라이브러리-통합">채널즈 라이브러리 통합</h2>
<p>지금까지는 평범한 장고 앱 하나를 만들었습니다; 아직 채널즈 라이브러리는 전혀 사용하지 않았습니다. 이제 사용해 볼 때입니다.</p>
<p>먼저 채널즈를 위한 루트 라우팅 설정을 만듭니다. 채널즈 라우팅 설정은 채널즈 서버가 HTTP 요청을 받았을 때 어떤 코드를 실행할지 결정한다는 점에서, 장고의 <code>URLConf</code>와 유사한 ASGI 애플리케이션입니다.</p>
<p>다음 코드를 <code>mysite/asgi.py</code> 파일에 붙여 넣어 시작해봅시다.</p>
<pre><code class="language-python"># mysite/asgi.py
import os

from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault(&#39;DJANGO_SETTINGS_MODULE&#39;, &#39;mysite.settings&#39;)

application = ProtocolTypeRouter({
    &quot;http&quot;: get_asgi_application(),
    # Just HTTP for now. (We can add other protocols later.)
})</code></pre>
<blockquote>
<p><strong>주의사항</strong></p>
<p>장고 2.2에서는 ASGI를 자체 지원하지 않기 때문에 채널즈의 대체 시스템을 활용해야 합니다. <code>mysite/asgi.py</code> 파일을 다음과 같이 만드세요:</p>
<pre><code class="language-python"># mysite/asgi.py
import os

import django
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter

os.environ.setdefault(&#39;DJANGO_SETTINGS_MODULE&#39;, &#39;mysite.settings&#39;)
django.setup()

application = ProtocolTypeRouter({
  &quot;http&quot;: AsgiHandler(),
   # 지금은 HTTP만 등록합니다. (나중에 다른 프로토콜들도 등록할 거예요!)
})</code></pre>
</blockquote>
<p>이제<code>mysite/settings.py</code> 파일에서 <code>INSTALLED_APPS</code> 리스트에 다음과 같이 채널즈를 추가합니다:</p>
<pre><code class="language-python"># mysite/settings.py
INSTALLED_APPS = [
    &#39;channels&#39;,
    &#39;chat&#39;,
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
]</code></pre>
<p>또한 채널즈가 루트 라우팅 설정을 가리키도록 해야합니다. <code>mysite/settings.py</code> 파일 끝에 다음 코드를 붙여 넣으세요:</p>
<pre><code># mysite/settings.py
# Channels
ASGI_APPLICATION = &#39;mysite.asgi.application&#39;</code></pre><p>채널즈가 <code>INSTALLED_APPS</code> 리스트에 추가되면, 기존의 장고 개발 서버를 채널즈 개발 서버로 대체하도록 <code>runserver</code> 커맨드를 제어합니다.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>다른 서드파티 앱이 <code>runserver</code> 커맨드를 오버로드하거나 대체할 경우에 주의하세요. 채널즈는 별도의 <code>runserver</code> 커맨드를 제공하기 때문에 해당 앱과 충돌할 수 있습니다. 그러한 대표적인 사례로 <code>whitenoise</code> 앱의 <code>runserver_nostatic</code>이 있습니다. 이를 해결하기 위해선 채널즈를 <code>INSTALLED_APPS</code> 설정값의 최상단으로 이동하거나, 충돌하는 다른 앱을 제거해주세요.</p>
</blockquote>
<hr>
<p>채널즈 개발 서버가 올바르게 작동하는지 확인하기 위해 다음 커맨드를 실행합니다:</p>
<pre><code class="language-bash">$ python3 manage.py runserver</code></pre>
<p>그러면 다음과 같은 문구가 커맨드 라인에 출력됩니다:</p>
<pre><code class="language-bash">Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run &#39;python manage.py migrate&#39; to apply them.
October 21, 2020 - 19:08:48
Django version 3.1.2, using settings &#39;mysite.settings&#39;
Starting ASGI/Channels version 3.0.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Note</code></pre>
<p>적용되지 않은 DB 마이그레이션에 대한 <code>warning</code>은 무시하세요. 이 튜토리얼에서는 DB를 사용하지 않습니다.</p>
<p>다음과 같은 문구로 시작하는 라인에 주목하세요: <code>Starting ASGI/Channels version 3.0.0 development server at http://127.0.0.1:8000/</code>. 이 줄은 채널즈 개발 서버가 장고 개발 서버를 대체하고 있음을 알려줍니다.</p>
<p>이제 다시 브라우저를 열고 <code>http://127.0.0.1:8000/chat/</code>로 가면 아까 만든 초기 화면이 여전히 잘 나타날 겁니다. 터미널로 돌아가 <code>Control-C</code>로 서버를 중지하세요.</p>
<hr>
<p>이 튜토리얼은 <a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-2">튜토리얼 2: 채팅 서버 만들기</a>로 이어집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 2: 소개
]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Fri, 15 Jan 2021 11:54:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/introduction.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.</p>
</blockquote>
<h1 id="소개">소개</h1>
<p>채널즈에 오신 것을 환영합니다!</p>
<p>채널즈는 장고에서 기본으로 제공하는 비동기 뷰를 감싸, 장고가 HTTP뿐만 아니라 긴시간 연결돼야 하는 웹소켓, MQTT, 챗봇, 라디오 등의 프로토콜을 처리할 수 있게 해줍니다. </p>
<p>이러한 작업은 쉽게 쓸 수 있는 장고의 동기적인 방식과 양립할 수 있고, 장고 뷰를 동기적이든, 완전히 비동기적이든, 아니면 둘을 적절히 섞든, 원하는 방식으로 작성할 수 있습니다. 그 뿐만 아니라 채널즈는 장고의 인증, 세션 시스템과 통합될 수 있고, 따라서 HTTP용으로 개발된 프로젝트를 다른 프로토콜로 쉽게 확장할 수 있습니다.</p>
<p>채널즈는 또한 이러한 이벤트 주도의 구조를 <code>채널 레이어</code>를 이용해 다루는데, <code>채널 레이어</code>는 여러 프로세스 간의 통신을 용이하게 만들면서 프로젝트를 여러 개의 프로세스로 쪼갤 수 있게 해줍니다.</p>
<p>아직 채널즈를 설치하지 않으셨다면 <a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0/">설치하기</a>를 먼저 읽어주세요. 이 글은 직접적인 튜토리얼은 아니지만, 아래 내용을 따라오시면 장고 프로젝트에 변화를 줄 수 있을 것입니다.</p>
<h2 id="turtles-all-the-way-down">Turtles All The Way Down</h2>
<p>채널즈는 &quot;Turtles All The Way Down&quot;의 원리 위에서 작동합니다 - 우리는 채널즈 &quot;애플리케이션&quot;이 무엇인지에 대한 공통된 생각이 있고, 가장 단순한 컨슈머들(장고에서의 뷰와 같은 것입니다)조차도 여러분이 직접 실행 가능한 하나의 완전한 ASGI 애플리케이션입니다.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>ASGI는 채널즈가 기반을 두고 있는 &#39;비동기 서버 설명서&#39;입니다. WSGI처럼, ASGI는 저희의 <code>Channels</code>와 <code>Daphne</code> 서버만을 사용하도록 강제하지 않고, 여러 서버와 프레임워크 중에 선택할 수 있도록 설계되었습니다. 더 자세한 사항은 <a href="http://asgi.readthedocs.io">http://asgi.readthedocs.io</a>를 참조해주세요.</p>
</blockquote>
<p>컨슈머(consumers)는 채팅 메시지나 알림과 같은 것을 처리하는 독립적인 요소입니다. 채널즈는 이러한 기본적인 컨슈머를 작성하고, URL 라우팅에 연결하고, 프로토콜을 감지하는 등의 기능을 제공합니다. </p>
<p>우리는 HTTP와 기존의 장고 애플리케이션을 구성요소로 활용합니다. 전통적인 장고 뷰를 여전히 채널즈와 함께 사용할 수 있고, 거기에 더해 기존의 코드를 유지한 상태로 HTTP Long-폴링이나 웹소켓을 함께 처리할 수 있습니다. URL 라우팅, 미들웨어 모두 ASGI 애플리케이션일 뿐입니다.</p>
<p>우리는 여러분이 대부분의 경우에 장고 뷰와 같이 안전한 동기적인 방식을 사용하길 원한다고 믿습니다. 단지 조금 더 복잡한 작업을 처리할 때는 더 직접적인, 비동기 도구를 사용할 수 있도록 해드리려 합니다.</p>
<h2 id="스코프scopes와-이벤트">스코프(Scopes)와 이벤트</h2>
<p>채널즈와 ASGI는 들어오는 연결을 두개의 컴포넌트로 분리합니다: 하나는 스코프이고, 다른 하는 이벤트들의 리스트입니다.</p>
<p>스코프는 하나의 들어오는 연결에 대한 상세 설명들의 집합으로, 요청이 발생한 URL, 웹소켓이 열린 IP 주소, 아니면 유저 정보와 같은 것들로 구성되어 있으며, 연결이 종료될 때까지 유지됩니다.</p>
<p>HTTP의 경우에 스코프는 하나의 요청동안만 유지됩니다. 하지만 웹소켓의 경우에, 스코프는 소켓의 수명 전체에 걸쳐 유지됩니다(하지만 소켓이 닫히고 재연결될 때 변할 수는 있습니다). 다른 프로토콜의 경우엔 각 프로토콜의 ASGI 스펙이 어떻게 작성되었는지에 따라 다릅니다; 예를 들어, 챗봇 프로토콜은 설령 근본적으로 챗 프로토콜이 상태를 갖지 않는다고(stateless) 하더라도, 스코프가 한 유저의 봇과의 대화가 종결될 때까지 유지될 것입니다.</p>
<p>스코프의 수명 동안, 여러 이벤트들이 발생합니다. 이 이벤트들은 유저와의 상호작용을 나타냅니다. 예를 들어 HTTP 요청을 보낸다든가, 웹소켓 프레임을 전송하는 등의 일들입니다. 채널즈, 혹은 ASGI 애플리케이션은 매 스코프마다 개별 인스턴스화되고, 해당 스코프 안에서 발생하는 이벤트들을 차례로 처리합니다.</p>
<p>HTTP 예시를 들자면:</p>
<ul>
<li>유저가 HTTP 요청을 보낸다</li>
<li>HTTP의 타입 스코프를 요청 URL, 메소드, 헤더 등을 담아 만든다.</li>
<li>http.request 이벤트를 HTTP body 데이터와 함께 보낸다.</li>
<li>채널즈(또는 ASGI 애플리케이션)이 이것을 처리해 http.response 이벤트를 생성하고 브라우저에 응답하며 연결이 종료된다.</li>
<li>HTTP 요청/응답이 완결되고 스코프가 삭제된다.</li>
</ul>
<p>챗봇의 예시는 다음과 같습니다:</p>
<ul>
<li>유저가 챗봇에게 첫번째 메시지를 보낸다.</li>
<li>그러면 유저의 ID, 이름, 유저명 등이 담긴 스코프를 만든다.</li>
<li>애플리케이션이 chat.received_message 이벤트를 텍스트 데이터와 함께 받는다. 이에 꼭 응답할 필요는 없지만 원한다면 하나나 둘, 혹은 그 이상의 챗 메시지를 chat.send_message의 이벤트 형태로 전송할 수 있다.</li>
<li>유저가 챗봇에게 메시지들을 수차례 전송하면서 그만큼 chat.received_message 이벤트들이 더 생성된다.</li>
<li>제한시간이 지나거나, 애플리케이션 프로세스가 재시작되면 스코프가 닫힌다.</li>
</ul>
<p>스코프의 수명주기동안 -그것이 채팅이든, HTTP 요청이든, 소켓 연결이든- 스코프 안의 모든 이벤트를 처리하는 하나의 애플리케이션 인스턴스를 만들고, 관련된 데이터를 그 위에 유지해야 합니다. 물론 이러한 작업을 수행하는 ASGI 애플리케이션을 직접 만들 수도 있겠지만, 채널즈는 그러한 일을 해주면서 쉽게 사용이 가능한 추상화된 요소를 제공합니다. 바로 컨슈머(consumers)입니다. </p>
<h2 id="컨슈머란">컨슈머란?</h2>
<p>컨슈머는 채널즈의 기본적인 단위요소입니다. 우리는 컨슈머가 이벤트를 받아들이기(consume)때문에 컨슈머라 부릅니다(스스로 동작하는 작은 애플리케이션으로 생각하셔도 좋습니다). 요청이나 새로운 소켓이 들어오면, 채널즈는 라우팅 테이블을 찾아 - 라우팅은 뒤에서 다룹니다 - 해당 연결에 대한 적절한 컨슈머를 찾고 그 인스턴스를 하나 만들어 처리합니다.</p>
<p>이것은 장고 뷰와는 다르게, 컨슈머가 오랫동안 동작함을(long-running) 의미합니다. 컨슈머는 물론 짧은 기간 동안 동작할 수도 있지만- HTTP 요청들 역시 컨슈머에 의해 처리될 수 있으니까요 - 좀 더 긴 수명을 갖도록(스코프가 살아있는 동안 존재하도록) 설계되었습니다.</p>
<p>기본적인 컨슈머의 형태는 다음과 같습니다:</p>
<pre><code class="language-python">class ChatConsumer(WebsocketConsumer):

    def connect(self):
        self.username = &quot;Anonymous&quot;
        self.accept()
        self.send(text_data=&quot;[Welcome %s!]&quot; % self.username)

    def receive(self, *, text_data):
        if text_data.startswith(&quot;/name&quot;):
            self.username = text_data[5:].strip()
            self.send(text_data=&quot;[set your username to %s]&quot; % self.username)
        else:
            self.send(text_data=self.username + &quot;: &quot; + text_data)

    def disconnect(self, message):
        pass</code></pre>
<p>각 종류의 프로토콜에선 서로 다른 타입의 이벤트들이 발생하고, 각각의 타입은 서로 다른 메소드로 구분됩니다. 여러분이 각 이벤트를 처리하는 코드를 작성하면, 채널즈가 그들을 스케쥴링하고 병렬적으로 작동시키는 역할을 담당합니다.</p>
<p>내부적으로, 채널즈는 완전히 비동기적인 이벤트 루프를 실행합니다. 그리고 만약 위와 같이 코드를 작성하면, 동기적인 쓰레드 상에서 호출될 것입니다. 즉 아래 코드에 나오는 것처럼, 장고 ORM과 같이 다른 작업을 가로막을 수 있는 동작을 안전하게 수행할 수 있습니다:</p>
<pre><code class="language-python">class LogConsumer(WebsocketConsumer):

    def connect(self, message):
        Log.objects.create(
            type=&quot;connected&quot;,
            client=self.scope[&quot;client&quot;],
        )</code></pre>
<p>하지만, 만약 더 많은 통제권을 원하고 비동기적인 함수만 사용하고 싶다면, 다음과 같이 완전히 비동기적인 컨슈머를 만들 수도 있습니다:</p>
<pre><code class="language-python">class PingConsumer(AsyncConsumer):
    async def websocket_connect(self, message):
        await self.send({
            &quot;type&quot;: &quot;websocket.accept&quot;,
        })

    async def websocket_receive(self, message):
        await asyncio.sleep(1)
        await self.send({
            &quot;type&quot;: &quot;websocket.send&quot;,
            &quot;text&quot;: &quot;pong&quot;,
        })</code></pre>
<p>더 자세한 것은 <code>컨슈머</code> 페이지에서 확인하세요.</p>
<h2 id="라우팅-그리고-다양한-프로토콜들">라우팅, 그리고 다양한 프로토콜들</h2>
<p>라우팅을 이용하면 여러 개의 컨슈머들을(잊지 마세요, 각각의 컨슈머는 그자체로 ASGI 애플리케이션입니다) 프로젝트와 같이 하나의 거대한 앱으로 합칠 수 있습니다:</p>
<pre><code class="language-python">application = URLRouter([
    url(r&quot;^chat/admin/$&quot;, AdminChatConsumer.as_asgi()),
    url(r&quot;^chat/$&quot;, PublicChatConsumer.as_asgi(),
])</code></pre>
<p>채널즈는 HTTP와 웹소켓의 세계에서만 동작하지 않습니다 - 각 프로토콜을 비슷한 이벤트 집합에 매핑하여, 장고 환경에서 어떠한 프로토콜이든 처리할 수 있게 해줍니다. 예를 들어, 다음과 같이 유사한 방식으로 챗봇을 만들 수 있습니다:</p>
<pre><code class="language-python">class ChattyBotConsumer(SyncConsumer):

    def telegram_message(self, message):
        &quot;&quot;&quot;
        Simple echo handler for telegram messages in any chat.
        &quot;&quot;&quot;
        self.send({
            &quot;type&quot;: &quot;telegram.message&quot;,
            &quot;text&quot;: &quot;You said: %s&quot; % message[&quot;text&quot;],
        })</code></pre>
<p>그리고 다른 라우터를 함께 사용해 하나의 프로젝트가 웹소켓와 채팅 요청 둘 모두를 처리할 수 있게 해줍니다.</p>
<pre><code class="language-python">application = ProtocolTypeRouter({

    &quot;websocket&quot;: URLRouter([
        url(r&quot;^chat/admin/$&quot;, AdminChatConsumer.as_asgi()),
        url(r&quot;^chat/$&quot;, PublicChatConsumer.as_asgi()),
    ]),

    &quot;telegram&quot;: ChattyBotConsumer.as_asgi(),
})</code></pre>
<p>채널즈의 목표는 장고 프로젝트가 여러분이 모던 웹에서 마주할 수 있는 어떠한 프로토콜이나 전송방식이든, 모두 여러분이 익숙한 구성요소들이나 코딩 방식을 사용해 처리할 수 있도록 돕는 것입니다.</p>
<p>더 자세한 것이 알고 싶으시다면, <code>라우팅</code> 페이지를 참조하세요.</p>
<h2 id="프로세스간-통신">프로세스간 통신</h2>
<p>표준 WSGI 서버처럼, 여러분의 애플리케이션은 프로토콜 이벤트들을 서버 프로세스 안에서 처리합니다 - 예를 들어 웹소켓을 처리하는 코드는 웹소켓 서버 프로세스 내에서 실행됩니다.</p>
<p>여러분의 애플리케이션에 들어오는 각 소켓이나 연결은 이러한 서버 중 하나의 안에서 애플리케이션 인스턴스에 의해 처리됩니다. 그들은 호출되고, 클라이언트에게 다시 데이터를 직접 보내줄 수 있습니다.</p>
<p>하지만, 더 복잡한 애플리케이션을 만들다 보면 서로 다른 애플리케이션 인스턴스들 간의 통신이 필요한 경우가 생깁니다 - 예를 들어 채팅방을 구현하려면, 하나의 애플리케이션 인스턴스에게 전송된 메시지를 (채팅방 내의 다른 사람들을 나타내는) 다른 인스턴스들에게 전파할 필요가 있을 것입니다.</p>
<p>여러분은 물론 이것을 데이터베이스를 폴링(짧은 주기로 반복 요청을 보내는것)하여 구현할 수 있습니다. 하지만 채널즈는 <code>채널 레이어</code>라는, 전송 집합과 관련해 저수준에서 추상화한 개념을 도입해 프로세스간의 정보 교환을 가능하게 했습니다. 각각의 애플리케이션 인스턴스는 고유한 <code>채널</code>명을 갖고 있고, <code>그룹</code>에 참가할 수 있으며, 이를 통해 포인트 투 포인트 메시지 전송과 메시지 전파 모두 가능합니다. </p>
<blockquote>
<p>역자: 카톡으로 생각하면 각각의 사람(의 매 연결)이 채널이고, 그룹이 채팅방(좀 더 정확히는, 채팅방 안에서 만들어지는 관련 있는 채널의 집합), 포인트 투 포인트는 일대일 채팅(목표 채널로 메시지 전송), 메시지 전파는 채팅방 채팅(그룹 안의 모든 채널로 메시지 전송)에 대응되는 추상화 개념이라 이해하시면 좋을 것 같습니다.</p>
</blockquote>
<blockquote>
<p><strong>주의사항</strong></p>
<p>채널 레이어는 채널즈의 선택적인 기능입니다. 원하신다면 CHANNEL_LAYERS 세팅값을 빈 값으로 설정해 비활성화할 수 있습니다.</p>
</blockquote>
<p>또한 자신의 정해진 채널명에 반응하는 전용 프로세스에 메시지를 보낼 수도 있습니다:</p>
<pre><code class="language-python"># In a consumer
self.channel_layer.send(
    &quot;myproject.thumbnail_notifications&quot;,
    {
        &quot;type&quot;: &quot;thumbnail.generate&quot;,
        &quot;id&quot;: 90902949,
    },
)</code></pre>
<p>채널 레이어에 대해서는 <code>채널 레이어</code> 페이지에서 더 자세히 확인하실 수 있습니다.</p>
<h2 id="장고와의-통합">장고와의 통합</h2>
<p>채널즈는 세션, 인증과 같이 장고가 제공하는 간편한 내장 기능들을 똑같이 가지고 있습니다. 여러분은 웹소켓 뷰에 적절한 미들웨어를 덧붙이는 것만으로 인증을 구현할 수 있습니다:</p>
<pre><code class="language-python">from django.urls import re_path
from django.core.asgi import get_asgi_application

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack

application = ProtocolTypeRouter({
    &quot;http&quot;: get_asgi_application(),
    &quot;websocket&quot;: AuthMiddlewareStack(
        URLRouter([
            re_path(r&quot;^front(end)/$&quot;, consumers.AsyncChatConsumer.as_asgi()),
        ])
    ),
})</code></pre>
<p>자세한 사항은 <code>세션</code>과 <code>미들웨어</code> 페이지에서 확인하실 수 있습니다.</p>
<hr>
<p><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0/">설치하기</a>로 이어집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 3: 설치하기]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Jan 2021 16:20:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/installation.html">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다. </p>
</blockquote>
<h1 id="설치하기">설치하기</h1>
<p>채널즈는 PyPI에 등록돼있어서 다음 코드만 실행해 설치할 수 있습니다.</p>
<pre><code class="language-bash">python -m pip install -U channels</code></pre>
<p>그 후엔 <code>channels</code>를 장고 <code>setting.py</code>의 <code>INSTALLED_APPS</code>에 추가해야 합니다.</p>
<pre><code class="language-python">INSTALLED_APPS = (
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.sites&#39;,
    ...
    &#39;channels&#39;,
)</code></pre>
<p>그 다음 장고 프로젝트의 <code>asgi.py</code> 파일(e.g. `mypro을 Django ASGI 애플리케이션을 감싸도록 설정합니다.</p>
<pre><code class="language-python">import os

from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault(&#39;DJANGO_SETTINGS_MODULE&#39;, &#39;mysite.settings&#39;)

application = ProtocolTypeRouter({
    &quot;http&quot;: get_asgi_application(),
    # 지금은 HTTP만 등록합니다. (나중에 다른 프로토콜들도 등록할 거예요!)
})</code></pre>
<blockquote>
<p><strong>주의사항</strong></p>
<p>장고 v2.2는 자체 ASGI 지원이 없어 채널즈의 대체 시스템을 이용해야 합니다. <code>myproject/asgi.py</code> 파일을 이렇게 설정하세요:</p>
<pre><code class="language-python">import os

import django
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter

os.environ.setdefault(&#39;DJANGO_SETTINGS_MODULE&#39;, &#39;mysite.settings&#39;)
django.setup()

application = ProtocolTypeRouter({
   &quot;http&quot;: AsgiHandler(),
   # 지금은 HTTP만 등록합니다. (나중에 다른 프로토콜들도 등록할 거예요!)
})</code></pre>
</blockquote>
<p>마지막으로, <code>ASGI_APPLICATION</code>가 루트 애플리케이션의 라우팅 오브젝트를 가리키도록 설정하세요:</p>
<pre><code class="language-python">ASGI_APPLICATION = &quot;myproject.asgi.application&quot;</code></pre>
<p>설정이 모두 끝났습니다! 한 번 활성화되면, <code>channels</code>는 장고와 통합되어 <code>runserver</code> 커맨드를 제어하게 됩니다. 자세한 사항은 <a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%86%8C%EA%B0%9C">소개</a>를 참조해주세요.</p>
<blockquote>
<p><strong>주의사항</strong></p>
<p>다른 서드파티 앱이 <code>runserver</code> 커맨드를 오버로드하거나 대체할 경우에 주의하세요. 채널즈는 별도의 <code>runserver</code> 커맨드를 제공하기 때문에 해당 앱과 충돌할 수 있습니다. 그러한 대표적인 사례로 <code>whitenoise</code> 앱의 <code>runserver_nostatic</code>이 있습니다. 이를 해결하기 위해선 채널즈를 <code>INSTALLED_APPS</code> 설정값의 최상단으로 이동하거나, 충돌하는 다른 앱을 제거해주세요.</p>
</blockquote>
<h2 id="최신-개발-버전-설치하기">최신 개발 버전 설치하기</h2>
<p>채널즈의 최신 버전을 설치하려면, 레포를 클론한 뒤 해당 디렉토리에서 들어가, 현재 가상환경에서 pip install을 실행합니다.</p>
<pre><code class="language-bash">$ git clone git@github.com:django/channels.git
$ cd channels
$ &lt;activate your project’s virtual environment&gt;
(environment) $ pip install -e .  # 온점은 현재 레포를 가리킵니다.</code></pre>
<hr>
<p><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1/">튜토리얼</a>로 이어집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Channels] 공식문서 번역 📘 - 1: 도입]]></title>
            <link>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Thu, 14 Jan 2021 16:00:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 <a href="https://channels.readthedocs.io/en/stable/">공식 문서</a>를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다. </p>
</blockquote>
<h1 id="장고-채널즈-django-channels">장고 채널즈 (Django Channels)</h1>
<p>채널즈는 장고를 HTTP 너머로 확장해 웹소켓, 챗 프로토콜, IoT 프로토콜 등을 처리할 수 있게 해주는 프로젝트로, <a href="https://asgi.readthedocs.io/en/latest/">ASGI</a>라는 파이썬 표준을 바탕으로 만들어졌습니다.</p>
<p>채널즈는 장고에서 v3.0 이후로 기본 제공하는 ASGI를 바탕으로 만들어졌고, 장고 v2.2도 지원합니다. Channels를 이용하면 HTTP를 비롯한 여러 통신을 동기적인 방식과 비동기적인 방식 모두로 자유롭게 처리할 수 있습니다.</p>
<h2 id="프로젝트">프로젝트</h2>
<p>채널즈는 다음의 패키지들로 구성되어 있습니다.</p>
<ul>
<li><code>Channels</code> 장고 통합 레이어</li>
<li><code>Daphne</code> HTTP와 Websocket용 종단 서버(termination server)</li>
<li><code>asgiref</code> 기본 ASGI 라이브러리</li>
<li><code>channels_redis</code> Redis를 활용한 채널 레이어 백엔드 (필수는 아님)</li>
</ul>
<p>이 문서는 전체 시스템을 다룹니다; 개별 릴리즈 노트나 안내사항은 각 레포지터리를 참조해주세요.</p>
<h2 id="주제들">주제들</h2>
<ul>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%86%8C%EA%B0%9C">소개</a></li>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0">설치하기</a></li>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1">튜토리얼</a><ol>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1">기본 설정</a></li>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-2">채팅 서버 구현하기</a></li>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-3">비동기적으로 작동하는 채팅 서버 구현하기</a></li>
<li><a href="https://velog.io/@infl_veggie/Channels-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-4">자동화된 테스트 코드 짜기</a></li>
</ol>
</li>
<li>컨슈머(Consumers)</li>
<li>라우팅(Routing)</li>
<li>데이터베이스 접근</li>
<li>채널 레이어(Channel Layers)</li>
<li>세션</li>
<li>인증</li>
<li>보안</li>
<li>테스팅</li>
<li>워커(Worker)와 백그라운드 작업</li>
<li>배포</li>
</ul>
<h2 id="참조-영어-원문">참조 (영어 원문)</h2>
<ul>
<li><a href="https://channels.readthedocs.io/en/stable/asgi.html">ASGI란?</a></li>
<li><a href="https://channels.readthedocs.io/en/stable/channel_layer_spec.html">채널 레이어 설명서(Specification)</a></li>
<li><a href="https://channels.readthedocs.io/en/stable/community.html">커뮤니티 프로젝트</a></li>
<li><a href="https://channels.readthedocs.io/en/stable/contributing.html">기여하는 법</a></li>
<li><a href="https://channels.readthedocs.io/en/stable/support.html">지원</a></li>
<li><a href="https://channels.readthedocs.io/en/stable/releases/index.html">릴리즈 노트</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 컨테이너 안에서 발생하는 SSL Error: CERTIFICATE VERIFY FAILED]]></title>
            <link>https://velog.io/@infl_veggie/%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%88%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-SSL-Error</link>
            <guid>https://velog.io/@infl_veggie/%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%88%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-SSL-Error</guid>
            <pubDate>Mon, 11 Jan 2021 17:26:50 GMT</pubDate>
            <description><![CDATA[<h2 id="에러">에러</h2>
<p>도커 컨테이너 안에서 <code>urllib3</code>로 외부 HTTP 요청시 다음과 같은 SSL 에러가 발생하였다.</p>
<pre><code>urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host=&#39;어떤 호스트&#39;, port=443): 
Max retries exceeded with url: 어떤 URL (Caused by SSLError)</code></pre><pre><code>SSLCertVerificationError certificate verify failed: 
self signed certificate in certificate chain
또는
unable to get local issuer certificate</code></pre><h3 id="왜-이런-일이">왜 이런 일이..?</h3>
<p><code>urllib3</code>에서 HTTPS 요청 수행을 위해 <code>HTTPSConnectionPool</code>을 만드는 과정에서 호스트로부터 certificate 정보를 가져오는데 실패하는 문제에서 비롯되었다.</p>
<p>삽질 끝에 해결하는데 이 <a href="https://stackoverflow.com/questions/37970990/ssl-certificate-verification-fails-inside-docker-container-on-specific-server">스택오버플로우 글</a>이 큰 도움이 되었다.</p>
<hr>
<h2 id="해결법">해결법</h2>
<p>일부 도커 베이스 이미지에서는 <code>ca-certificates</code> 패키지가 구식인 경우가 있다. 이 경우 urllib3가 제대로 인증서 정보를 가져오지 못한다. 도커파일 안에 다음 구문을 추가해 최신버젼으로 업데이트하면 해결된다.</p>
<pre><code class="language-bash"># for SSL
RUN apt-get update &amp;&amp; apt-get install -y ca-certificates</code></pre>
<p>여전히 에러가 발생한다면 <code>ca-certificates</code>에 수작업으로 인증 정보(crt 파일)를 등록해야한다. <code>letsencrypt</code>를 사용하는 경우엔 발급된 <code>cert.pem</code> 파일에 해당한다. 도커를 실행하는 호스트 서버 상에서 다음 명령을 실행한다.</p>
<pre><code class="language-bash">sudo cp /etc/letsencrypt/live/{도메인명}/cert.pem /usr/local/share/ca-certificates/my-ca.crt
sudo update-ca-certificates</code></pre>
<p>다음과 같이 나타나면 성공이다.</p>
<pre><code class="language-bash">Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.</code></pre>
<hr>
<h3 id="급한-불만-끄고-싶은-경우">급한 불만 끄고 싶은 경우</h3>
<p>에러만 빨리 없애야 하는경우 다음과 같이 설정해 ssl을 조용하게 할 수 있으나 보안 문제를 야기할 수 있다. <a href="https://stackoverflow.com/a/62666855/14119702">스택오버플로우 글</a> 참조</p>
<pre><code class="language-python">import ssl
import urllib3

http = urllib3.PoolManager(cert_reqs=ssl.CERT_NONE)
http.request(...)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 랜덤 초대코드 생성하기]]></title>
            <link>https://velog.io/@infl_veggie/python-%EB%9E%9C%EB%8D%A4-%EC%B4%88%EB%8C%80%EC%BD%94%EB%93%9C-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@infl_veggie/python-%EB%9E%9C%EB%8D%A4-%EC%B4%88%EB%8C%80%EC%BD%94%EB%93%9C-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 31 Dec 2020 19:50:44 GMT</pubDate>
            <description><![CDATA[<h2 id="랜덤-코드-생성기">랜덤 코드 생성기</h2>
<p>친구 초대링크나 추천코드, 이메일 검증코드 등을 위해 <code>&#39;WUJtQT09&#39;</code> 같은 랜덤한 코드를 생성해야 할 때가 있다. 필요가 생겨 간단히 만들어 보았다.</p>
<hr>
<h2 id="코드">코드</h2>
<h3 id="python-2">Python 2</h3>
<pre><code class="language-python">import uuid
import base64

def generate_random_slug_code(length=8):
    &quot;&quot;&quot;
    generates random code of given length
    &quot;&quot;&quot;
    return base64.urlsafe_b64encode(
        uuid.uuid4().bytes.encode(&quot;base64&quot;).rstrip()
    ).decode()[:length]</code></pre>
<h3 id="python-3">Python 3</h3>
<pre><code class="language-python">import uuid
import base64
import codecs

def generate_random_slug_code(length=8):
    &quot;&quot;&quot;
    generates random code of given length
    &quot;&quot;&quot;
    return base64.urlsafe_b64encode(
        codecs.encode(uuid.uuid4().bytes, &quot;base64&quot;).rstrip()
    ).decode()[:length]</code></pre>
<blockquote>
<p>UUID.bytes의 타입이 python2(<code>str</code>)와 3(<code>bytes</code>)에서 다르기 때문에 인코딩 부분이 조금 다르다.
둘 중에 사용하는 Python 버젼에 맞추어 선택하면 된다.</p>
</blockquote>
<hr>
<h2 id="코드-뜯어보기">코드 뜯어보기</h2>
<p>매번 완전히 새롭고 중복될 여지가 적은 시드값으로 uuid4를 택했다. UUID는 다음과 같은 형식으로 랜덤하게 생성된다.</p>
<pre><code class="language-python">&gt;&gt;&gt; import uuid
&gt;&gt;&gt; uuid.uuid4()
UUID(&#39;184062bb-6ad4-403d-a2b2-5c8be8d5f613&#39;) 
# xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx </code></pre>
<p>이 값을 base64로 인코딩했다.</p>
<pre><code class="language-python">&gt;&gt;&gt; import codecs
&gt;&gt;&gt; codecs.encode(uuid.uuid4().bytes, &quot;base64&quot;).rstrip()
b&#39;mkgmmjobSa2WbKf/PfOqIw==&#39;</code></pre>
<p>랜덤 코드를 초대 링크 등 url에서 활용하는 경우가 많다. url에서도 사용 가능한 코드를 만들기 위해 base64의 urlsafe_b64encode 함수를 활용했다. 
이때 인코딩한 값은 단순 문자열이 아닌 바이트스트링 타입이다. 문자열로 활용하려면 .decode()를 붙여주면 된다.</p>
<pre><code class="language-python">&gt;&gt;&gt; import base64
&gt;&gt;&gt; base64.urlsafe_b64encode(_).decode()
&#39;bWtnbW1qb2JTYTJXYktmL1BmT3FJdz09&#39;</code></pre>
<p>이제 이 값을 원하는 길이만큼 슬라이싱해주면 된다. 단, <code>length</code> 값이 유효한 범위(<code>1-32</code>) 안에 있어야 함에 주의하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DRF] simplejwt에서 발생한 "TypeError: 'str' object is not callable"]]></title>
            <link>https://velog.io/@infl_veggie/DRF-simplejwt-TypeError-str-object-is-not-callable</link>
            <guid>https://velog.io/@infl_veggie/DRF-simplejwt-TypeError-str-object-is-not-callable</guid>
            <pubDate>Wed, 30 Dec 2020 13:59:42 GMT</pubDate>
            <description><![CDATA[<h2 id="에러">에러</h2>
<p>서버의 인증 라이브러리를 django-rest-framework-simplejwt 4.6.0 버전으로 이전하고 있던 어느 평화로운 오후에, 느닷없이 다음과 같은 에러가 등장하셨다.</p>
<pre><code class="language-bash">File &quot;/**/account/serializers.py&quot;, line 57, in validate
    if not api_settings.USER_AUTHENTICATION_RULE(self.user):
TypeError: &#39;str&#39; object is not callable</code></pre>
<hr>
<h2 id="디버깅">디버깅</h2>
<p>코드를 분해해보니 drf-simplejwt가 앱 시작시에 불러오는 세팅값 중에 유저 유효성 검사 함수를 가리키는 <code>USER_AUTHENTICATION_RULE</code>값이 <strong>함수로 올바르게 임포트되지 못하고 문자열로 세팅되는 버그</strong>가 있었다. </p>
<h3 id="왜-이런-일이">왜 이런 일이..?</h3>
<p>simplejwt는 세팅시에 <a href="https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/settings.py#L183">DRF의 <code>APISettings</code> 클래스</a>를 활용한다. 에러 메시지의 <code>api_settings</code> 역시 이 클래스(를 상속하는 <a href="https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/c684e8396f8446cc289ed73a0740ca222c85470d/rest_framework_simplejwt/settings.py#L56">클래스</a>)의 인스턴스인데, <code>api_settings.USER_AUTHENTICATION_RULE</code> 구문에서 다음 함수가 실행된다.</p>
<pre><code class="language-python">def __getattr__(self, attr):
    if attr not in self.defaults:
        raise AttributeError(&quot;Invalid API setting: &#39;%s&#39;&quot; % attr)

    try:
        # Check if present in user settings
        val = self.user_settings[attr]            [1]
    except KeyError:
        # Fall back to defaults
        val = self.defaults[attr]            [2]

    # Coerce import strings into classes
    if attr in self.import_strings:            [3]
        val = perform_import(val, attr)            [4]

    # Cache the result
    self._cached_attrs.add(attr)
    setattr(self, attr, val)
    return val</code></pre>
<ol>
<li>먼저 settings.py에서 유저가 지정한 <code>USER_AUTHENTICATION_RULE</code>값이 있는지 확인 후</li>
<li>없으면 디폴트 값인 <code>&#39;rest_framework_simplejwt.authentication.default_user_authentication_rule&#39;</code>을 가져온다. </li>
<li>만약 <strong><code>self.import_strings</code> 튜플에 <code>&#39;USER_AUTHENTICATION_RULE&#39;</code>가 있으면</strong> </li>
<li>이 값이 가리키는 함수를 임포트해 반환하게 된다.</li>
</ol>
<p>그런데 <code>self.import_strings</code>의 디폴트 값인<code>IMPORT_STRINGS</code>에 <code>&#39;USER_AUTHENTICATION_RULE&#39;</code>가 누락돼 <strong>해당 값이 가리키는 함수가 아닌 값 자체를 반환</strong>하고 있었다.
따라서 해당 변수를 호출하면 타입에러가 발생하는 것이었다. </p>
<hr>
<h2 id="해결방안">해결방안</h2>
<p>다음과 같이 해당 변수에 <code>&#39;USER_AUTHENTICATION_RULE&#39;</code> 값을 추가하면 해결된다.</p>
<pre><code class="language-python">IMPORT_STRINGS = (
    &#39;AUTH_TOKEN_CLASSES&#39;,
    &#39;TOKEN_USER_CLASS&#39;,
    &#39;USER_AUTHENTICATION_RULE&#39;,
)</code></pre>
<h3 id="">+</h3>
<p>현재 <a href="https://github.com/SimpleJWT/django-rest-framework-simplejwt">simplejwt의 깃헙레포(11/26)</a>에는 누락된 변수가 추가되어 있으나, <code>pip</code>로 설치되는 <a href="https://pypi.org/project/djangorestframework-simplejwt/">PyPI 4.6.0 버전(11/13)</a>에는 값이 누락되어 에러가 발생한다. 
언제가 나올 다음 버젼에서는 해당 문제가 수정되겠지만, 지금은 에러가 나는 <code>TokenObtainPairSerializer</code>를 커스터마이징해 <code>USER_AUTHENTICATION_RULE</code>의 디폴트 값인 <code>default_user_authentication_rule</code> 함수를 직접 호출하는 방식으로 해결하였다.</p>
<pre><code class="language-python">-  if not api_settings.USER_AUTHENTICATION_RULE(self.user):
+  if not user_authentication_rule(self.user):</code></pre>
]]></description>
        </item>
    </channel>
</rss>