Jekyll2019-11-27T14:45:05+09:00https://swarf00.github.io/feed.xml장고(Django) 핥짝 맛보기튜토리얼로 맛보는 장고(Django)의 맛. 떠먹여주진 않지만 떠먹을 수 있는 방법을 알려드립니다. 다양한 삽질의 과정을 통해서 삽질 잘 하는 방법을 터득하실 수 있습니다. 삽질 또한 때로는 테크닉이 될 수 있다는 마음으로 따라하신다면 훌륭한 삽질 전문가가 되실 겁니다. 시간이 충분하다면 맛보기에서 끝나지 않고 장고(Django) 내부구조까지 탈탈 털어내보겠습니다. 아니...터는 방법을 알려드리겠습니다.
Sehun Kimpaul-kim00@hanmail.net사용자인증(5)2019-09-06T00:00:00+09:002019-09-06T00:00:00+09:00https://swarf00.github.io/2019/09/06/associate-user<h2 id="1-사용자가-작성하는-게시글">1. 사용자가 작성하는 게시글</h2>
<p>지금까지의 게시글은 로그인만 되어 있으면 누구라도 작성하도록 되어 있습니다. 게시글에 사용자의 이름을 남겨서 작성자가 누구인지만 알게 해뒀습니다. 그런데 만약 사용자의 이름이 겹친다면 어떻해야 하나요? 또 내가 작성한 글을 나만 수정하고 싶은데 이름만 같다면 누구라도 내 글을 수정할 수 있나요? 누가 작성한 글인지에 대한 식별을 작성자가 입력한 사용자 이름으로만 구별하는 것은 좋은 방법이 아니란 것이 드러났습니다.</p>
<p>그럼 author 라는 필드를 수정해서 User 모델의 pk 값인 id 값을 저장하도록 하면 어떨까요? 그렇게 하면 실제로 저장된 사용자는 User 모델에 저장된 사람에 한정되게 됩니다. 한번 적용하고 다시 설명하도록 하죠.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="c1"># author = models.CharField('작성자', max_length=16, null=False)
</span> <span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">PositiveIntegerField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'[{}] {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</code></pre></div></div>
<p>아직 마이그레이션 하시지 마시고 일단 아래 설명을 들어보세요.<br />
이렇게 하면 실제 데이터가 게시글이 등록될 때 등록하는 사용자의 id 값이 저장되고, 해당 게시글을 수정할 때는 로그인된 사용자의 id 값을 author 와 필드와 비교해서 동일할 경우만 수정이 되도록 할 수도 있습니다. 또한 Article 모델에서 자기가 작성한 게시글만 필터링 해서 볼 수도 있겠네요. 그리고 게시글 목록에서도 author 값으로 User 테이블을 검색해서 작성자의 이름을 표시하도록 할 수 있습니다. 이렇게만 해도 충분히 원하는 기능들을 사용할 수 있을 것 같습니다. author 에 사용자의 id 값이 저장되어 있다는 것을 우리는 잘 알고 있기 때문에 실수할 일이 없겠죠. 하지만 두명이 이상이 같이 작업을 하거나, 1년이 지나고 2년이 지나서 여러 개발자들을 거쳐가면서 데이터베이스 모델이 복잡해지고 소스코드가 커지면 author가 User 모델의 id 값이라는 것을 알아차리기 어려울 수 있습니다. 그렇게 되면 실제 존재하지 않는 User의 id를 입력하거나 잘못된 id 값을 입력하는 등의 오류가 발생할 수가 있죠. 물론 소스코드에서 무결성을 유지되도록 잘 처리할 수도 있겠지만 성능적인 부분은 고려하지 않더라도 그것 또한 또하나의 문제가 발생할 수 있는 지점이 됩니다.<br />
이렇게 모델의 필드값을 다른 모델의 id로 사용할 때의 장점이 있는 반면 단점도 분명히 존재합니다. 일반적으로 RDB(관계형 데이터베이스)에서 이런 경우 모델과 모델에 제약조건을 추가하여 두 모델 간의 관계성을 데이터베이스에 주입하고, 데이터베이스가 알아서 무결성을 보장하도록 설정합니다. Foreign Key(외래키)라고 하는 것이 바로 그것인데, ForeignKey에 대한 설명은 <a href="https://namsieon.com/entry/SQL-%EC%B0%B8%EC%A1%B0%ED%82%A4-Foreign-key-%EC%99%B8%EB%9E%98%ED%82%A4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%A0%9C%EC%95%BD%EC%A1%B0%EA%B1%B4">여기</a>를 참고하시길 바랍니다.</p>
<h2 id="foreignkey외래키">ForeignKey(외래키)</h2>
<p>데이터베이스(RDB)에서는 ForeignKey 를 지정해서 테이블 간의 관계를 표현하는 것이 가능합니다. 이 ForeignKey를 장고ORM에서도 상당히 편리한 방법으로 표현할 수 있습니다. ForeignKey 라는 필드타입을 제공하는데 sql의 그 ForeignKey 를 생성하는 기능을 제공합니다. ForeignKey 필드는 연결되어지는 모델과 해당 모델이 1:N 관계가 됩니다. 당연히 하나의 Article 은 1명의 User가 작성을 할 수 있고, 1명의 User는 다수의 Article을 작성할 수 있으니 User 와 Article 은 1:N 관계가 됩니다. 이해가 안되면 닥공😏 일단 Article 모델의 author 필드를 ForeignKey 로 변경하도록 하겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="c1"># author = models.CharField('작성자', max_length=16, null=False)
</span> <span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s">'user.User'</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s">'articles'</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'[{}] {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="모델-수정">모델 수정</h3>
<p>ForeignKey 필드를 가장 전형적인 방식으로 선언을 했습니다. 첫번째 인자로 ForeignKey로 연결할 모델의 식별자를 전달합니다. 직접 모델 클래스를 전달하는 방법도 있고, 저렇게 '앱label.모델이름'으로 표현하는 방법도 있습니다. 어느 방법을 사용하셔도 동일하지만 앱이 많아지면(실무에선) 이런 방식으로 선언하는 것을 추천합니다.(소곤소곤🤫 앱끼리 모델명이 중복되는 경우가 있어서 헷갈리드라고욤)
두번째 인자 <code class="language-plaintext highlighter-rouge">related_name</code> 은 실제 데이터베이스 상에 추가되는 속성은 아니고 ForeignKey 로 설정되어지는 모델(User)에서 Article 객체를 참조해야 할 때 사용하는 속성으로 사용됩니다. 예를 들어 특정 사용자가 작성한 모든 게시글을 검색할 때 <code class="language-plaintext highlighter-rouge">user.articles.all()</code> 이라고 하면 User 모델에 정의한 적 없는 articles 라는 속성으로 추가되어 연관된 Article 모델을 검색할 수 있게 됩니다. <code class="language-plaintext highlighter-rouge">related_name</code> 이라는 속성을 추가하지 않으면 기본적으로 클래스이름(소문자)+'_set' 으로 <code class="language-plaintext highlighter-rouge">related_name</code> 이 추가됩니다. <code class="language-plaintext highlighter-rouge">related_name</code> 을 설정하지 않았다면 User 모델의 인스턴스에서 <code class="language-plaintext highlighter-rouge">user.article_set.all()</code>이라고 검색할 수 있습니다. 굳이 <code class="language-plaintext highlighter-rouge">related_name</code> 속성을 정의해주는 이유는 간혹 두 모델 사이에 ForeignKey 가 두개 이상 존재하는 경우도 있습니다. 이럴 때 자동 생성된 <code class="language-plaintext highlighter-rouge">related_name</code>이 겹쳐 오류가 생길 수 있으니 습관적으로 <code class="language-plaintext highlighter-rouge">related_name</code> 을 추가해주면 오류를 줄일 수 있습니다.
<code class="language-plaintext highlighter-rouge">on_delete</code> 속성은 ForeignKey 로 연결되는 모델(User)의 데이터가 삭제될 경우 해당 ForeignKey 필드의 값을 어떻게 할 지에 대한 설정입니다. 기본값은 <code class="language-plaintext highlighter-rouge">CASCADE</code> 입니다. 윗물이 맑으면 아랫물이 맑다는 말이 있죠. <code class="language-plaintext highlighter-rouge">CASCADE는</code> 연결된 모델(User)의 인스턴스가 삭제되면 해당 인스턴스를 ForeignKey로 연결한 Article의 인스턴스도 같이 삭제해버립니다. 만약 연결된 모델(User)의 인스턴스가 삭제되더라도 ForeignKey로 연결된 Article의 인스턴스를 삭제하지 않아야 할 경우 선택할 수 있는 방법도 있습니다. SET_NULL, SET_DEFAULT, SET, PROTECT, DO_NOTHING 입니다. 친절하게도 <a href="https://docs.djangoproject.com/ko/2.2/ref/models/fields/#foreignkey">문서</a>에 자세한 설명이 나와있으니 확인해보세요. 번역기 돌려보시고 이해가 가지 않으면 댓글을 남겨주세요😂</p>
<p>이제 실제 migration 해보겠습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./manage.py makemigrations
<span class="nv">$ </span>./manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, bbs, contenttypes, sessions
Running migrations:
Applying auth.0010_alter_group_name_max_length...Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
File <span class="s2">"./manage.py"</span>, line 15, <span class="k">in</span> <module>
execute_from_command_line<span class="o">(</span>sys.argv<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/__init__.py"</span>, line 381, <span class="k">in </span>execute_from_command_line
utility.execute<span class="o">()</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/__init__.py"</span>, line 375, <span class="k">in </span>execute
self.fetch_command<span class="o">(</span>subcommand<span class="o">)</span>.run_from_argv<span class="o">(</span>self.argv<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/base.py"</span>, line 323, <span class="k">in </span>run_from_argv
self.execute<span class="o">(</span><span class="k">*</span>args, <span class="k">**</span>cmd_options<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/base.py"</span>, line 364, <span class="k">in </span>execute
output <span class="o">=</span> self.handle<span class="o">(</span><span class="k">*</span>args, <span class="k">**</span>options<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/base.py"</span>, line 83, <span class="k">in </span>wrapped
res <span class="o">=</span> handle_func<span class="o">(</span><span class="k">*</span>args, <span class="k">**</span>kwargs<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/core/management/commands/migrate.py"</span>, line 234, <span class="k">in </span>handle
<span class="nv">fake_initial</span><span class="o">=</span>fake_initial,
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/db/migrations/executor.py"</span>, line 117, <span class="k">in </span>migrate
state <span class="o">=</span> self._migrate_all_forwards<span class="o">(</span>state, plan, full_plan, <span class="nv">fake</span><span class="o">=</span>fake, <span class="nv">fake_initial</span><span class="o">=</span>fake_initial<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/db/migrations/executor.py"</span>, line 147, <span class="k">in </span>_migrate_all_forwards
state <span class="o">=</span> self.apply_migration<span class="o">(</span>state, migration, <span class="nv">fake</span><span class="o">=</span>fake, <span class="nv">fake_initial</span><span class="o">=</span>fake_initial<span class="o">)</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/db/migrations/executor.py"</span>, line 247, <span class="k">in </span>apply_migration
migration_recorded <span class="o">=</span> True
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/db/backends/sqlite3/schema.py"</span>, line 34, <span class="k">in </span>__exit__
self.connection.check_constraints<span class="o">()</span>
File <span class="s2">"/Users/sehunkim/minitutorial/venv/lib/python3.7/site-packages/django/db/backends/sqlite3/base.py"</span>, line 318, <span class="k">in </span>check_constraints
bad_value, referenced_table_name, referenced_column_name
django.db.utils.IntegrityError: The row <span class="k">in </span>table <span class="s1">'bbs_article'</span> with primary key <span class="s1">'1'</span> has an invalid foreign key: bbs_article.author_id contains a value <span class="s1">'swarf00'</span> that does not have a corresponding value <span class="k">in </span>user_user.id.
</code></pre></div></div>
<p>이제는 익숙하시겠죠. 역시나 오류가 발생합니다. 침착하게 오류메시지를 살펴봅니다. ForeignKey로 참조하는 User 모델의 필드가 <code class="language-plaintext highlighter-rouge">User.id</code>(int) 인데 ForeignKey 에 저장된 값이 swarf00(str) 이라 무결성이 맞지 않아 마이그레이션이 되지 않는다고 합니다. ForeignKey 필드로 설정된 필드에 이미 문자열 데이터가 들어가 있으니 User 모델의 <code class="language-plaintext highlighter-rouge">id</code> 의 자료형이 맞지 않아 오류가 발생하는 겁니다. 이럴 때 깨끗이 데이터를 날리고 시작하면 깔끔하고 편한데 실무에서는 책상도 깨끗하게 날라가는 겁니다.(데이터베이스 작업을 할 때는 꼭 백업을 해둡시다)</p>
<p>일단 데이터베이스가 변경되었는지 오류로 인해 롤백이 되었는 지 확인해봅니다.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">></span> <span class="n">pragma</span> <span class="n">table_info</span><span class="p">(</span><span class="n">bbs_article</span><span class="p">);</span>
<span class="mi">0</span><span class="o">|</span><span class="n">id</span><span class="o">|</span><span class="nb">integer</span><span class="o">|</span><span class="mi">1</span><span class="o">||</span><span class="mi">1</span>
<span class="mi">1</span><span class="o">|</span><span class="n">title</span><span class="o">|</span><span class="nb">varchar</span><span class="p">(</span><span class="mi">126</span><span class="p">)</span><span class="o">|</span><span class="mi">1</span><span class="o">||</span><span class="mi">0</span>
<span class="mi">2</span><span class="o">|</span><span class="n">content</span><span class="o">|</span><span class="nb">text</span><span class="o">|</span><span class="mi">1</span><span class="o">||</span><span class="mi">0</span>
<span class="mi">3</span><span class="o">|</span><span class="n">author_id</span><span class="o">|</span><span class="nb">integer</span><span class="o">|</span><span class="mi">1</span><span class="o">||</span><span class="mi">0</span>
<span class="mi">4</span><span class="o">|</span><span class="n">created_at</span><span class="o">|</span><span class="nb">datetime</span><span class="o">|</span><span class="mi">1</span><span class="o">||</span><span class="mi">0</span>
</code></pre></div></div>
<p>일단 author 필드가 author_id 필드로 변경되었고 자료형이 integer로 변경된 것이 확인됩니다. ForeignKey 필드 이름로 author 를 사용했는데 실제 데이터베이스에 <code class="language-plaintext highlighter-rouge">author_id</code> 로 필드명이 변경된 이유는 ForeignKey 필드의 또다른 속성 <code class="language-plaintext highlighter-rouge">to_field</code> 값을 설정하지 않아 기본값인 참조하는 모델의 pk 필드인 id로 설정이 된 것 입니다. 잘 사용하지 않는 속성이니 대충 넘어갑니다.</p>
<p>어쨋든 데이터베이스가 잘 마이그레이션되었으니 <code class="language-plaintext highlighter-rouge">author_id</code> 의 값에 적절한 값을 넣어 수정합니다. 강제로 <code class="language-plaintext highlighter-rouge">user_id</code> 에 1을 설정하도록 하겠습니다. 1이 아니라 User 테이블에 있는 어떠한 id 값이라도 상관없습니다.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">></span> <span class="k">update</span> <span class="n">bbs_article</span> <span class="k">set</span> <span class="n">author_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">shell</code> 커맨드를 실행하여 ForeignKey 가 잘 작동되는 지 몇가지 테스트를 해보겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">>>></span> <span class="kn">from</span> <span class="nn">bbs.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="o">>>></span> <span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">author</span>
<span class="o"><</span><span class="n">User</span><span class="p">:</span> <span class="n">swarf00</span><span class="o">@</span><span class="n">gmail</span><span class="o">.</span><span class="n">com</span><span class="o">></span>
<span class="o">>>></span> <span class="n">user</span> <span class="o">=</span> <span class="n">article</span><span class="o">.</span><span class="n">author</span>
<span class="o">>>></span> <span class="n">user</span><span class="o">.</span><span class="n">articles</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="o"><</span><span class="n">QuerySet</span> <span class="p">[</span><span class="o"><</span><span class="n">Article</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="n">How</span> <span class="n">to</span> <span class="n">create</span> <span class="n">a</span> <span class="n">article</span><span class="o">></span><span class="p">,</span> <span class="o"><</span><span class="n">Article</span><span class="p">:</span> <span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="n">foo</span><span class="o">></span><span class="p">,</span> <span class="o"><</span><span class="n">Article</span><span class="p">:</span> <span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="err">새로운</span> <span class="err">게시글</span><span class="o">></span><span class="p">]</span><span class="o">></span>
</code></pre></div></div>
<blockquote>
<p>실제 사용중인 데이터베이스에서는 이런식으로 ForeignKey를 변경하는 경우는 없다고 생각하셔야 합니다. 정확하게는 말씀드리면 이렇게 변경하도록 설계를 하면 안됩니다. 예제를 위해 일부러(진짜로🤣) 이렇게 설계한 것 입니다. 이런 경우 일반적으로 ForeignKey를 제대로 설정하지 못해 버리게 되는 데이터가 대량으로 발생할 수 있습니다.</p>
</blockquote>
<h3 id="뷰-수정">뷰 수정</h3>
<p>실제 ForeignKey 연결이 잘된 것이 확인되었습니다. 이제 뷰와 템플릿을 순차적으로 수정해 나가겠습니다. 지금까지는 author가 문자열였지만 이제는 ForeignKey 즉 숫자형이기 때문에 문자열이 저장이 되거나 문자열로 처리를 할 경우 오류가 발생할 수 있습니다. 먼저 뷰부터 수정을 해보도록 하죠.
현재 뷰는 3가지가 있죠. <code class="language-plaintext highlighter-rouge">ArticleListView</code>, <code class="language-plaintext highlighter-rouge">ArticleDetailView</code>, <code class="language-plaintext highlighter-rouge">ArticleCreateUpdateView</code> 이 세가지가 있는데 author 를 참조하는 건 다행히 <code class="language-plaintext highlighter-rouge">ArticleCreateUpdateView</code> 뿐 입니다.(이것 역시 나의 Big Picture!!). POST 로 입력받은 author를 처리하지 않도록 수정하고, 저장할 때 로그인한 사용자의 인스턴스를 article.author 에 저장해주도록 수정합니다. 또 한가지 더 해야 할 것이 있는데, 작성된 글을 수정할 때는 작성자가 수정하려는 사용자와 동일한 사용자인지 확인하는 절차를 거쳐야 합니다. 아래와 같이 ~복붙~수정해 봅니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">):</span>
<span class="n">login_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_URL</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_update.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">pk</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid pk'</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">article</span><span class="o">.</span><span class="n">author</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">:</span> <span class="c1"># 작성자가 수정하려는 사용자와 다른 경우
</span> <span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid user'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">article</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="n">article</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'action'</span><span class="p">)</span>
<span class="n">post_data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">)}</span> <span class="c1"># 작성자를 입력받지 않도록 수정
</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">post_data</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">post_data</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'{} 값이 존재하지 않습니다.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">key</span><span class="p">),</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="n">post_data</span><span class="p">[</span><span class="s">'author'</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span> <span class="c1"># 작성자를 현재 사용자로 설정
</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">messages</span><span class="o">.</span><span class="n">get_messages</span><span class="p">(</span><span class="n">request</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'create'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="o">**</span><span class="n">post_data</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'게시글이 저장되었습니다.'</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">post_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'게시글이 저장되었습니다.'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'알 수 없는 요청입니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="s">'/article/'</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span> <span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span> <span class="k">else</span> <span class="bp">None</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>실제로 뷰는 이렇게만 해도 충분해 보입니다. 실제로 테스트로 작성된 글을 수정하기도 하고 새로운 글을 작성해보면 의도한 대로 동작되는 것이 확인됩니다. 아직 템플릿을 수정하지 않은 상태이기 때무에 작성자 입력란에 어떠한 텍스트를 입력하거나 공백으로 두어도 정상적으로 로그인한 사용자로 게시글의 작성자가 저장되는 것을 확인할 수 있습니다. 또한 작성자와 로그인한 사용자가 다를 경우 오류 페이지를 보여주게 되면 성공입니다.</p>
<h3 id="템플릿-수정">템플릿 수정</h3>
<p>이제 템플릿을 변경할 차례입니다. 크게 두가지를 수정하면 됩니다. 다행히 User 모델에 <code class="language-plaintext highlighter-rouge">__str__</code> 함수가 미리 정의되어 있고, <code class="language-plaintext highlighter-rouge">article.author</code> 라고 출력하더라도 이메일만 출력이 되어 기존의 <code class="language-plaintext highlighter-rouge">article.author</code> 템플릿변수를 수정할 필요는 없습니다. 그렇지 않다면 <code class="language-plaintext highlighter-rouge">article.author</code> 를 출력할 경우 <code class="language-plaintext highlighter-rouge"><User: swarf00@gmail.com></code> 식으로 출력이 되어 보기에 좀 불편하기 때문에 <code class="language-plaintext highlighter-rouge">article.author.email</code> 이라고 변경해줘야 합니다. 템플릿에서 해야 할 일은 단지 article_update.html 에서 작성자 입력란을 삭제하는 것 입니다. 이건 딱히 아래 수정된 코드를 보지 않아도 척척 잘 하시겠지만 복붙하시는 분들이 아직도 많이 계시리라 짐작되기 때문에 친절하게 수정된 코드를 올려드립니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_update.html --></span>
{% extends 'base.html' %}
{% block title %}
{% if article %}
<span class="nt"><title></span>게시글 수정 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>
{% else %}
<span class="nt"><title></span>게시글 작성<span class="nt"></title></span>
{% endif %}
{% endblock title %}
{% block content %}
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">class=</span><span class="s">"form-horizontal"</span><span class="nt">></span>
{% csrf_token %}
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"action"</span> <span class="na">value=</span><span class="s">"{% if article %}update{% else %}create{% endif %}"</span><span class="nt">></span>
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-striped table-bordered"</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>번호<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>제목<span class="nt"></th></span>
<span class="nt"><td><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"title"</span> <span class="na">value=</span><span class="s">"{{ article.title }}"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>내용<span class="nt"></th></span>
<span class="nt"><td><textarea</span> <span class="na">rows=</span><span class="s">"10"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"content"</span><span class="nt">></span>{{ article.content }}<span class="nt"></textarea></td></span>
<span class="nt"></tr></span>
<span class="c"><!--<tr>--></span>
<span class="c"><!--<th>작성자</th>--></span>
<span class="c"><!--<td><input type="text" class="form-control" name="author" value="{{ article.author }}"></td>--></span>
<span class="c"><!--</tr>--></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성일<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.created_at | date:"Y-m-d H:i" }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>게시글 저장<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre></div></div>
<p>첨부터 모델 설명할 때 ForeignKey로 설명하면 되는데 굳이~ 첨부터 돌아돌아 여기까지 오도록 삽질을 끌어내, 여러분들의 값진 피와 땀으로 성장을 보게되어 뿌듯합니다. 이 정도로 가볍게 핥짝거리는 걸로 만족하지 못하시는 ~변태~분들은 다음 포스트를 기다려주세요.</p>
<blockquote>
<p>Talk is cheap. Show me the code.(말은 쉽지. 코드를 보여줘.)</p>
<p>Linus Torvalds</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고ORM 의 ForeignKey 사용법을 배워봅니다. ForeignKey는 다소 많은 옵션을 제공하지만 자주 사용되는 옵션은 몇가지 되지 않으니 소개해드리는 것만은 꼭 기억하시길 바랍니다.사용자인증(4)2018-12-19T00:00:00+09:002018-12-19T00:00:00+09:00https://swarf00.github.io/2018/12/19/social-login<h2 id="1-소셜로그인">1. 소셜로그인</h2>
<p>앞서 인증기능을 구현하는 방법에 대해 살펴봤는데 로컬에서 회원가입과 로그인은 사용자에게 불편함과 불안함을 가져다 주기도 합니다. 여러분은 사용자의 정보를 안전하게 관리하고 있더라도 사용자는 그것을 알 수 없기에 자기 자신의 정보를 여러분의 서비스에 저장하는 것에 부담감이 있을 수 있습니다. 게다가 모바일로 접속한 사용자라면 이메일 주소 하나만 입력하더라도 피곤하다고 느낄 것이고 모바일의 터치키보드 떄문에 오타로 인한 스트레스를 받을 수 있기 때문에 좀 더 빠르고 편한 인증 방식을 제공해주는 것이 좋습니다.<br />
인증이 반드시 필요한 서비스라면 이메일 기반의 로컬인증 시스템만 제공하는 것은 사용자 입장에서는 난감할 수 있습니다. 이럴 때 이미 많은 <strong>사용자들에게 신뢰를 받고 있으며 충분히 많은 사용자를 보유한 서비스에게 인증기능을 위임해서 인증결과만 받아볼 수 있는 방법</strong>이 있습니다. <code class="language-plaintext highlighter-rouge">간편로그인</code>이라고 부르기도 하고 <code class="language-plaintext highlighter-rouge">소셜로그인</code>이라고도 부르기도 하는 기능인데 이름은 어찌되었든 사용자와 개발자 입장에서 간편하고 안전하게 인증을 할 수 있는 방법입니다. 이성적으로 간편로그인이라는 용어가 맞을 것 같은데 장고 커뮤니티에서 주로 소셜로그인이라 부르기 때문에 여기서도 소셜로그인이라고 하겠습니다.</p>
<h3 id="oauth-소개">oauth 소개</h3>
<p>소셜로그인은 <code class="language-plaintext highlighter-rouge">oauth</code> 라는 인증 프로토콜을 구현한 api를 외부에 공개해서 <strong>누구라도<code class="language-plaintext highlighter-rouge">인증(authentication)</code>과 <code class="language-plaintext highlighter-rouge">권한허가(authorization)</code>를 사용할 수 있도록 제공하는 api</strong> 입니다. 현재 oauth는 2.0 버전까지 나와 있지만 소셜로그인 api 제공 서비스마다 구현된 프로토콜이 다양합니다. 국내보다 해외에서 oauth 2.0 버전을 지원하는 곳이 더 많습니다. 아무래도 1.0 버전의 보안 취약성을 개선한 2.0 버전을 선호하기 때문인데 국내 서비스들도 2.0 버전을 지원하는 사례들이 많아지고 있습니다.</p>
<p>oauth 2.0 버전이 내용을 이해하기는 그리 어렵지 않습니다. api 제공해야 하는 프로바이더(authorization server or resource server) 입장에서는 복잡하지만 각 프로바이더들이 제공하는 api를 사용하는 여러분의 서비스(client) 입장에서는 몇 가지만 구현해주면 됩니다. <strong>먼저 oauth 2.0 에 대해 잘 정리된 <a href="https://meetup.toast.com/posts/105">문서</a>를 읽어 보시고</strong> 아래 내용을 따라하시길 바랍니다.<br />
한가지 소셜로그인을 구현해 보면 나머지 서비스들도 비슷하게 구현해보실 수 있을 겁니다. 먼저 우라나라에서 가장 많은 사용자를 보유한 네이버 로그인 api로 소셜로그인 기능을 구현해보도록 하겠습니다.</p>
<h3 id="네이버-소셜로그인-네아로-앱-등록">네이버 소셜로그인 [네아로] 앱 등록</h3>
<p><a href="https://django-allauth.readthedocs.io/en/latest/">django-allauth</a> 에서는 네이버, 카카오 등 다른 여러 서비스들에 대한 소셜로그인 기능을 제공하는데 빠르고 편하게 여러 소셜로그인 기능을 제공하고 싶으신 분들이 이 라이브러리를 사용하시고 소셜로그인의 구현방법을 좀 더 자세히 알고 싶으신 분들은 여기에 나온 방식을 따라하면서 공부해보실 수 있습니다.</p>
<blockquote>
<p>django-aullauth 는 설정을 database(admin 사이트) 로 합니다. 이렇게 하면 다양한 프로바이더를 각각 설정하기에 용이한 면이 있습니다. 여기에서는 네이버의 소셜로그인만 구현하기 때문에 설정파일에 설정하는 것으로 구현했습니다. 여러분의 편의에 따라 설정값을 데이터베이스에 저장하도록 변경하셔도 좋습니다.</p>
</blockquote>
<p>먼저 네이버 소셜로그인을 사용하려면 사용권한을 얻어야 합니다. 네이버 <a href="https://developers.naver.com/apps/#/register">개발자센터</a>에서 여러분의 앱을 등록하고 몇가지 설정을 하면 사용허가가 됩니다.</p>
<ol>
<li>애플리케이션 이름 입력</li>
<li>사용 API 선택 - 네아로 (네이버 아이디로 로그인)</li>
<li>필수권한 선택 - 회원이름, 이메일 (그 외에 여러분이 필요한 것들)</li>
<li>서비스 환경 추가 - PC 웹</li>
<li>서비스 URL 입력 - http://localhost:8000</li>
<li>Callback URL 입력 - http://localhost:8000/user/login/social/naver/callback/</li>
<li>등록하기 버튼 클릭</li>
</ol>
<p><img src="https://swarf00.github.io/snapshots/naver_app_register_01.png" alt="네이버 앱등록 01" /></p>
<p>사용 API 와 서비스 환경 추가는 원하는 것들을 선택할 때마다 추가가 됩니다. 여기에서 설명하는 부분은 네이버 소셜로그인 이므로 네아로만 추가하셔도 됩니다. 서비스 URL 과 Callback URL 의 도메인은 현재 개발환경이기 때문에 localhost:8000로 설정했습니다. 여러분의 도메인이 있고 해당 <strong>도메인으로 연결된 서버에 배포할 때 반드시 수정</strong>해주셔야 합니다. Callback URL 은 <code class="language-plaintext highlighter-rouge">input</code> 칸 옆에 있는 + 버튼을 누르셔서 두개 이상의 URL을 입력가능합니다. 나중에 장고앱에서 로그인을 연동할 때 반드시 여기에 등록된 Callback URL 중에 하나를 사용해야 됩니다.</p>
<p><img src="https://swarf00.github.io/snapshots/naver_app_register_02.png" alt="네이버 앱등록 02" /></p>
<blockquote>
<p>Callback URL 을 여러개 등록하면 케이스마다 네이버 로그인 이후의 처리방식을 달리 할 수 있습니다. 예를 들어 네이버 javascript sdk를 사용하면 브라우저에서 편리하게 access_token 을 받아올 수 있습니다. 이렇게 되면 서버에서 access_token에 대해서 검증을 할 필요가 없게 됩니다(사실은 검증할 수 없습니다). 여기서는 네이버 sdk를 사용하지 않고 서버에서 인증처리를 하도록 했습니다. 경우에 따라서 두 가지 방식 모두 제공해야 한다면 각 케이스마다 Callback URL 분리해서 사용하는 것도 좋은 방법입니다.</p>
</blockquote>
<p>앱이 잘 등록이 되었다면 <a href="https://developers.naver.com/apps/#/list">앱 목록</a>에서 여러분이 등록한 앱을 선택해서 앱 정보를 확인합니다. Client ID 와 Client Secret 두가지를 복사해서 설정파일에 설정합니다. Client Secret 는 보기 버튼을 클릭하셔야 내용을 볼 수 있습니다. <strong>Client Secret 는 소스코드 외에는 복사를 하셔도 안 되고 외부에 노출되지 않도록 주의</strong>하셔야 합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/naver_app_register_03.png" alt="네이버 앱등록 03" /></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">NAVER_CLIENT_ID</span> <span class="o">=</span> <span class="s">'your client id'</span>
<span class="n">NAVER_SECRET_KEY</span> <span class="o">=</span> <span class="s">'your secret key'</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<blockquote>
<p>공개된 git repository에 관리할 경우 설정파일에 민감한 내용은 푸시되지 않도록 주의하세요.</p>
</blockquote>
<p>네이버 로그인에 사용되는 버튼이미지도 <a href="https://developers.naver.com/inc/devcenter/downloads/naveridro/2014_Login_with_NAVER_button_png.zip">네이버</a>에서 제공하고 있습니다. sdk 를 사용하신다면 필요없겠지만 여기에서는 sdk를 사용하지 않으므로 로그인버튼을 만들어줘야 합니다(css로 잘 디자인 하셔도 됩니다.). 따로 로그인버튼을 만들기 어려운 상황이라면 <a href="https://developers.naver.com/inc/devcenter/downloads/naveridro/2014_Login_with_NAVER_button_png.zip">이걸</a> 이용하셔도 됩니다. 다운로드해서 압축을 풀어보면 다양한 버튼이미지들이 있습니다. 저는 완성형 버튼을 사용할 예정인데 평상시에는 녹색이었다가 hover 상태가 되면 흰색버튼으로 바뀌게 할 예정입니다.<code class="language-plaintext highlighter-rouge">네이버 아이디로 로그인_완성형_Green.PNG</code>과 <code class="language-plaintext highlighter-rouge">네이버 아이디로 로그인_완성형_White.PNG</code> 두 이미지를 사용할 것인데 파일이름에 한글과 공백문자가 포함되어 이름을 변경할 것을 추천합니다. 각각 파일이름을 <code class="language-plaintext highlighter-rouge">naver_login_green.png</code>, <code class="language-plaintext highlighter-rouge">naver_login_white.png</code> 으로 변경해서 user/static/user/img 디렉토리에 저장합니다.</p>
<h3 id="네이버-소셜로그인-템플릿-생성">네이버 소셜로그인 템플릿 생성</h3>
<p>여기서는 뷰보다 먼저 템플릿을 만들 것입니다. 기존 로그인 기능과 함께 소셜로그인을 제공할 예정이어서 <code class="language-plaintext highlighter-rouge">login_form.html</code> 의 content 블록 상단에 <code class="language-plaintext highlighter-rouge">include</code> 템플릿태그를 추가합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/login_form.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<span class="nt"><title></span>로그인<span class="nt"></title></span>{% endblock %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static 'user/css/user.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
{% include 'user/partials/social_login_panel.html' %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
로그인하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>로그인하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/resend_verify_email/"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"link-below-button"</span><span class="nt">></span>인증이메일 재발송<span class="nt"></div></span>
<span class="nt"></a></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p>기존 로그인과 동일한 화면에서 보여지지만 분리해두는게 소스코드를 보기에도 좋고, 로그인 외의 다른 화면에서도 사용하기에 좋습니다. 나중에 가입하기 화면에서도 소셜로그인 기능을 추가할 건데 동일하게 <code class="language-plaintext highlighter-rouge">include</code> 템플릿태그만 추가하면 됩니다. <code class="language-plaintext highlighter-rouge">social_login_panel.html</code> 파일을 하나 만드셔서 아래와 같이 추가합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/partials/social_login_panel.html --></span>
{% load static %}
{% static 'user/img/kakao_login.png' as kakao_button %}
{% static 'user/img/kakao_login_ov.png' as kakao_button_hover %}
{% static 'user/img/naver_login_green.png' as naver_button %}
{% static 'user/img/naver_login_white.png' as naver_button_hover %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
소셜로그인
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body text-center"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pull-left"</span><span class="nt">></span>
<span class="nt"><a></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"{{ kakao_button }}"</span>
<span class="na">onmouseover=</span><span class="s">"this.src='{{ kakao_button_hover }}'"</span>
<span class="na">onmouseleave=</span><span class="s">"this.src='{{ kakao_button }}'"</span><span class="na">height=</span><span class="s">"34"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pull-right"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">onclick=</span><span class="s">"naverLogin()"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"{{ naver_button }}"</span>
<span class="na">onmouseover=</span><span class="s">"this.src='{{ naver_button_hover }}'"</span>
<span class="na">onmouseleave=</span><span class="s">"this.src='{{ naver_button }}'"</span><span class="na">height=</span><span class="s">"34"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>먼저 상단에 <code class="language-plaintext highlighter-rouge">static</code> 템플릿태그의 렌더링 된 문자열을 as 변수명 으로 변수에 저장을 하고 이후 img 태그의 <code class="language-plaintext highlighter-rouge">src</code> 속성에는 이 변수를 사용했습니다. <code class="language-plaintext highlighter-rouge">static</code> 템플릿태그가 반복되는 경우에는 이렇게 변수에 저장하면 좀 더 깔끔하게 코드를 관리할 수 있습니다. 아까 저장한 네이버 버튼을 화면에 출력해보니 빈 공간이 너무 많이 남아서 카카오 소셜로그인 버튼도 추가했습니다. 카카오 로그인 버튼은 <a href="https://developers.kakao.com/assets/img/about/logos/login/kr/kakao_account_login_btn_medium_narrow.zip">여기</a> 에서 다운로드 하시고 user/static/user/img 디렉토리에 각각<code class="language-plaintext highlighter-rouge"> kakao_login.png</code>, <code class="language-plaintext highlighter-rouge">kakao_login_ov.png</code> 라는 이름으로 저장하시면 됩니다.</p>
<p>각 로그인 버튼의 <code class="language-plaintext highlighter-rouge">src</code>, <code class="language-plaintext highlighter-rouge">onmouseover</code>, <code class="language-plaintext highlighter-rouge">onmouseleave</code> 속성값을 설명해 드리면 src 는 img 의 기본 이미지 url 입니다. 마우스가 버튼 위에 올라가면 <code class="language-plaintext highlighter-rouge">onmouseover</code>에 등록된 스크립트가 실행되어 이미지의 <code class="language-plaintext highlighter-rouge">src</code> 를 변경하게 되어 있습니다. 마우스가 버튼에서 벗어나면 <code class="language-plaintext highlighter-rouge">onmouseleave</code>에 등록된 스크립트가 실행되어 원래의 이미지로 변경이 됩니다.</p>
<blockquote>
<p>여기서는 카카오 소셜로그인은 구현하지 않을 예정이지만 네이버 소셜로그인을 배우시고 개인적으로 구현해보시길 권합니다.</p>
</blockquote>
<p>네이버 소셜로그인 버튼이미지가 <code class="language-plaintext highlighter-rouge">a</code> 태그로 감싸있고 <code class="language-plaintext highlighter-rouge">onclick</code> 속성에 <code class="language-plaintext highlighter-rouge">naverLogin()</code> 를 호출하도록 바인드했습니다. <strong><code class="language-plaintext highlighter-rouge">naverLogin</code> 함수는 네이버 인증페이지로 화면을 이동시켜 사용자롤 로그인하고, 사용자로부터 사용권한을 제공</strong>받도록 하는 기능을 합니다. <code class="language-plaintext highlighter-rouge">naverLogin</code> 함수 구현에 앞서 <a href="https://developers.naver.com/docs/login/api/">네이버 기술문서</a>를 먼저 읽어보시길 바랍니다.</p>
<p><img src="https://swarf00.github.io/snapshots/naver_social_login_01.png" alt="네이버 소셜로그인 01" /></p>
<p>이정도만 해도 그럴싸한 소셜로그인 UI 가 완성되었습니다. 여기서는 소셜로그인을 로컬로그인(이메일)보다 위에 위치하도록 했습니다. UX에서 보통 더 선호하는 기능이 있을 때 덜 중요한 요소보다 위쪽에 위치하도록 합니다. 여기서도 소셜로그인을 장려하는 마음으로 상단에 위치시켰으니 혹시 여러분은 다른 생각이 있으시다면 위치를 바꾸셔도 상관없습니다.</p>
<h3 id="네이버-로그인-javascript-구현">네이버 로그인 javascript 구현</h3>
<p>아까 보신대로 네이버 소셜로그인을 하기 위해서는 javascript 코드도 약간 필요합니다. 이번에 구현할 javascript 는 서버에서 렌더링할 부분이 없기 때문에 static 디렉토리에 파일을 분리합니다. 우선 <code class="language-plaintext highlighter-rouge">social_login_panel.html</code> 파일 하단에 <code class="language-plaintext highlighter-rouge">script</code> 태그를 추가합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/partials/social_login_panel.html --></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><script </span><span class="na">src=</span><span class="s">"{% static 'user/js/social_login.js' %}"</span><span class="nt">></script></span>
</code></pre></div></div>
<p>그리고 <code class="language-plaintext highlighter-rouge">user/static/user/js/social_login.js</code> 파일을 하나 생성하시고 아래 코드를 추가해주세요.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1">// user/static/user/js/social_login.js</span>
<span class="kd">function</span> <span class="nx">buildQuery</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">params</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="nx">key</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">=</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">params</span><span class="p">[</span><span class="nx">key</span><span class="p">])}).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">&</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">buildUrl</span><span class="p">(</span><span class="nx">baseUrl</span><span class="p">,</span> <span class="nx">queries</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">baseUrl</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">?</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">buildQuery</span><span class="p">(</span><span class="nx">queries</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">naverLogin</span><span class="p">()</span> <span class="p">{</span> <span class="c1">// 네이버 로그인</span>
<span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">response_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">code</span><span class="dl">'</span><span class="p">,</span>
<span class="na">client_id</span><span class="p">:</span><span class="dl">'</span><span class="s1">nfenn0pzKTlihOzu_h8S</span><span class="dl">'</span><span class="p">,</span>
<span class="na">redirect_uri</span><span class="p">:</span> <span class="nx">location</span><span class="p">.</span><span class="nx">origin</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/user/login/social/naver/callback/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">state</span><span class="p">:</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">[name=csrfmiddlewaretoken]</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span>
<span class="p">}</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">buildUrl</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://nid.naver.com/oauth2.0/authorize</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">)</span>
<span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>설명드려야 할 부분은 <code class="language-plaintext highlighter-rouge">naverLogin</code> 함수에서 <code class="language-plaintext highlighter-rouge">params</code> 객체의 각 값들입니다.<br />
먼저 <code class="language-plaintext highlighter-rouge">reponse_type</code> 은 항상 <code class="language-plaintext highlighter-rouge">code</code> 입니다. 네이버 sdk 에서는 <code class="language-plaintext highlighter-rouge">token</code> 으로 설정되어 있는데 이렇게 하면 어러분의 서버에서 인증토큰을 받는 것이 아니라 브라우저에서 전달하는 인증토큰을 사용해야 하기 때문에 보안에 취약할 수 있습니다. 즉 이 방식으로 전달받은 것들로 사용자 가입을 한다면 악의적으로 다른 사람의 <code class="language-plaintext highlighter-rouge">access_token</code> 을 도용해도 확인할 수 있는 방법이 없습니다. 여러분이 소셜로그인된 사용자의 회원정보를 서버에 저장하지 않는 경우에 적합한 방식입니다. 여기서는 <strong>네이버 소셜로그인을 하고 회원정보를 이용해서 서버에 회원을 가입시키고 회원정보를 저장할 것이기 때문에 <code class="language-plaintext highlighter-rouge">code</code> 타입으로 설정</strong>하셔야 합니다.<br />
<code class="language-plaintext highlighter-rouge">client_id</code> 는 아까 앱등록하고 발급받은 <code class="language-plaintext highlighter-rouge">client_id</code> 입니다. <code class="language-plaintext highlighter-rouge">client_id</code> 는 <code class="language-plaintext highlighter-rouge">secret</code> 과는 달리 공개되어도 되는 부분입니다.<br />
<code class="language-plaintext highlighter-rouge">redirect_uri</code> 는 앱등록할 때 입력한 Callback URL 중의 하나를 입력하면 됩니다. 개발단계에서는 <code class="language-plaintext highlighter-rouge">hostname</code> 이 <code class="language-plaintext highlighter-rouge">localhost</code> 이지만 실제 서비스에 배포를 하면 해당 도메인으로 설정되도록 했습니다.<br />
<code class="language-plaintext highlighter-rouge">state</code> 는 아주 중요한 값입니다. <strong>csrf 등의 공격으로 사용자가 해당 서비스를 접속하지 않고 소셜로그인을 시도하는 경우를 차단</strong>하기 위해서 필요한 값입니다. 임의의 문자열이 필요한데 서버에서도 올바른 값인지 비교할 수 있도록 로그인 <code class="language-plaintext highlighter-rouge">csrfmiddlewaretoken</code> 을 이용했습니다. <code class="language-plaintext highlighter-rouge">state</code> 값을 서버에서 이용하는 방법은 뷰를 개발할 때 설명하도록 하겠습니다.</p>
<p><code class="language-plaintext highlighter-rouge">naverLogin</code> 함수는 결국 생성된 url 로 화면이 전환하도록 하는 기능을 제공합니다. 전환된 화면은 네이버에서 제공하는 화면으로 여러분이 컨트롤할 수 없습니다. 다만 앱설정 화면 몇가지 설정만 변경할 수 있습니다.</p>
<p>이 상태에서 네이버 아이디로 로그인 버튼을 클릭하면 화면이 네이버에서 제공하는 화면으로 전환되고 아직 네이버에 로그인되지 않은 상태일 경우 로그인화면이 나타나고 로그인을 하면 앱 권한설정하는 화면으로 전환됩니다.(로그인이 되어 있을 때는 곧바로 앱 권한설정하는 화면으로 이동됩니다.)</p>
<p><img src="https://swarf00.github.io/snapshots/naver_social_login_02.png" alt="네이버 소셜로그인 02" /></p>
<p>필수 제공 항목에 아까 설정한 필수권한으로 설정한 이름과 이메일이 선택할 수 있게 나타납니다. 사용자가 이 항목들을 제공하지 않고 싶을 경우에 선택을 해제할 수 있습니다. 여기서는 이름과 이메일을 이용해 회원가입을 구현해야 하기 때문에 어느 하나라도 선택을 해제할 경우 이후 callback 으로 이동된 뷰에서 로그인을 허용하지 않을 것입니다. 동의하기 버튼을 누르면 <code class="language-plaintext highlighter-rouge">redirect_uri</code> 로 설정한 주소로 이동합니다. 물론 아직 뷰가 없으니 오류가 날 것입니다.</p>
<h3 id="네이버-로그인-callback-뷰-구현">네이버 로그인 callback 뷰 구현</h3>
<p>현재는 네이버의 소셜로그인만 제공하고 있지만 향후 카카오나 페이스북 등의 소셜로그인도 기능을 제공할 수 있기 때문에 callback 뷰는 하나만 제공하고, 프로바이더 별로 믹스인을 추가하도록 하는 방법을 선택했습니다. 프로바이더 별로 callback 뷰를 생성할 수도 있으나 여러분의 선택입니다. <del>분리하는 게 저 개인적으로 선호하는 방법입니다.</del></p>
<p><code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 라는 이름으로 뷰를 하나 생성합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">from</span> <span class="nn">django.views.generic.base</span> <span class="kn">import</span> <span class="n">TemplateView</span><span class="p">,</span> <span class="n">View</span>
<span class="kn">from</span> <span class="nn">django.middleware.csrf</span> <span class="kn">import</span> <span class="n">_compare_salted_tokens</span>
<span class="kn">from</span> <span class="nn">user.oauth.providers.naver</span> <span class="kn">import</span> <span class="n">NaverLoginMixin</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">SocialLoginCallbackView</span><span class="p">(</span><span class="n">NaverLoginMixin</span><span class="p">,</span> <span class="n">View</span><span class="p">):</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_REDIRECT_URL</span>
<span class="n">failure_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_URL</span>
<span class="n">required_profiles</span> <span class="o">=</span> <span class="p">[</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">]</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">provider</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'provider'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">provider</span> <span class="o">==</span> <span class="s">'naver'</span><span class="p">:</span> <span class="c1"># 프로바이더가 naver 일 경우
</span> <span class="n">csrf_token</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'state'</span><span class="p">)</span>
<span class="n">code</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'code'</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">_compare_salted_tokens</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">COOKIES</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'csrftoken'</span><span class="p">)):</span> <span class="c1"># state(csrf_token)이 잘못된 경우
</span> <span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">'잘못된 경로로 로그인하셨습니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="n">is_success</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">login_with_naver</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">,</span> <span class="n">code</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_success</span><span class="p">:</span> <span class="c1"># 로그인 실패할 경우
</span> <span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">success_url</span> <span class="k">if</span> <span class="n">is_success</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">set_session</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 뷰는 화면이 필요없고 <strong>오직 서버단에서 네이버 인증토큰을 받고 인증처리</strong>를 하는 기능만 합니다. 그래서 기본 제네릭뷰를 상속받았습니다. 로그인에 설정할 경우 <code class="language-plaintext highlighter-rouge">settings.LOGIN_REDIRECT_URL</code> 로 이동하고 로그인에 실패할 경우 <code class="language-plaintext highlighter-rouge">settings.LOGIN_URL</code> 에 이동하도록 설정했습니다. 만일 변경하고 싶으시다면 <code class="language-plaintext highlighter-rouge">success_url</code>, <code class="language-plaintext highlighter-rouge">failure_url</code> 클래스변수를 수정하시면 됩니다.(해당 값들은 설정파일에 설정하는 것이 재사용하는 데 편리합니다.)</p>
<p><code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 는 네이버 소셜로그인 기능을 구현한 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 을 추가했습니다. <a href="/2018/12/19/social-login.html#네이버-로그인-믹스인-구현"><code class="language-plaintext highlighter-rouge">NaverLoginMixin</code></a> 는 조금 뒤에 설명드리겠습니다.</p>
<p>먼저 살펴봐야 할 부분은 <code class="language-plaintext highlighter-rouge">provider = kwargs.get('provider')</code> 부분인데 <code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 에서는 url 라우터로부터 프로바이더 이름을 인자로 전달받습니다. 이렇게 하면 하나의 callback 뷰에서 여러 개의 프로바이더 를 처리할 수 있습니다. 현재는 하나의 프로바이더 만 제공하기 때문에 이렇게 했지만 2개 이상의 프로바이더 만 제공하더라도 뷰클래스를 분리하는 것도 더 효율적일 것 같습니다.</p>
<p><strong><code class="language-plaintext highlighter-rouge">_compare_salted_tokens</code> 함수를 통해 요청된 url의 query 값의 <code class="language-plaintext highlighter-rouge">state</code> 값과 쿠키의 <code class="language-plaintext highlighter-rouge">csrftoken</code> 을 비교</strong>합니다. state 값은 <code class="language-plaintext highlighter-rouge">naverLogin()</code> 함수에서 전달한 <code class="language-plaintext highlighter-rouge">state</code> 값입니다. 만일 정상적인 로그인페이지에서 네이버 소셜로그인 버튼을 누른 거라면 <code class="language-plaintext highlighter-rouge">state</code> 값이 쿠키의 <code class="language-plaintext highlighter-rouge">csrftoken</code>과 동일한 값이어야 합니다. <code class="language-plaintext highlighter-rouge">_compare_salted_tokens</code> 함수로 비교하면 동일한 지 아닌 지 알 수 있습니다. <code class="language-plaintext highlighter-rouge">state</code> 값이 정상적인 값이라면 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 에 정의한 <code class="language-plaintext highlighter-rouge">login_with_naver</code> 메소드를 통해서 로그인을 시도합니다. 로그인이 정상적으로 되었다면 <code class="language-plaintext highlighter-rouge">success_url</code> 로 이동하도록 하고 로그인에 실패했다면 <code class="language-plaintext highlighter-rouge">failure_url</code> 로 이동합니다.</p>
<h3 id="네이버-로그인-믹스인-구현">네이버 로그인 믹스인 구현</h3>
<p><code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 는 장고에서 제공하는 클래스가 아니라 여러분이 구현할 클래스입니다. user 앱에 <code class="language-plaintext highlighter-rouge">oauth</code> 라는 패키지를 생성하고 그 안에 <code class="language-plaintext highlighter-rouge">providers</code> 라는 패키지를 생성합니다. <code class="language-plaintext highlighter-rouge">providers</code> 패키지에 <code class="language-plaintext highlighter-rouge">naver.py</code> 라는 파일을 생성 후 아래와 같이 코드를 추가합니다.</p>
<blockquote>
<p>일반디렉토리와 달리 패키지는 내부에 <code class="language-plaintext highlighter-rouge">__init__.py</code> 모듈(파일)이 있어야 합니다.</p>
</blockquote>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/oauth/providers/naver.py
</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">login</span>
<span class="k">class</span> <span class="nc">NaverLoginMixin</span><span class="p">:</span>
<span class="n">naver_client</span> <span class="o">=</span> <span class="n">NaverClient</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">login_with_naver</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">code</span><span class="p">):</span>
<span class="c1"># 인증토근 발급
</span> <span class="n">is_success</span><span class="p">,</span> <span class="n">token_infos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">naver_client</span><span class="o">.</span><span class="n">get_access_token</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">code</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_success</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="s">'{} [{}]'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'error_desc'</span><span class="p">),</span> <span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'error'</span><span class="p">))</span>
<span class="n">access_token</span> <span class="o">=</span> <span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'access_token'</span><span class="p">)</span>
<span class="n">refresh_token</span> <span class="o">=</span> <span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'refresh_token'</span><span class="p">)</span>
<span class="n">expires_in</span> <span class="o">=</span> <span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'expires_in'</span><span class="p">)</span>
<span class="n">token_type</span> <span class="o">=</span> <span class="n">token_infos</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'token_type'</span><span class="p">)</span>
<span class="c1"># 네이버 프로필 얻기
</span> <span class="n">is_success</span><span class="p">,</span> <span class="n">profiles</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_naver_profile</span><span class="p">(</span><span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_success</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="n">profiles</span>
<span class="c1"># 사용자 생성 또는 업데이트
</span> <span class="n">user</span><span class="p">,</span> <span class="n">created</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">profiles</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'email'</span><span class="p">))</span>
<span class="k">if</span> <span class="n">created</span><span class="p">:</span> <span class="c1"># 사용자 생성할 경우
</span> <span class="n">user</span><span class="o">.</span><span class="n">set_password</span><span class="p">(</span><span class="bp">None</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">profiles</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'name'</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">is_active</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">user</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="c1"># 로그인
</span> <span class="n">login</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="s">'user.oauth.backends.NaverBackend'</span><span class="p">)</span> <span class="c1"># NaverBackend 를 통한 인증 시도
</span>
<span class="c1"># 세션데이터 추가
</span> <span class="bp">self</span><span class="o">.</span><span class="n">set_session</span><span class="p">(</span><span class="n">access_token</span><span class="o">=</span><span class="n">access_token</span><span class="p">,</span> <span class="n">refresh_token</span><span class="o">=</span><span class="n">refresh_token</span><span class="p">,</span> <span class="n">expires_in</span><span class="o">=</span><span class="n">expires_in</span><span class="p">,</span> <span class="n">token_type</span><span class="o">=</span><span class="n">token_type</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">True</span><span class="p">,</span> <span class="n">user</span>
<span class="k">def</span> <span class="nf">get_naver_profile</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="p">):</span>
<span class="n">is_success</span><span class="p">,</span> <span class="n">profiles</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">naver_client</span><span class="o">.</span><span class="n">get_profile</span><span class="p">(</span><span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_success</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="n">profiles</span>
<span class="k">for</span> <span class="n">profile</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">required_profiles</span><span class="p">:</span>
<span class="k">if</span> <span class="n">profile</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">profiles</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="s">'{}은 필수정보입니다. 정보제공에 동의해주세요.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">True</span><span class="p">,</span> <span class="n">profiles</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 에서 네이버의 api를 구현한 네이버 클라이언트를 <code class="language-plaintext highlighter-rouge">naver_client</code> 클래스변수로 추가했습니다. 네이버의 인증토큰 발급과 프로필 정보를 가져오는 두 가지의 기능을 제공합니다. <a href="/2018/12/19/social-login.html#네이버-클라이언트-구현">네이버 클라이언트</a>는 나중에 설명하기로 하고 <code class="language-plaintext highlighter-rouge">login_with_naver</code> 메소드를 설명드리겠습니다. <code class="language-plaintext highlighter-rouge">login_with_naver</code> 메소드는 <code class="language-plaintext highlighter-rouge">naver_client</code>로부터 <code class="language-plaintext highlighter-rouge">token_infos</code> 객체를 전달받는데 <code class="language-plaintext highlighter-rouge">token_infos</code> 객체는 아래와 같은 키를 갖는 딕셔너리 객체입니다.</p>
<ol>
<li>error - 에러코드</li>
<li>error_description - 에러메시지</li>
<li>access_token - 인증토큰</li>
<li>refresh_token - 인증토큰 재발급토큰</li>
<li>expires_in - 인증토큰 만료기한(초)</li>
<li>token_type - 인증토큰 사용하는 api 호출시 인증방식(Authorization 헤더 타입)</li>
</ol>
<p>만일 인증토큰을 받아오는 데 실패했다면 에러메시지와 함께 함수를 바로 종료합니다.</p>
<p>인증토큰이 정상적으로 발급되었다면 회원가입을 위해 이메일과 사용자의 이름을 받아야 하는데, 네이버에서 profile api도 제공해주기 때문에 이것을 이용해서 받아오면 됩니다. <strong><code class="language-plaintext highlighter-rouge">get_naver_profile</code> 메소드는 api를 통해 받아 온 프로필 정보를 검증하는 역할</strong>을 합니다. 프로필 정보는 사용자가 제공항목에 선택한 값들과 사용자의 id 값만 전달되는데 만일 <strong>이메일이나 이름을 선택하지 않은 경우 에러메시지를 반환</strong>하도록 했습니다.</p>
<p>프로필정보까지 정상적으로 받아오면 사용자 모델에서 <code class="language-plaintext highlighter-rouge">get_or_create</code> 메소드를 통해 동일한 이메일의 사용자가 있는 지 확인 후 없으면 새로 생성합니다. 소셜로그인은 가입과 로그인을 동시에 제공하는 것이 더 좋습니다. 이미 <strong>가입되어 있는 사용자라면 회원정보(이름)만 수정</strong>하면 되고, <strong>가입되어 있지 않은 케이스라면 새로 회원정보를 생성해서 가입</strong>시켜 줍니다. 소셜로그인은 로컬 비밀번호가 필요없기 때문에 새로 사용자 데이터가 추가되는 경우라면 <code class="language-plaintext highlighter-rouge">set_password(None)</code> 메소드를 통해 랜덤한 비밀번호를 생성해서 저장합니다. 이미 소셜로그인을 통해서 이메일에 대한 인증도 되었으니 <code class="language-plaintext highlighter-rouge">is_active</code> 값도 활성화 시켜주고 저장을 하면 가입이 완료입니다. 만일 <strong>이미 가입되어 있던 사용자라면 이메일과 비밀번호로도 로그인이 가능하고 네이버 소셜로그인으로도 로그인이 가능</strong>합니다.</p>
<p>가입된 이후에 로그인처리까지 해줘야 합니다. 로그인은 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크의 <code class="language-plaintext highlighter-rouge">login</code> 함수를 이용합니다. <code class="language-plaintext highlighter-rouge">login</code> 함수는 사용자 데이터와 로그인처리를 해줄 인증백엔드의 경로가 필요합니다. 기본 인증모듈인 <code class="language-plaintext highlighter-rouge">'django.contrib.auth.backends.ModelBackend'</code> 는 <code class="language-plaintext highlighter-rouge">username(email)</code> 과 비밀번호를 이용해서 인증처리를 하는데 소셜로그인은 비밀번호를 전달받을 수가 없습니다. 어쩔 수 없이 소셜로그인을 위한 인증백엔드를 추가로 구현해줘야 합니다. <a href="/2018/12/19/social-login.html#인증백엔드-구현">인증백엔드</a>의 구현은 조금 뒤에 설명하겠습니다.</p>
<p>소셜로그인의 마지막은 <strong>세션정보에 인증토큰정보를 추가</strong>하는 것입니다. 현재는 인증토큰이 필요없지만 네이버 api를 이용한 기능을 제공할 경우도 있습니다. 이 때 사용자의 인증토큰이 있어야만 사용자의 권한으로 네이버 서비스 api 기능들을 제공할 수 있는데 매번 재로그인을 할 수 없으니 인증토큰과 그 외 정보들을 세션에 저장합니다. 인증토큰 재발급토큰(refresh_token)도 함께 저장을 해야 인증토큰이 만료가 되더라도 재발급토큰으로 다시 인증토큰을 갱신할 수 있습니다. 만일 재발급토큰도 만료가 되었거나 문제가 있어서 인증토큰을 갱신할 수 없다면 로그아웃 처리 해주면 됩니다.</p>
<h3 id="네이버-클라이언트-구현">네이버 클라이언트 구현</h3>
<p>네이버 api 를 호출하는 모듈이 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 에서 필요합니다. 네이버 api 는 인증과 관련된 부분과 서비스 api를 통칭하지만 현재 여러분은 인증과 관련된 api 만 구현할 것입니다. 그래서 네이버 로그인 믹스인과 동일한 패키지에 구현해도 괜찮을 듯 합니다. 하지만 나중에 네이버 서비스 api도 구현하게 된다면 이것을 외부로 분리시키는 것이 바람직합니다.</p>
<p>네이버의 api를 호출할 때 <code class="language-plaintext highlighter-rouge">requests</code> 라이브러리를 사용하여 호출하도록 했습니다. <code class="language-plaintext highlighter-rouge">requests</code> 는 파이썬의 표준 http 클라이언트보다 사용하기 간편하고, 무엇보다 직관적입니다. <code class="language-plaintext highlighter-rouge">requests</code> 라이브러리를 먼저 설치하세요.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>pip <span class="nb">install </span>requests
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 이 정의된 <code class="language-plaintext highlighter-rouge">oauth/providers/naver.py</code> 에 <code class="language-plaintext highlighter-rouge">NaverClient</code> 라는 클래스를 추가하고 아래와 같이 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/oauth/providers/naver.py
</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="k">class</span> <span class="nc">NaverClient</span><span class="p">:</span>
<span class="n">client_id</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">NAVER_CLIENT_ID</span>
<span class="n">secret_key</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">NAVER_SECRET_KEY</span>
<span class="n">grant_type</span> <span class="o">=</span> <span class="s">'authorization_code'</span>
<span class="n">auth_url</span> <span class="o">=</span> <span class="s">'https://nid.naver.com/oauth2.0/token'</span>
<span class="n">profile_url</span> <span class="o">=</span> <span class="s">'https://openapi.naver.com/v1/nid/me'</span>
<span class="n">__instance</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">__new__</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">cls</span><span class="o">.</span><span class="n">__instance</span><span class="p">,</span> <span class="n">cls</span><span class="p">):</span>
<span class="n">cls</span><span class="o">.</span><span class="n">__instance</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">__new__</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">cls</span><span class="o">.</span><span class="n">__instance</span>
<span class="k">def</span> <span class="nf">get_access_token</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">code</span><span class="p">):</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">auth_url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s">'client_id'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">client_id</span><span class="p">,</span> <span class="s">'client_secret'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">secret_key</span><span class="p">,</span>
<span class="s">'grant_type'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">grant_type</span><span class="p">,</span> <span class="s">'state'</span><span class="p">:</span> <span class="n">state</span><span class="p">,</span> <span class="s">'code'</span><span class="p">:</span> <span class="n">code</span><span class="p">})</span>
<span class="k">return</span> <span class="n">res</span><span class="o">.</span><span class="n">ok</span><span class="p">,</span> <span class="n">res</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_profile</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">access_token</span><span class="p">,</span> <span class="n">token_type</span><span class="o">=</span><span class="s">'Bearer'</span><span class="p">):</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">profile_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">'Authorization'</span><span class="p">:</span> <span class="s">'{} {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">token_type</span><span class="p">,</span> <span class="n">access_token</span><span class="p">)})</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<span class="k">if</span> <span class="n">res</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'resultcode'</span><span class="p">)</span> <span class="o">!=</span> <span class="s">'00'</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span><span class="p">,</span> <span class="n">res</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'message'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">True</span><span class="p">,</span> <span class="n">res</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'response'</span><span class="p">)</span>
</code></pre></div></div>
<p><a href="https://developers.naver.com/docs/login/web/">네이버 소셜로그인 튜토리얼</a> 문서를 참고해서 구현했습니다. 특별히 어려운 내용은 없고 간단히 requests 모듈의 사용법을 알려드리면 <code class="language-plaintext highlighter-rouge">get</code>, <code class="language-plaintext highlighter-rouge">post</code>, <code class="language-plaintext highlighter-rouge">put</code>, <code class="language-plaintext highlighter-rouge">delete</code> 등의 함수들이 구현되어 있고, 각각의 함수는 함수명과 동일한 http 메소드로 요청을 합니다. 첫번째 위치 인자는 url 이고 그 외 파라미터는 keyword 인자로 전달하면 됩니다. <code class="language-plaintext highlighter-rouge">get_profile</code> 메소드에서 <code class="language-plaintext highlighter-rouge">headers</code> 라는 파라미터가 사용되는데 http 헤더의 값을 딕셔너리 형태로 전달하면 됩니다. <code class="language-plaintext highlighter-rouge">Authorization</code> 헤더를 <code class="language-plaintext highlighter-rouge">token_type(bearer)</code> 와 인증토큰을 조합한 값으로 추가했습니다. 각 함수 반환데이터는 json 메소드를 통해 본문의 내용을 딕셔너리 형태로 반환해 줄 수도 있습니다. 물론 본문이 json 타입이 아닐 경우 에러가 발생합니다.</p>
<p>눈치채셨겠지만 여기서 약간 재미있는 프로그래밍(디자인) 패턴을 사용했습니다. <strong><code class="language-plaintext highlighter-rouge">singleton</code> 이라는 패턴인데 첫번째 생성자 호출 때만 객체만 생성시키고 이후 생성자 호출부터는 먼저 생성된 객체를 공유</strong>하게 하는 방식입니다. <code class="language-plaintext highlighter-rouge">NaverClient</code> 클래스를 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 뿐만 아니라 다른 클래스에서도 공유하며 사용할 수 있습니다. <code class="language-plaintext highlighter-rouge">NaverClient</code> 객체는 인스턴스변수가 없기 때문에 하나의 객체를 서로 공유하더라도 문제가 발생하지 않습니다. 이렇게 인스턴스변수가 존재하지 않으나 여러 클래스에서 유틸리티처럼 사용하는 클래스의 경우 싱글턴 패턴을 많이 사용합니다. 객체를 생성하는 비용이 줄어 서버의 가용성을 높이는 좋은 패턴이니 구현방법을 알아두세요. 파이썬에서 여러가지 방법이 있으나 가장 간단한 방법으로 구현했습니다.</p>
<blockquote>
<p>일반적으로 싱글턴은 생성자가 아니라 명시적으로 <code class="language-plaintext highlighter-rouge">getInstance</code> 라는 static 메소드를 제공해서 객체를 생성합니다. <code class="language-plaintext highlighter-rouge">getInstance</code> 를 사용하지 않고 생성자를 사용해 객체를 생성하면 에러를 발생시켜 싱글턴으로 구현되었음을 개발자에게 알려주는 것이죠. 싱글턴 객체에 인스턴스변수를 추가하거나 클래스변수를 변경하면 벌받습니다. ㅠㅠ</p>
</blockquote>
<h3 id="인증백엔드-구현">인증백엔드 구현</h3>
<p><code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 에서 로그인할 때 인증백엔드를 <code class="language-plaintext highlighter-rouge">'user.oauth.backends.NaverBackend'</code> 로 전달했습니다. 인증백엔드의 경로대로 <code class="language-plaintext highlighter-rouge">oauth</code> 패키지에 <code class="language-plaintext highlighter-rouge">backends.py</code> 파일을 추가하고 아래의 클래스를 생성해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># user/oauth/backends.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.backends</span> <span class="kn">import</span> <span class="n">ModelBackend</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.models</span> <span class="kn">import</span> <span class="n">AnonymousUser</span>
<span class="n">UserModel</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">NaverBackend</span><span class="p">(</span><span class="n">ModelBackend</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">authenticate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span><span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="n">username</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">UserModel</span><span class="o">.</span><span class="n">USERNAME_FIELD</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">UserModel</span><span class="o">.</span><span class="n">_default_manager</span><span class="o">.</span><span class="n">get_by_natural_key</span><span class="p">(</span><span class="n">username</span><span class="p">)</span>
<span class="k">except</span> <span class="n">UserModel</span><span class="o">.</span><span class="n">DoesNotExist</span><span class="p">:</span>
<span class="k">pass</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">user_can_authenticate</span><span class="p">(</span><span class="n">user</span><span class="p">):</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">NaverBackend</code> 백엔드는 기본인증백엔드(<code class="language-plaintext highlighter-rouge">ModelBackend</code>) 를 상속받아 대부분의 기능들을 그대로 사용합니다. <strong><code class="language-plaintext highlighter-rouge">authenticate</code> 메소드에서 비밀번호를 비교하여 인증하는 부분이 있는데 이 부분을 삭제하고 소셜로그인으로 <code class="language-plaintext highlighter-rouge">email</code> 만 비교</strong>하도록 했습니다. 저는 예전에 <code class="language-plaintext highlighter-rouge">naverid</code> 도 데이터베이스에 저장하고 <code class="language-plaintext highlighter-rouge">email</code> 과 <code class="language-plaintext highlighter-rouge">naverid</code> 도 같이 비교하도록 구현한 적이 있는데 naverid 가 서비스에 필요하다면 저장하는 것이 맞으나 불필요하다면 굳이 데이터베이스에 저장할 필요는 없습니다. 만일 <code class="language-plaintext highlighter-rouge">email</code>이 사용자 테이블에 존재하지 않는다면 <code class="language-plaintext highlighter-rouge">None</code> 을 반환해주면 됩니다. 함수에서 아무것도 반환하지 않으면 <code class="language-plaintext highlighter-rouge">None</code> 을 리턴하므로 사용자 데이터 검색에 실패할 경우 아무것도 하지 않도록(<code class="language-plaintext highlighter-rouge">pass</code>) 했습니다.</p>
<p><code class="language-plaintext highlighter-rouge">user_can_authenticate</code> 메소드는 사용자 데이터의 <code class="language-plaintext highlighter-rouge">is_active</code> 가 <code class="language-plaintext highlighter-rouge">True</code> 인지 확인하는 기능을 제공합니다. 비밀번호와 관계가 없으니 이것을 확인하는 것으로 인증백엔드의 인증테스트를 종료합니다.</p>
<p>소셜로그인은 이미 프로바이더(네이버)에게 인증을 위임했기 때문에 인증백엔드에서 추가로 인증할 것이 별로 없습니다. 다만 사용자 모델의 정의가 이전 예제와 다르게 무언가 추가로 인증해야 할 필드들이 생겼을 경우에만 해당 필드를 이용해서 추가 인증을 하면 됩니다.</p>
<h3 id="인증백엔드-설정">인증백엔드 설정</h3>
<p>인증백엔드는 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 에서 사용을 하지만 이것은 로그인을 시도할 때 어떤 백엔드를 사용할 지에 대한 설정입니다. 이후 로그인된 상태에서 또다른 요청을 할 때 장고는 세션의 정보를 확인하여 로그인된 사용자가 맞는지, 맞다면 어떤 사용자인지를 식별하는데 장고의 기본값인 기본인증백엔드를 통해 식별처리를 실행합니다. 소셜로그인으로 로그인 사용자를 위해 설정파일의 <code class="language-plaintext highlighter-rouge">AUTHENTICATION_BACKENDS</code> 변수에 <code class="language-plaintext highlighter-rouge">NaverBackend</code> 를 추가합니다. <code class="language-plaintext highlighter-rouge">AUTHENTICATION_BACKENDS</code>는 설정은 세션의 사용자 정보를 식별할 때 사용될 백엔드를 리스트로 설정하여 실제 사용자 정보를 식별할 때 리스트의 순서대로 백엔드에 인증을 시도하고, 인증이 되면 해당 인증된 사용자 정보를 넘겨주고, 인증에 실패할 경우 리스트의 다음 백엔드에 위임하게 됩니다. 모든 백엔드에서 인증에 실패할 경우 인증되지 않은 사용자라고 처리하는 것이죠.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">NAVER_CLIENT_ID</span> <span class="o">=</span> <span class="s">'your client id'</span>
<span class="n">NAVER_SECRET_KEY</span> <span class="o">=</span> <span class="s">'your secret key'</span>
<span class="n">AUTHENTICATION_BACKENDS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'user.oauth.backends.NaverBackend'</span><span class="p">,</span> <span class="c1"># 네이버 인증백엔드
</span> <span class="s">'django.contrib.auth.backends.ModelBackend'</span>
<span class="p">]</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<blockquote>
<p>센스있는 분들은 리스트의 순서가 중요함을 느끼실 텐데 가장 많은 사용자가 이용하는 백엔드를 가장 위에 설정하고, 가장 사용하지 않는 백엔드를 가장 밑에 설정하는 것이 인증 성능을 높이는 한가지 포인트라고 할 수도 있습니다. ~.^ 찡끗~</p>
</blockquote>
<h3 id="next-query-파라미터-처리">next query 파라미터 처리</h3>
<p>로그인되지 않은 상태에서 게시글 작성 화면 접근시 자동으로 로그인페이지로 이동하고 url 의 쿼리에 next query 파라미터가 추가됩니다.</p>
<p><img src="https://swarf00.github.io/snapshots/naver_social_login_03.png" alt="네이버 소셜로그인 03" /></p>
<p>현재 소셜로그인을 성공할 경우 무조건 settings.LOGIN_REDIRECT_URL 로 이동하는데 next query 파라미터가 있는 경우 소셜로그인 이후 해당 url로 이동하도록 수정하겠습니다.</p>
<p>우선 javascript 의 redirect_uri 를 통해 next query 파라미터를 전달하겠습니다.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// user/static/user/js/social_login.js</span>
<span class="kd">function</span> <span class="nx">buildQuery</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">params</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="nx">key</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">=</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">encodeURIComponent</span><span class="p">(</span><span class="nx">params</span><span class="p">[</span><span class="nx">key</span><span class="p">])}).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">&</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">buildUrl</span><span class="p">(</span><span class="nx">baseUrl</span><span class="p">,</span> <span class="nx">queries</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">baseUrl</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">?</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">buildQuery</span><span class="p">(</span><span class="nx">queries</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">naverLogin</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">response_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">code</span><span class="dl">'</span><span class="p">,</span>
<span class="na">client_id</span><span class="p">:</span><span class="dl">'</span><span class="s1">nfenn0pzKTlihOzu_h8S</span><span class="dl">'</span><span class="p">,</span>
<span class="na">redirect_uri</span><span class="p">:</span> <span class="nx">location</span><span class="p">.</span><span class="nx">origin</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/user/login/social/naver/callback/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">,</span>
<span class="na">state</span><span class="p">:</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">[name=csrfmiddlewaretoken]</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span>
<span class="p">}</span>
<span class="nx">url</span> <span class="o">=</span> <span class="nx">buildUrl</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://nid.naver.com/oauth2.0/authorize</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">)</span>
<span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>query 파라미터가 있을 경우 redirect_uri 에도 그대로 추가하도록 했습니다. 다음으로는 callback view 에서 next query 파라미터를 읽고 소셜로그인이 성공했을 경우 해당 url로 이동하도록 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">SocialLoginCallbackView</span><span class="p">(</span><span class="n">NaverLoginMixin</span><span class="p">,</span> <span class="n">View</span><span class="p">):</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_REDIRECT_URL</span>
<span class="n">failure_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_URL</span>
<span class="n">required_profiles</span> <span class="o">=</span> <span class="p">[</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">]</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">provider</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'provider'</span><span class="p">)</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'next'</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_url</span><span class="p">)</span>
<span class="k">if</span> <span class="n">provider</span> <span class="o">==</span> <span class="s">'naver'</span><span class="p">:</span>
<span class="n">csrf_token</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'state'</span><span class="p">)</span>
<span class="n">code</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'code'</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">_compare_salted_tokens</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">COOKIES</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'csrftoken'</span><span class="p">)):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">'잘못된 경로로 로그인하셨습니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="n">is_success</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">login_with_naver</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">,</span> <span class="n">code</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">is_success</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="n">success_url</span> <span class="k">if</span> <span class="n">is_success</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">failure_url</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">set_session</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
</code></pre></div></div>
<p>간단하게 <code class="language-plaintext highlighter-rouge">success_url = request.GET.get('next', self.success_url)</code> 로 지역변수 success_url 을 정의했습니다. 소셜로그인이 성공한 이후에 이 success_url 지역변수를 이용해 이동하도록 변경했습니다.</p>
<p>이제 로그아웃 이후 새 게시글 작성 버튼을 클릭하면 정상적으로 로그인 화면으로 이동되고, 소셜로그인을 하더라도 로그인 이후 게시글 작성 화면으로 이동됩니다.</p>
<h3 id="urlpatterns-추가-등록">urlpatterns 추가 등록</h3>
<p><code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 는 앞서 살펴 본 대로 모든 프로바이더들을 모두 처리하도록 구현되어 있습니다. 물론 네이버 이외의 다른 프로바이더는 아직 미구현 상태이지만 여러분들이 곧바로 추가할 테니까요.^^ <code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 여러분들이 개발한 <code class="language-plaintext highlighter-rouge">SocialLoginCallbackView</code> 를 등록하면 소셜로그인 기능이 완료됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.views</span> <span class="kn">import</span> <span class="n">LogoutView</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">bbs.views</span> <span class="kn">import</span> <span class="n">hello</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span>
<span class="kn">from</span> <span class="nn">user.views</span> <span class="kn">import</span> <span class="n">UserRegistrationView</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="p">,</span> <span class="n">ResendVerifyEmailView</span><span class="p">,</span> <span class="n">SocialLoginCallbackView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">'article-list'</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/<pk>/verify/<token>/'</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/resend_verify_email/'</span><span class="p">,</span> <span class="n">ResendVerifyEmailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/login/'</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/logout/'</span><span class="p">,</span> <span class="n">LogoutView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/login/social/<provider>/callback/'</span><span class="p">,</span> <span class="n">SocialLoginCallbackView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>굳이 하나의 클래스에 구현한 이유는 클래스를 좀 더 일반적(general)인 형태로 구현하려고 설계하다보니 이렇게 되었습니다. 지금이라도 네이버 전용 콜백 클래스로 변형해도 나쁘지 않지만 여러분들에게 숙제로 남겨드립니다.</p>
<p>이제 아까 callback 페이지가 없어서 오류가 난 화면에서 새로고침을 해보시거나 다시 로그인화면으로 이동하셔서 네이버 소셜로그인을 테스트해보세요. 정상적으로 로그인이 되었다면 <del>복붙</del> 잘 따라하신 것입니다.</p>
<h2 id="2-회원가입-화면에-소셜로그인-추가">2. 회원가입 화면에 소셜로그인 추가</h2>
<p>회원가입할 때도 소셜로그인 기능을 제공해주면 사용자들의 가입이 훨씬 편해질 것입니다. 이미 소셜로그인 기능은 다 구현된 상태이기 때문에 <strong>회원가입 템플릿만 수정</strong>해주면 됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/registration_form.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static 'user/css/user.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
{% include 'user/partials/social_login_panel.html' %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">include</code> 템플릿태그 하나로 소셜로그인을 추가했습니다.😏 회원가입 화면에서는 소셜로그인 화면을 조금 다르게 하고 싶다면 <code class="language-plaintext highlighter-rouge">include</code> 하지 않고 새로 구현하셔도 상관없습니다. 만약 회원가입 화면에서도 패널이름으로 소셜로그인 이라고 표시되는 것이 보기 싫으시다면 인자로 <code class="language-plaintext highlighter-rouge">include</code> 템플릿 태그에 <code class="language-plaintext highlighter-rouge">with</code> 키워드로 패널이름을 넘겨주셔도 좋습니다. 기왕 말이 나왔으니 리팩토링을 하도록 하죠. 먼저 <code class="language-plaintext highlighter-rouge">social_login_panel.html</code> 템플릿에서 <code class="language-plaintext highlighter-rouge">panel_name</code> 이라는 변수로 소셜로그인 이라는 텍스트를 대체합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/partials/social_login_panel.html --></span>
{% load static %}
{% static 'user/img/kakao_login.png' as kakao_button %}
{% static 'user/img/kakao_login_ov.png' as kakao_button_hover %}
{% static 'user/img/naver_login_green.png' as naver_button %}
{% static 'user/img/naver_login_white.png' as naver_button_hover %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
{{ panel_name }}
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body text-center"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pull-left"</span><span class="nt">></span>
<span class="nt"><a></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"{{ kakao_button }}"</span>
<span class="na">onmouseover=</span><span class="s">"this.src='{{ kakao_button_hover }}'"</span>
<span class="na">onmouseleave=</span><span class="s">"this.src='{{ kakao_button }}'"</span><span class="na">height=</span><span class="s">"34"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"pull-right"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">onclick=</span><span class="s">"naverLogin()"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"{{ naver_button }}"</span>
<span class="na">onmouseover=</span><span class="s">"this.src='{{ naver_button_hover }}'"</span>
<span class="na">onmouseleave=</span><span class="s">"this.src='{{ naver_button }}'"</span><span class="na">height=</span><span class="s">"34"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>이제 <code class="language-plaintext highlighter-rouge">login_form.html</code> 과 <code class="language-plaintext highlighter-rouge">registration_form.html</code> 의 <code class="language-plaintext highlighter-rouge">include</code> 템플릿태그를 수정합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/login_form.html --></span>
<span class="c"><!-- 생략 --></span>
{% include 'user/partials/social_login_panel.html' with panel_name='소셜로그인' %}
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/registration_form.html --></span>
<span class="c"><!-- 생략 --></span>
{% include 'user/partials/social_login_panel.html' with panel_name='소셜회원가입' %}
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>소셜로그인, 소셜회원가입으로 각각 인자를 넘겨주도록 수정하고 로그인과 회원가입 화면을 다시 한번 접속해보시면 정상적으로 출력이 됩니다.</p>
<h2 id="3-소셜로그인-앱-분리">3. 소셜로그인 앱 분리</h2>
<p>소셜로그인 기능을 user 앱과는 별도의 앱으로 분리해도 되겠지만 여기서는 user 앱 내부에 소셜로그인 기능을 내장하도록 했습니다. 소셜로그인 기능만 다른 프로젝트에서 재사용하고 싶으시다면 아까 생성한 static 파일들과 뷰, 그리고 <code class="language-plaintext highlighter-rouge">oauth</code> 패키지를 따로 앱으로 분리하셔도 됩니다.</p>
<blockquote>
<p>앱분리 후 분리된 앱의 사용방법은 3가지만 하시면 됩니다.</p>
<ol>
<li>설정파일에 <code class="language-plaintext highlighter-rouge">AUTHENTICATION_BACKENDS</code>, <code class="language-plaintext highlighter-rouge">NAVER_CLIENT_ID</code>, <code class="language-plaintext highlighter-rouge">NAVER_SECRET_KEY</code> 설정</li>
<li><code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 <code class="language-plaintext highlighter-rouge">CallbackView</code> 를 등록합니다.</li>
<li>템플릿 생성 및 <code class="language-plaintext highlighter-rouge">naverLogin()</code> 호출. 템플릿은 어쩔 수 없으니 해당 프로젝트에 맞게 생성하시고 네이버 로그인 버튼의 <code class="language-plaintext highlighter-rouge">onclick</code> 속성을 <code class="language-plaintext highlighter-rouge">naverLogin()</code> 으로 설정해주시면 됩니다.</li>
</ol>
</blockquote>
<p>여기서는 앱을 분리하지 않고 조금 복잡하지만 소셜로그인과 사용자 앱을 하나의 앱으로 관리할 예정입니다. 하나의 앱으로 관리하면 좋은 점은 소셜로그인 기능이 사용자 모델에 어느정도 의존성이 있기 때문에 문제의 여지가 줄어듭니다. 예를들어 장고 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크에서 기본으로 제공하는 사용자 모델은 사용자의 이름이 <code class="language-plaintext highlighter-rouge">first_name</code>, <code class="language-plaintext highlighter-rouge">last_name</code> 으로 분리되어 있으나 새로운 사용자 모델에서는 <code class="language-plaintext highlighter-rouge">name</code> 이라는 하나의 필드로만 제공하고, 소셜로그인할 때도 <code class="language-plaintext highlighter-rouge">name</code> 이라는 필드를 사용합니다. 만일 사용자 모델에 <code class="language-plaintext highlighter-rouge">name</code> 이라는 필드가 없다면 오류가 생길테니 <code class="language-plaintext highlighter-rouge">NaverLoginMixin</code> 을 수정해주셔야 합니다. <code class="language-plaintext highlighter-rouge">email</code> 필드의 이름이 변경될 경우도 마찬가지이구요.</p>
<p>사용자 앱이 인증 기능 제공을 위한 앱이기 때문에 소셜로그인 기능을 추가해도 큰 문제는 없습니다. 그렇더라도 소셜로그인 기능을 별도의 앱으로 분리하는 것이 여전히 좋다고 생각하는 것이 잘못된 생각은 아닙니다. 기능별로 가급적 <strong>앱을 분리하는 것은 좋은 아이디어이지만 분리된 앱을 재사용할 때 문제가 되는 점이 있는 지 없는 지 면밀히 검토하고 문제가 발생하지 않도록 확실한 처리를 하도록 주의</strong>하셔야 합니다.</p>
<blockquote>
<p>창조적인 개발을 하려면 내가 틀릴지도 모른다는 공포를 버려야 한다</p>
<p>swarf00, 공포는 떠나가고...창조적인 개발도 하는데...야근은 해야 하고...눈물이...뚝뚝...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크에서 소셜로그인(oauth)를 이용하여 로그인 및 회원가입을 구현하는 방법을 알아봅니다.모델폼을 이용하여 쉽게 템플릿을 구현하는 방법을 알아봅니다.템플릿 리팩토링2018-12-17T00:00:00+09:002018-12-17T00:00:00+09:00https://swarf00.github.io/2018/12/17/template-refactoring<h2 id="1-static-파일-분리">1. STATIC 파일 분리</h2>
<p>벌써 bbs 앱에서 4 페이지, user 앱에서 3 페이지의 화면을 만들었습니다. 화면이 점점 많아지다보니 어쩔 수 없이 코드가 복잡해지고 중복되는 부분들이 많이 있어 한번쯤 쉬면서 코드를 정리해 보는 게 좋습니다. 우선 가장 문제가 있어보이는 부분은 템플릿입니다. 뷰와 모델은 조금씩 리팩토링을 해왔는데 템플릿은 <del>귀찮아서</del> 상대적으로 복잡도가 낮아 미루고 있었습니다. 더 늦어지기 전에 템플릿도 리팩토링을 해두도록 하겠습니다.</p>
<h3 id="static-파일을-분리하는-이유">static 파일을 분리하는 이유</h3>
<p>보통 css나 javascript를 html 또는 템플릿에 포함시키지 않습니다. 왜냐하면 css 나 javascript 는 서버에서 매번 렌더링 할 필요없이 모든 사용자에게 동일하게 적용됩니다. 다른 말로 표현하자면, css나 javascript 에는 데이터베이스에 저장된 값을 적용하거나 템플릿태그를 적용할 일이 없습니다. 간혹 서버에서 렌더링해야 할 필요가 있는 경우도 있지만 예외적인 케이스입니다. html은 반대로 서버에서 매번 새로 생성해야 하는 부분이기 때문에 어쩔 수 없이 매 요청마다 렌더링 후 전송을 해야합니다. 이렇게 서버에서 매번 새로 생성해줘야 하는 코드를 동적(dynamic) 코드라고 하고 css, javascript, image 와 같이 매번 새로 생성할 필요가 없는 코드를 정적(static) 코드라고 합니다. 정적 코드는 각 종류에 따라 css, js 등의 파일로 관리를 하고 정적 파일은 서버와 브라우저에서 아래와 같이 관리합니다.</p>
<pre><code class="language-mermaid">sequenceDiagram
participant 로컬캐시
participant 브라우저
participant 서버(장고)
Note right of 브라우저: static.css 1차 요청
브라우저 -->> 로컬캐시: static.css 파일 있니?
로컬캐시 -->> 브라우저: static.css 파일 없어
브라우저 -->> 서버(장고): static.css 파일 보내줘
서버(장고) ->> 브라우저: static.css 파일 여기있어. (Last-Modified: 2018-12-12 13:20:15 GMT)
브라우저 ->> 로컬캐시: static.css 파일 보관 좀^^
Note right of 브라우저: static.css 2차 요청
브라우저 -->> 로컬캐시: static.css 파일 있니?
로컬캐시 -->> 브라우저: 있어.(Last-Modified: 2018-12-12 13:20:15 GMT).
브라우저 -->> 서버(장고): static.css 파일 보내줘. (If-Modified-Since: 2018-12-12 13:20:15 GMT)
서버(장고) -->> 브라우저: 변경된 거 없어. 너 가지고 있는 거 그냥 써
브라우저 -->> 로컬캐시: static.css 변경되지 않았대. 가지고 있는 거 줄래
Note right of 브라우저: static.css 3차 요청
브라우저 -->> 로컬캐시: static.css 파일 있니?
로컬캐시 -->> 브라우저: 있어.(Last-Modified: 2018-12-12 13:20:15 GMT).
브라우저 -->> 서버(장고):static.css 파일 보내줘. (If-Modified-Since: 2018-12-12 13:20:15 GMT)
서버(장고) ->> 브라우저: static.css 파일 여기있어. (Last-Modified: 2018-12-16 14:20:15 GMT)
브라우저 ->> 로컬캐시: static.css 파일 보관 좀^^
</code></pre>
<p>(1차요청) 장고(또는 웹서버)에서 정적파일을 전송할 때 <code class="language-plaintext highlighter-rouge">Last-Modified</code> 헤더에 파일의 최종 수정된 시간을 표시합니다.<br />
(2차요청) 브라우저는 전송받은 정적파일을 로컬에 저장했다가 다음 번에 동일한 파일을 서버에 요청할 때 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 헤더에 <code class="language-plaintext highlighter-rouge">Last-Modified</code> 헤더의 값(최종 변경된 시간)을 표시해서 요청하고, 서버는 요청 헤더에 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 가 있으면 해당 <strong>파일의 최종 수정시간이 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 보다 작거나 같으면 파일은 보내지 않고</strong> 변경된 내용이 없다는 응답(<code class="language-plaintext highlighter-rouge">304, NotModified</code>)만 합니다.<br />
(3차요청) 브라우저는 그 정적파일을 로컬에 저장했다가 다음 번에 동일한 파일을 서버에 요청할 때 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 헤더에 <code class="language-plaintext highlighter-rouge">Last-Modified</code> 헤더의 값(최종 변경된 시간)을 표시해서 요청하고, 서버는 요청 헤더에 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 가 있으면 해당 파일의 최종 수정시간이 <code class="language-plaintext highlighter-rouge">If-Modified-Since</code> 보다 이후 시간일 경우는 다시 파일을 전송하고 <code class="language-plaintext highlighter-rouge">Last-Modified</code> 헤더에 파일의 최종 수정된 시간을 표시합니다.</p>
<p>이런식으로 처리하면 정적파일에 대해서 불필요한 전송을 하지 않게 되어 <strong>서버입장에서는 파일전송에 허비하는 시간이 줄고, 브라우저입장에서는 좀 더 빠르게 화면을 표시</strong>해줄 수 있게 됩니다. 물론 현재 여러분의 템플릿코드의 분량은 굉장히 작고 css나 javascript 가 거의 없지만 나중에는 템플릿의 절반 가량이 css와 javascript 로 구성될 수도 있습니다. 그럴 때는 이렇게 static 파일로 분리해서 운영하는 방식이 많은 도움이 많이 됩니다.</p>
<blockquote>
<p>css 나 javascript 는 정적파일로 따로 분리가 가능하지만 일반 html 태그들은 정적파일로 분리할 수가 없습니다. 장고의 템플릿처럼 중복되는 부분들을 <code class="language-plaintext highlighter-rouge">{% extends 'base.html' %}</code> 처럼 표시하면 브라우저가 알아서 다운로드하고 관리해주면 되지 않을까? 라고 생각했던 분들이 있을 수 있는데, html 표준에서 아직 그러한 기능이 제공되고 있지 않아 브라우저에서도 그런 기능을 제공하지 않습니다. 대신 브라우저를 대신해서 javascript 가 그런 역할을 대신하도록 하는 프레임워크들이 있습니다. 이것을 <code class="language-plaintext highlighter-rouge">spa(Single Page Application)</code>라고 하는데 대표적으로 <code class="language-plaintext highlighter-rouge">reactjs</code>, <code class="language-plaintext highlighter-rouge">vuejs</code>, <code class="language-plaintext highlighter-rouge">angularjs</code>, <code class="language-plaintext highlighter-rouge">emberjs</code>, <code class="language-plaintext highlighter-rouge">backbonejs</code> 등이 있습니다. spa 프레임워크는 화면을 크게 뼈대(템플릿)와 데이터로 분리해서 다루는데 뼈대는 정적파일로 관리(캐싱)하고 데이터는 동적으로 매번 서버에서 받아와 화면을 렌더링합니다.</p>
</blockquote>
<h3 id="static-설정">STATIC 설정</h3>
<p>장고에서는 모든 요청을 <code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 등록된 핸들러에 전달합니다. 브라우저가 static 파일을 다운로드 요청을 하더라도 이것에 대한 핸들러가 있어야 하는데 설정파일의 <code class="language-plaintext highlighter-rouge">STATIC_URL</code> 값이 설정되어 있으면 장고에 내장되어 있는 static 핸들러에 라우팅이 등록됩니다. 요청된 url이 설정된 <code class="language-plaintext highlighter-rouge">STATIC_URL</code> 로 시작되면 static 핸들러로 라우팅되도록 되어 있습니다. 여기서는 <code class="language-plaintext highlighter-rouge">STATIC_URL</code>로 기본설정된 <code class="language-plaintext highlighter-rouge">'/static/'</code> 을 변경지 않고 앞으로 static 파일들은 각 앱의 static 디렉토리에 저장하고, 요청하는 url도 <code class="language-plaintext highlighter-rouge">/static/</code> 으로 시작하도록 설정하겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s">'/static/'</span>
<span class="n">STATIC_ROOT</span> <span class="o">=</span> <span class="s">'/var/www/static'</span>
<span class="n">STATICFILES_DIRS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>static 관련 설정이 몇가지 더 있는데 아직은 필요하지 않지만 미리 설정해두도록 하겠습니다.</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">STATIC_URL</code>: 이 경로로 시작되는 요청은 static 핸들러로 라우팅</li>
<li><code class="language-plaintext highlighter-rouge">STATIC_ROOT</code>: <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드로 static 파일들을 모을 때 저장될 디렉토리 경로</li>
<li><code class="language-plaintext highlighter-rouge">STATICFILES_DIRS</code>: <code class="language-plaintext highlighter-rouge">collectstatic</code> 또는 <code class="language-plaintext highlighter-rouge">findstatic</code> 커맨드 실행시 검색하는 디렉토리 경로들의 리스트. 주로 앱 내부의 static 디렉토리가 아닌 다른 곳에 저장되어 있을 경우 설정함.</li>
</ol>
<p><code class="language-plaintext highlighter-rouge">STATIC_ROOT</code> 는 현재 존재하는 디렉토리 경로를 설정해야 합니다. 윈도우는 예외이지만 리눅스 또는 맥에서는 <code class="language-plaintext highlighter-rouge">manage.py</code> 를 실행하는 권한으로 <strong>디렉토리 권한이 설정</strong>되어 있어야 합니다.(웹서버에서 static 파일을 직접 전송해야 한다면 웹서버에서 읽을 수 있어야 합니다.) 이미 잘 설정되어 있는 경우는 상관없으나 새로 디렉토리를 생성해야 한다<del>거나 무슨 말인지 모르겠다</del>면 아래 명령어를 사용해보세요.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/www/static
<span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/www/static
<span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span><span class="nb">sudo chown</span> <span class="sb">`</span><span class="nb">whoami</span><span class="sb">`</span>:<span class="sb">`</span><span class="nb">id</span> <span class="nt">-g</span> <span class="nt">-n</span><span class="sb">`</span> /var/www/static/
</code></pre></div></div>
<p>이제 앞으로 /static/ 으로 시작하는 url 은 장고의 static 핸들러가 자동으로 처리해 줄 것 입니다.</p>
<blockquote>
<p>2, 3 번의 설정은 collectstatic 커맨드를 사용할 때 필요한 것들인데 이것들이 잘 설정되었다면 <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드를 실행했을 때 <code class="language-plaintext highlighter-rouge">STATIC_ROOT</code>('/var/www/static')의 경로에 static 파일들이 복사될 것입니다. 이것은 나중에 static 파일들을 생성 후 테스트해보겠습니다.</p>
</blockquote>
<h3 id="css-분리">CSS 분리</h3>
<p>우선 부트스트랩 css 는 대부분의 페이지에서 필요하니 base.html 로 옮기겠습니다. base.html 의 css 블록에 해당 태그(<code class="language-plaintext highlighter-rouge"><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"></code>)를 추가하고 나머지 템플릿들에서는 해당 라인을 대신해서 <code class="language-plaintext highlighter-rouge">{{ block.super }}</code> 를 추가합니다. <strong>css 블록 안에 <code class="language-plaintext highlighter-rouge">{{ block.super }}</code> 만 남는 경우는 과감히 css 블록을 삭제</strong>합니다.</p>
<p>그리고 각 템플릿들의 css 블록 안에 있는 css 코드들을 css 파일로 따로 분리합니다. 각 앱 별로 css는 따로 관리할 예정입니다. css 뿐만 아니라 javascript, 이미지 등의 파일도 모두 각 앱의 static 라는 디렉토리 안에서 관리하도록 하겠습니다. 모든 앱들의 static 파일들을 앱구분없이 하나의 디렉토리에서 관리할 수도 있습니다. 하지만 이렇게 되면 앱들을 다른 프로젝트에서 재사용할 수 없게 됩니다. <strong>bbs 나 user 앱을 다른 프로젝트에서도 재사용하고 싶다면 반드시 static 파일들을 각자의 앱 내부에 저장</strong>해주세요.</p>
<p>각 앱별로 static 디렉토리를 생성하고 그 안에 앱이름의 디렉토리를 다시 생성하고 그 안에 css, js, img 라는 디렉토리를 생성합니다. 먼저 bbs 앱의 <code class="language-plaintext highlighter-rouge">article_list.html</code> 템플릿부터 분리하겠습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% load static %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static 'bbs/css/bbs.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-hover table-responsive"</span><span class="nt">></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
{% for article in articles %}
<span class="nt"><tr</span> <span class="na">onclick=</span><span class="s">"location.href='/article/{{ article.pk }}/'"</span><span class="nt">></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td><td></span>{{ article.title }}<span class="nt"></td><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %}
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/article/create/"</span><span class="nt">><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"button"</span><span class="nt">></span>새 게시글 작성<span class="nt"></button></a></span>
{% endblock content %}
</code></pre></div></div>
<p>부트스트랩 css와 마찬가지로 헤더에 <code class="language-plaintext highlighter-rouge"><link rel="stylesheet" href="/static/css/bbs.css"></code> 라고 표기하면 <code class="language-plaintext highlighter-rouge">'/static/css/bbs.css'</code> 파일을 css로서 사용하겠다는 선언입니다. 이렇게 해도 지금은 잘 동작하지만 문제는 여러분의 앱이 다른 프로젝트에서 사용될 때 해당 프로젝트의 <code class="language-plaintext highlighter-rouge">STATIC_URL</code> 설정이 '/static/' 이 아니면 어떻합니까? 그러면 앱의 모든 템플릿을 override 해야 하는데 그렇게 하기에는 너무 번거로운 일이 될 것입니다. 장고에서는 설정파일의 <code class="language-plaintext highlighter-rouge">STATIC_URL</code> 설정을 신경쓰지 않고 유연하게 static 파일을 설정할 수 있는 템플릿태그를 제공합니다. static 이라는 태그인데 href 속성에 static 템플릿태그를 이용해서 설정파일의 <code class="language-plaintext highlighter-rouge">STATIC_URL</code> + '/css/bbs.css' 로 변환됩니다. 즉, <code class="language-plaintext highlighter-rouge">{% static '/css/bbs.css' %}</code> 는 settings.<code class="language-plaintext highlighter-rouge">STATIC_URL + '/css/bbs.css'</code> 와 같습니다. static 템플릿태그 사용할 때는 반드시 템플릿상단에 <code class="language-plaintext highlighter-rouge">{% load static %}</code> 를 추가하셔서 템플릿엔진에서 static 템플릿태그를 사용할 수 있게 로딩해줘야 합니다.
{% raw %}</p>
<p>분리된 css 는 <code class="language-plaintext highlighter-rouge">bbs/static/css/bbs.css</code> 파일에 그대로 붙여넣기 해줍니다. 단 css 파일 내부에는 <code class="language-plaintext highlighter-rouge"><style></style></code> 태그가 있어서는 안됩니다. 순수 css 만 넣어주세요.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* bbs/static/css/bbs.css */</span>
<span class="nt">tbody</span> <span class="o">></span> <span class="nt">tr</span> <span class="p">{</span><span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;}</span>
</code></pre></div></div>
<blockquote>
<p>실제 서비스에서 css, javascript, image 같이 정적인(static) 파일들은 장고를 통하지 않고 nginx 와 같은 웹서버가 직접 파일을 읽어서 전송하는 것이 효율적입니다. <strong>장고에 비해 nginx 의 성능은 뛰어나며 동시에 여러 요청을 처리할 수 있기 때문에 단순한 파일 전송은 웹서버가 직접 처리</strong>하는 것입니다. 웹서버에 특정 파일들은 직접 처리하라는 설정을 해줘야 하는데 모든 파일들을 일일이 설정하기에는 복잡하고, 앱마다 정적파일들이 저장된 디렉토리가 다르기 때문에 웹서버에 설정하기가 쉽지 않습니다. 장고는 <code class="language-plaintext highlighter-rouge">collectstatic</code> 이라는 커맨드를 통해 각 앱의 static 디렉토리와 설정파일에 설정된 <code class="language-plaintext highlighter-rouge">STATICFILES_DIRS</code> 디렉토리들을 하나의 디렉토리에 모아둡니다. 이 때 동일한 경로에 동일한 경로의 동일한 이름의 파일이 있으면 오류가 발생합니다. 그래서 css 이름을 서로 다르게 만들었습니다. 이렇게 여러 앱들의 정적파일들이 하나의 디렉토리에 모이게 되면 웹서버에서 하나의 <code class="language-plaintext highlighter-rouge">urlpattern</code> 만 설정해도 정적파일들을 처리할 수 있습니다. 이후 <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드를 설명할 때 자세히 설명하겠습니다.</p>
</blockquote>
<p>그 다음으로 user 앱의 css 를 분리합니다. <code class="language-plaintext highlighter-rouge">login_form.html</code>, <code class="language-plaintext highlighter-rouge">resend_verify_email.html</code>, <code class="language-plaintext highlighter-rouge">user_form.html</code> 세 템플릿이 거의 비슷한 css를 가지고 있습니다. 이것을 하나의 css(user.css)에서 모아두면 효율적일 것 같습니다. css의 selector로 클래스 <code class="language-plaintext highlighter-rouge">registration</code> 이 있는데 세 템플릿 모두에게 적용하기에는 일반적인 용어는 아닙니다. 그래서 좀 더 추상적이고 일반화된 이름(<code class="language-plaintext highlighter-rouge">user-panel</code>)으로 변경합니다. <code class="language-plaintext highlighter-rouge">login_form.html</code> 에는 css 코드가 한 줄 더 있는데 이것도 user.css 파일에 추가합니다.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* user/static/css/user.css */</span>
<span class="p">{</span><span class="err">%</span> <span class="err">raw</span> <span class="err">%</span><span class="p">}</span>
<span class="nc">.user-panel</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">360px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.control-label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.user-panel</span> <span class="nc">.form-actions</span> <span class="o">></span> <span class="nt">button</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.link-below-button</span> <span class="p">{</span> <span class="nl">margin-top</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span> <span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;}</span>
</code></pre></div></div>
<p>당연히 각 템플릿의 css 블록은 <code class="language-plaintext highlighter-rouge">{{ block.super }}</code> 와 <code class="language-plaintext highlighter-rouge"><link rel="stylesheet" href=" {% static '/css/user.css' %}"></code> 만 남기고 모두 삭제합니다.</p>
<h2 id="2-템플릿-분리">2. 템플릿 분리</h2>
<p>회원가입과 로그인의 템플릿이 거의 동일하게 생겨서 왠지 하나의 템플릿을 사용해도 될 것 같습니다. 하나의 템플릿만으로도 회원가입뷰에서 렌더링할 때는 회원가입으로 보이고, 로그인뷰에서 사용할 때는 로그인으로 보이도록 if-else 템플릿태그를 사용하면 가능하긴 합니다만 그렇게 하지 않을 예정입니다. 혹시나 비슷한 페이지들이 늘어나고 약간씩 달라지는 경우가 많이 있을 수 있는데 그럴 때마다 분기 처리를 한다면 템플릿자체가 너무 복잡하고 나중에는 유지보수 하기 힘들어지는 경우가 많이 있기 때문입니다. 중복되는 부분은 템플릿을 따로 분리해서 필요한 템플릿에서 추가해서 사용하도록 하겠습니다.</p>
<h3 id="템플릿-include">템플릿 include</h3>
<p>user 앱에서 각 템플릿들의 공통된 부분들을 따로 분리하는데 이번에는 <code class="language-plaintext highlighter-rouge">extends</code> 템플릿태그로 상속받는 것이 아니라 <strong><code class="language-plaintext highlighter-rouge">include</code> 템플릿태그를 이용해서 템플릿의 일부를 분리</strong>시키는 방법을 사용해보도록 하겠습니다. 먼저 예시로<code class="language-plaintext highlighter-rouge"> user_form.html</code> 템플릿의 <code class="language-plaintext highlighter-rouge">for</code> 블록을 분리합니다. <code class="language-plaintext highlighter-rouge">form_field.html</code> 템플릿 파일을 생성해서 분리한 내용을 저장합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/partials/form_field.html --></span>
{% for field in form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group {% if field.errors|length > 0 %}has-error{%endif %}"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"{{ field.html_name }}"</span> <span class="na">id=</span><span class="s">"{{ field.id_for_label }}"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">type=</span><span class="s">"{{ field.field.widget.input_type }}"</span> <span class="na">value=</span><span class="s">"{{ field.value|default_if_none:'' }}"</span><span class="nt">></span>
{% for error in field.errors %}
<span class="nt"><label</span> <span class="na">class=</span><span class="s">"control-label"</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ error }}<span class="nt"></label></span>
{% endfor %}
<span class="nt"></div></span>
{% endfor %}
</code></pre></div></div>
<p>이제는 <code class="language-plaintext highlighter-rouge">user_form.html</code> 템플릿에서 분리된 내용을 삭제하고 <code class="language-plaintext highlighter-rouge">include</code> 템플릿태그를 이용해서 분리한 템플릿을 불러서 삽입합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/user_form.html --></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>삽입되는 템플릿에 데이터(argument)를 전달할 때는 with 구문으로 인자를 넘겨줄 수 있습니다.(<code class="language-plaintext highlighter-rouge">with form=form</code>) form=form 의 좌변은 keyword argument 의 키이름이고, 우변은 전달시킬 데이터입니다. <code class="language-plaintext highlighter-rouge">form=form</code> 이라고 값을 넘겨주면 <code class="language-plaintext highlighter-rouge">form_field.html</code> 에서 form 이라는 데이터를 동일한 이름의 파라미터로 사용하도록 전달한다는 의미입니다.</p>
<p>복잡한 <code class="language-plaintext highlighter-rouge">if-else</code> 템플릿태그 없이 user 앱의 세가지 템플릿의 중복을 이렇게 최소화했습니다. 아까 설명했던 것처럼 하나의 템플릿을 사용하고 중간에 텍스트 몇개만 <code class="language-plaintext highlighter-rouge">if-else</code> 로 변경할 수도 있겠지만 <strong><code class="language-plaintext highlighter-rouge">if-else</code> 가 가독성에 그다지 좋지 않다는 개인적인 의견이 있고, 앞으로 더 복잡하게 변할 수 있다</strong>는 생각이 있어 이렇게 분리하는 리팩토링을 했습니다.</p>
<p>마지막으로 템플릿 이름을 좀 일관성 있게 변경하도록 하겠습니다. <code class="language-plaintext highlighter-rouge">user_form.html</code> 을 <code class="language-plaintext highlighter-rouge">registration_form.html</code> 으로 변경하고 <code class="language-plaintext highlighter-rouge">resend_verify_email.html</code> 을 <code class="language-plaintext highlighter-rouge">resend_verify_form.html</code> 으로 변경하고 각 뷰에도 변경된 템플릿 이름으로 설정하도록 하겠습니다.<br />
변경된 템플릿은 정리된 내용을 보고 맞게 리팩토링이 됐는지 확인해보세요. 뷰에서 <code class="language-plaintext highlighter-rouge">template_name</code> 변수를 변경하는 것은 직접 변경해보세요.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/registration_form.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static '/css/user.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/resend_verify_form.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<span class="nt"><title></span>인증이메일 재발송<span class="nt"></title></span>{% endblock %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static '/css/user.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
인증이메일 발송
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
<span class="nt"><b</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>재발송할 이메일주소를 입력해주세요.<span class="nt"></b></span>
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>인증이메일 발송<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/login_form.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<span class="nt"><title></span>로그인<span class="nt"></title></span>{% endblock %}
{% block css %}
{{ block.super }}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{% static '/css/user.css' %}"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default user-panel"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
로그인하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% include 'user/partials/form_field.html' with form=form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>로그인하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/resend_verify_email/"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"link-below-button"</span><span class="nt">></span>인증이메일 재발송<span class="nt"></div></span>
<span class="nt"></a></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<h2 id="3-static-파일-모으기">3. STATIC 파일 모으기</h2>
<p>위에서 몇번 소개해드린 대로 <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드를 이용해 각 앱의 static 파일들을 하나의 디렉토리에 모아둘 수 있습니다. <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드를 실행하기 위해서는 <a href="#static-설정">설정</a> 이 되어 있어야 합니다. <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드는 설정파일의 <code class="language-plaintext highlighter-rouge">STATICFILES_FINDERS</code> 로 추가된 탐색기에게 static 파일들을 검색을 위임합니다. 기본으로 추가되어 있는 검색기의 기능을 간단히 살펴보겠습니다.</p>
<ol>
<li>'django.contrib.staticfiles.finders.FileSystemFinder' - <code class="language-plaintext highlighter-rouge">STATICFILES_DIRS</code> 에 설정된 디렉토리들를 검색합니다.</li>
<li>'django.contrib.staticfiles.finders.AppDirectoriesFinder' - 각 앱의 static 디렉토리를 검색합니다.</li>
</ol>
<p><code class="language-plaintext highlighter-rouge">FileSystemFinder</code> 는 bower 등을 통해서 프론트엔드 라이브러리(css, js)들을 관리할 경우 앱과는 별도의 위치에 저장하는데 이럴 때 <code class="language-plaintext highlighter-rouge">STATICFILES_DIRS</code> 에 해당 디렉토리들을 추가해서 static 파일들을 모을 수 있게 합니다.<br />
<code class="language-plaintext highlighter-rouge">AppDirectoriesFinder</code> 는 항상 사용하도록 하고 앱에서 생성한 static 파일들은 항상 앱 내부의 static 디렉토리에 저장하고 static 디렉토리 내에 앱이름의 디렉토리를 다시 만들어 <code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드로 파일들이 모일 때 이름이 겹쳐져서 오류가 나지 않도록 해야 합니다.</p>
<p>더 깊은 설명보다는 실제로 collectstatic 커맨드를 실행해서 실제로 잘 모이는 지 확인해봅니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py collectstatic
125 static files copied to <span class="s1">'/var/www/static'</span><span class="nb">.</span>
<span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span><span class="nb">ls</span> /var/www/static/
admin bbs user
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">collectstatic</code> 커맨드를 실행할 경우 오류가 발생하지 않으면 몇개의 파일이 복사가 되었는지 결과 화면에 출력합니다. 저장되는 디렉토리의 파일리스트를 보면 admin, bbs, user 디렉토리가 출력이됩니다. admin 프레임워크도 앱이기 때문에 admin 앱의 정적파일들도 자동 수집이 됩니다.</p>
<p>만일 모이는 파일들이 경로와 이름이 겹치게 되면 아래와 같은 오류가 발생합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py collectstatic
Found another file with the destination path <span class="s1">'css/common.css'</span><span class="nb">.</span> It will be ignored since only the first encountered file is collected. If this is not what you want, make sure every static file has a unique path.
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">STATICFILES_DIRS</code> 설정을 할 때 디렉토리들을 튜플로 설정하는데, 만일 추가할 디렉토리가 1개일 때 쉼표(,)를 생략할 경우도 오류가 발생합니다. 해당 오류가 발생하면 아래와 같은 메시지가 출력됩니다. 이럴경우 <code class="language-plaintext highlighter-rouge">STATICFILES_DIRS에</code> 설정된 디렉토리명 뒤에 쉼표(,)를 추가해주시면 됩니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py collectstatic
SystemCheckError: System check identified some issues:
ERRORS:
?: <span class="o">(</span>staticfiles.E001<span class="o">)</span> The STATICFILES_DIRS setting is not a tuple or list.
HINT: Perhaps you forgot a trailing comma?
</code></pre></div></div>
<blockquote>
<p>파이썬에서 튜플의 <strong>원소갯수가 1개일 경우 마지막 원소 뒤에 쉼표(,)를 표시하지 않으면 자동으로 튜플을 언패킹</strong>합니다. 이 점 주의하세요.</p>
</blockquote>
<p>개발업무를 하다보면 바쁘기도 하고, 귀찮기도 하고, 때로는 더 좋은 방법을 몰라서 당시에 편한 방법으로 개발을 하곤 합니다. 하지만 이것을 그대로 놔두면 언젠가는 스파게티가 되기 때문에 스파게티가 되기 전에 정기적으로 리팩토링을 검토해보세요.</p>
<blockquote>
<p>컴퓨터가 이해할 수 있는 코드는 어느 바보나 다 짤 수 있다. 좋은 프로그래머는 사람이 이해할 수 있는 코드를 짠다.</p>
<p>마틴 파울러, 이 글귀는 어느 것 하나 손댈 수 없다.</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크에서 템플릿의 리팩토링을 예제를 통해 배워봅니다.사용자인증(3)2018-12-14T00:00:00+09:002018-12-14T00:00:00+09:00https://swarf00.github.io/2018/12/14/logout<h2 id="1-이메일인증">1. 이메일인증</h2>
<p>회원가입을 할 때 이메일이 중복이 되지 않는다면 조건없이 가입이 되었습니다. 간혹 실수 혹은 고의로 이메일 주소를 실존하지 않는 이메일로 입력하거나 누군가 다른 사람의 이메일로 가입하는 경우도 있을 수 있습니다. 일반적으로 가입 직후 이메일을 회원의 아이디로 사용하는 경우 가입된 이메일로 인증이메일을 보내고 인증이메일의 인증하기 버튼을 클릭할 경우 인증이 되고 아이디를 사용할 수 있도록 변경해줍니다. 이메일인증은 장고에서 제공하지 않는 기능이지만 내장된 이메일 전송기능을 활용해서 비교적 간단하게 이메일 인증기능을 구현해봅니다.</p>
<p>이메일인증 기능을 구현하기 전에 어떤 순서를 통해 인증이 되는지 설계를 하고 각 단계를 하나씩 구현해 나가도록 하겠습니다.</p>
<ol>
<li>가입즉시 인증이메일 보내기</li>
<li>인증토큰 생성</li>
<li>사용자인증 페이지로 이동할 수 있는 링크를 포함한 인증이메일 발송</li>
<li>인증이메일에서 인증하기 링크 클릭 후 사용자인증 페이지로 이동</li>
<li>사용자인증 페이지에서 url에 포함된 사용자id와 인증토큰을 비교해서 인증</li>
<li>정상적인 사용자인 경우 is_active 를 True 로 변경 후 인증완료 화면 표시</li>
<li>비정상적인 사용자인 경우 인증실패 화면 표시하여 재인증 가능한 링크 제공</li>
<li>인증되지 않은 사용자의 경우 인증이메일 재발송 가능하도록 링크 제공</li>
</ol>
<h3 id="이메일보내기">이메일보내기</h3>
<p>이메일 인증 기능을 보내려면 우선 장고에서 이메일을 보낼 수 있어야 합니다. 이메일을 보내기 위해서 설정파일에 몇가지 설정을 하고, 이메일 템플릿을 작성만 하면 됩니다.</p>
<p>gmail 기준으로 설정하는 방법을 설명드립니다. gmail 이외에도 대부분의 이메일 서비스에서 아래와 같은 설정내용을 제공하니 잘 찾아보시기 바랍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">EMAIL_HOST</span> <span class="o">=</span> <span class="s">'smtp.gmail.com'</span>
<span class="n">EMAIL_PORT</span> <span class="o">=</span> <span class="mi">587</span>
<span class="n">EMAIL_HOST_USER</span> <span class="o">=</span> <span class="s">'awesome@gmail.com'</span>
<span class="n">EMAIL_HOST_PASSWORD</span> <span class="o">=</span> <span class="s">'7h1515myp455w0rd'</span>
<span class="n">EMAIL_USE_TLS</span> <span class="o">=</span> <span class="bp">True</span>
</code></pre></div></div>
<p>gmail을 사용하신다면 다른 부분은 모두 동일하고 <code class="language-plaintext highlighter-rouge">EMAIL_HOST_USER</code>, <code class="language-plaintext highlighter-rouge">EMAIL_HOST_PASSWORD</code> 만 자신의 계정의 맞는 값으로 설정하세요.</p>
<blockquote>
<p>(534, b'5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbvz\n5.7.14 Ss4-Ru3haJUuYoQ3nIS14Mkq0Coxa7Tq0uR1kRepwx–S7Vt_uiyHZNq9UExqZb5w5Yjvk\n5.7.14 jKWrk_N2A3c9SIwf9f6Xb44Tluq2ygOmBBGUXW8nb_MV60BO0rWStZkKG9ti830JXJwfhw\n5.7.14 TXT7CybJO0k-oRO3pER74z6Y5cUctpv50uLkb-R7ViW2w93tu2FVp5STDlpwg_QMy3Wl5H\n5.7.14 _kaLsuezAqNG5-w-Oqg0ZPE8OTxgs1PA0_cxeORoSQseNivNXQ> Please log in via\n5.7.14 your web browser and then try again.\n5.7.14 Learn more at\n5.7.14 https://support.google.com/mail/answer/78754 d21sm2862209pgv.37 - gsmtp')</p>
<p>이런 에러가 발생했다면 <a href="https://myaccount.google.com/lesssecureapps?pli=1">gmail 설정</a> 에서 <code class="language-plaintext highlighter-rouge">보안 수준이 낮은 앱 허용</code> 을 활성화시켜주세요. <strong>google 은 google 앱이 아닌 앱에서 로그인을 시도할 경우 차단하는 것이 기본설정입니다.</strong></p>
</blockquote>
<p>shell 커맨드에서 이메일이 잘 발송되는 지 테스트해봅니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">>>></span> <span class="kn">from</span> <span class="nn">minitutorial</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="o">>>></span> <span class="kn">from</span> <span class="nn">user.models</span> <span class="kn">import</span> <span class="n">User</span>
<span class="o">>>></span> <span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="s">'swarf00@gmail.com'</span><span class="p">)</span> <span class="c1"># 회원가입된 사용자
</span><span class="o">>>></span> <span class="n">user</span><span class="o">.</span><span class="n">email_user</span><span class="p">(</span><span class="s">'test'</span><span class="p">,</span> <span class="s">'this is a test'</span><span class="p">,</span> <span class="n">from_email</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">EMAIL_HOST_USER</span><span class="p">)</span> <span class="c1"># 제목, 본문
</span></code></pre></div></div>
<p>확인해보시면 여기까지 문제없이 이메일이 발송되고 몇 초 이내에 이메일을 받아보실 수 있을 겁니다. 그럼 실제로 뷰에 관련 내용을 구현해보도록 합니다.</p>
<h3 id="이메일인증-재발송-구현하기">이메일인증, 재발송 구현하기</h3>
<p>auth 프레임워크의 <strong>모델 백엔드는 <code class="language-plaintext highlighter-rouge">is_active</code> 가 <code class="language-plaintext highlighter-rouge">True</code> 인 사용자만 정상적인 사용자로 인증</strong>합니다. 기존에는 이메일인증 기능이 없었기 때문에 가입과 동시에 <code class="language-plaintext highlighter-rouge">is_active</code> 값이 <code class="language-plaintext highlighter-rouge">True</code> 로 저장되도록 했었는데 이메일인증이 되기 전까지 <code class="language-plaintext highlighter-rouge">False</code> 로 저장을 하도록 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/models.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">):</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">EmailField</span><span class="p">(</span><span class="n">verbose_name</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s">'email address'</span><span class="p">),</span> <span class="n">unique</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">'name'</span><span class="p">),</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">is_staff</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span>
<span class="n">_</span><span class="p">(</span><span class="s">'staff status'</span><span class="p">),</span>
<span class="n">default</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="n">help_text</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s">'Designates whether the user can log into this admin site.'</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">is_active</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span>
<span class="n">_</span><span class="p">(</span><span class="s">'active'</span><span class="p">),</span>
<span class="n">default</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="c1"># 기본값을 False 로 변경
</span> <span class="n">help_text</span><span class="o">=</span><span class="n">_</span><span class="p">(</span>
<span class="s">'Designates whether this user should be treated as active. '</span>
<span class="s">'Unselect this instead of deleting accounts.'</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="c1"># 생략
</span>
</code></pre></div></div>
<p>가입 즉시 이메일인증 메일을 발송하도록 <code class="language-plaintext highlighter-rouge">UserRegistrationView</code> 의 <code class="language-plaintext highlighter-rouge">form_valid</code> 메소드를 오버라이드 합니다. <strong><code class="language-plaintext highlighter-rouge">form_valid</code> 메소드는 폼객체의 필드값들이 유효성 검증을 통과할 경우 호출</strong>되는데 각 필드의 값을 데이터베이스에 저장하는 역할을 합니다. 즉 <code class="language-plaintext highlighter-rouge">form_valid</code> 메소드가 실행 후 이메일인증 메일을 발송하면 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">messages</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.tokens</span> <span class="kn">import</span> <span class="n">default_token_generator</span>
<span class="kn">from</span> <span class="nn">minitutorial</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">verify_url</span> <span class="o">=</span> <span class="s">'/user/verify/'</span>
<span class="n">token_generator</span> <span class="o">=</span> <span class="n">default_token_generator</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">send_verification_email</span><span class="p">(</span><span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">def</span> <span class="nf">send_verification_email</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
<span class="n">token</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">token_generator</span><span class="o">.</span><span class="n">make_token</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">email_user</span><span class="p">(</span><span class="s">'회원가입을 축하드립니다.'</span><span class="p">,</span> <span class="s">'다음 주소로 이동하셔서 인증하세요. {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">build_verification_link</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">)),</span> <span class="n">from_email</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">EMAIL_HOST_USER</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_verification_link</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'{}/user/{}/verify/{}/'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">META</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'HTTP_ORIGIN'</span><span class="p">),</span> <span class="n">user</span><span class="o">.</span><span class="n">pk</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">UserRegistrationForm</code> 는 <code class="language-plaintext highlighter-rouge">ModelForm</code> 을 상속받은 클래스인데 <code class="language-plaintext highlighter-rouge">form_valid</code> 메소드를 호출하면 데이터베이스에 저장(<code class="language-plaintext highlighter-rouge">Form.save()</code>)을 하고 저장된 데이터를 폼객체의 <code class="language-plaintext highlighter-rouge">instance</code> 변수에 저장을 합니다. 그래서 <code class="language-plaintext highlighter-rouge">token</code> 을 생성할 때 이 <code class="language-plaintext highlighter-rouge">form.instance</code> 를 이용하도록 했습니다. (장고의 내장 뷰에서는 폼클래스에서 <code class="language-plaintext highlighter-rouge">save()</code> 메소드 호출 직후 처리를 하기도 합니다.)
<strong><code class="language-plaintext highlighter-rouge">default_token_generator</code> 는 사용자 데이터를 가지고 해시데이터를 만들어주는 객체</strong>인데 이것을 이용해서 생성된 사용자 고유의 토큰을 생성합니다. 생성된 <code class="language-plaintext highlighter-rouge">토큰</code>과 <code class="language-plaintext highlighter-rouge">사용자id(pk)</code> 값을 <code class="language-plaintext highlighter-rouge">인증페이지의 url</code>에 포함하여 어떤 사용자의 토큰인지 <code class="language-plaintext highlighter-rouge">url</code>만 보고 확인할 수 있도록 합니다.<br />
이제 실제로 가입하기를 해보면 이메일이 발송되고 이메일 내용에 <code class="language-plaintext highlighter-rouge">다음 주소로 이동하셔서 인증하세요. http://localhost:8000/user/9/verify/524-08f72f288f9f86d04084/</code> 와 같이 매번 다른 토큰값(524-08f72f288f9f86d04084)을 인증페이지의 <code class="language-plaintext highlighter-rouge">url</code>에 포함시킵니다. 물론 이 링크를 클릭할 경우 아직은 이동할 페이지가 없다는 오류가 발생됩니다.</p>
<h3 id="인증페이지-생성">인증페이지 생성</h3>
<p>이제 인증이메일의 링크를 클릭했을 때 이동할 인증페이지를 만들어야 합니다. 먼저 인증뷰를 생성합니다. 인증뷰는 <strong><code class="language-plaintext highlighter-rouge">url</code>의<code class="language-plaintext highlighter-rouge"> 사용자id</code> 값과 <code class="language-plaintext highlighter-rouge">token</code> 을 가지고 해당 사용자의 정상적인 <code class="language-plaintext highlighter-rouge">token</code> 값인지 확인</strong> 후 정상적인 경우 로그인페이지(또는 웰컴페이지)로 이동시키고 인증이 완료되었다는 메시지를 출력시켜주면 됩니다. 정상적이지 않은 경우 인증실패 메시지와 인증메일을 재발송할 수 있도록 링크를 추가하면 됩니다. 어차피 나중에 로그인 페이지에서 인증이메일 재발송 기능을 추가할 예정이니 인증실패시 로그인페이지로 이동할 수 있는 링크를 제공해주도록 하겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponseRedirect</span>
<span class="kn">from</span> <span class="nn">django.views.generic.base</span> <span class="kn">import</span> <span class="n">TemplateView</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">UserVerificationView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">redirect_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">token_generator</span> <span class="o">=</span> <span class="n">default_token_generator</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">is_valid_token</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">'인증이 완료되었습니다.'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">'인증이 실패되었습니다.'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">redirect_url</span><span class="p">)</span> <span class="c1"># 인증 성공여부와 상관없이 무조건 로그인 페이지로 이동
</span>
<span class="k">def</span> <span class="nf">is_valid_token</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">pk</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'pk'</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'tonen'</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span>
<span class="n">is_valid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">token_generator</span><span class="o">.</span><span class="n">check_token</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="k">if</span> <span class="n">is_valid</span><span class="p">:</span>
<span class="n">user</span><span class="o">.</span><span class="n">is_active</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">user</span><span class="o">.</span><span class="n">save</span><span class="p">()</span> <span class="c1"># 데이터가 변경되면 반드시 save() 메소드 호출
</span> <span class="k">return</span> <span class="n">is_valid</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p><strong>토큰의 유효성 확인도 <code class="language-plaintext highlighter-rouge">default_token_generator</code> 를 이용</strong>합니다. 유효한 토큰일 경우 사용자의 <code class="language-plaintext highlighter-rouge">is_active</code> 를 <code class="language-plaintext highlighter-rouge">True</code> 로 변경시키고 저장해야 합니다. 주의할 것은 <strong>인증에 실패했다고 <code class="language-plaintext highlighter-rouge">is_active</code> 를 <code class="language-plaintext highlighter-rouge">False</code> 로 변경시키면 안됩니다</strong>. 혹시나 악의적인 목적으로 <code class="language-plaintext highlighter-rouge">url</code> 을 난수로 대입할 경우 정상적인 사용자id와 충돌이 생겨 인증상태가 변경될 수도 있으니 인증이 실패할 경우는 그대로 무시하고, 다만 실패되었다는 메시지만 출력해주는 것으로 확인시켜주면 됩니다.</p>
<blockquote>
<p>인증이 성공할 경우 곧바로 인증세션정보를 생성해서(<code class="language-plaintext highlighter-rouge">django.contrib.auth.login()</code>) 로그인된 것으로 처리한다면 사용자 입장에서는 좀 편리할 수 있으나 그만큼 보안강도가 약해지는 것이기 때문에 별도로 로그인을 하도록 유도하는 것이 보안에 좀 더 좋은 방법입니다.</p>
</blockquote>
<p>인증뷰의 핸들러를 호출할 수 있도록 <code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 추가합니다. <code class="language-plaintext highlighter-rouge">url</code> 에는 사용자id(pk) 와 토큰(token)이 포함되도록 선언하면 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">user.views</span> <span class="kn">import</span> <span class="n">UserRegistrationView</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="p">,</span> <span class="n">UserVerificationView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">'article-list'</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/<pk>/verify/<token>/'</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/login/'</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>이제 정상적으로 인증이 되는지 이메일의 링크를 클릭하고 이동된 로그인 페이지에서 로그인을 해봅니다. 정상적으로 로그인이 되었다면 한가지 작업을 더 해주면 좋을 것 같습니다.</p>
<h3 id="이메일-템플릿-생성">이메일 템플릿 생성</h3>
<p>이메일 본문에 일반 텍스트로만 전달하다보니 이메일의 내용이 신뢰가 가지 않고 일부 이메일 클라이언트에서는 링크주소를 클릭할 경우 아무런 반응이 없을 때도 있습니다. 이메일도 미리 만들어놓은 템플릿에 사용자별로 인증코드만 수정해서 보내면 좀 더 좋을 것 같습니다.<br />
이메일에는 css가 적용되지 않는 경우가 많으니 <strong>반드시 태그안에 inline style 속성으로 디자인</strong>해야 합니다. 뉴스레터 디자인을 위한 여러 종류에 에디터가 있으나 저는 <a href="https://grapesjs.com/demo-newsletter-editor.html">grapejs</a> 를 통해 디자인을 했습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/user/registration_verification.html --></span>
<span class="nt"><table</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; width: 100%; height: 100%; background-color: rgb(234, 236, 237); font-family: Arial Black, Gadget, sans-serif;"</span> <span class="na">width=</span><span class="s">"100%"</span> <span class="na">height=</span><span class="s">"100%"</span> <span class="na">bgcolor=</span><span class="s">"rgb(234, 236, 237)"</span><span class="nt">></span>
<span class="nt"><tbody</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><tr</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; vertical-align: top;"</span> <span class="na">valign=</span><span class="s">"top"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><table</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-family: Helvetica, serif; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-top: auto; margin-right: auto; margin-bottom: auto; margin-left: auto; height: 0px; width: 90%; max-width: 550px;"</span> <span class="na">width=</span><span class="s">"90%"</span> <span class="na">height=</span><span class="s">"0"</span><span class="nt">></span>
<span class="nt"><tbody</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><tr</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; vertical-align: top; font-size: medium; padding-bottom: 50px;"</span> <span class="na">valign=</span><span class="s">"top"</span><span class="nt">></span>
<span class="nt"><table</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-bottom: 20px; height: 0px;"</span> <span class="na">height=</span><span class="s">"0"</span><span class="nt">></span>
<span class="nt"><tbody</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><tr</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; background-color: rgb(255, 255, 255); overflow-x: hidden; overflow-y: hidden; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; text-align: center;"</span> <span class="na">bgcolor=</span><span class="s">"rgb(255, 255, 255)"</span> <span class="na">align=</span><span class="s">"center"</span><span class="nt">></span>
<span class="nt"><table</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; width: 100%; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; height: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; border-collapse: collapse;"</span> <span class="na">width=</span><span class="s">"100%"</span> <span class="na">height=</span><span class="s">"0"</span><span class="nt">></span>
<span class="nt"><tbody</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><tr</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-size: 13px; line-height: 20px; color: rgb(111, 119, 125); padding-top: 10px; padding-right: 20px; padding-bottom: 0px; padding-left: 20px; vertical-align: top;"</span> <span class="na">valign=</span><span class="s">"top"</span><span class="nt">></span>
<span class="nt"><h1</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-size: 25px; font-weight: 300; color: rgb(68, 68, 68);"</span><span class="nt">></span>
<span class="nt"><span</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-family: Arial,Helvetica,sans-serif;"</span><span class="nt">></span>가입을 환영합니다.!!<span class="nt"></span></span>
<span class="nt"></h1></span>
<span class="nt"><p</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><span</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-family: Arial,Helvetica,sans-serif;"</span><span class="nt">></span>회원님이 가입하신 것이 맞다면 아래 "인증하기" 버튼을 눌러서 인증해주세요. 가입하신 적이 없을 경우 인증하기를 누르지 마시고 무시하세요.<span class="nt"></span></span>
<span class="nt"></p></span>
<span class="nt"><table</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; margin-top: 0px; margin-right: auto; margin-bottom: 10px; margin-left: auto; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; width: 100%;"</span> <span class="na">width=</span><span class="s">"100%"</span><span class="nt">></span>
<span class="nt"><tbody</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><tr</span> <span class="na">style=</span><span class="s">"box-sizing: border-box;"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; padding-top: 20px; padding-right: 0px; padding-bottom: 20px; padding-left: 0px; text-align: center;"</span> <span class="na">align=</span><span class="s">"center"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"{{ url }}"</span> <span class="na">class=</span><span class="s">"button"</span> <span class="na">style=</span><span class="s">"box-sizing: border-box; font-size: 16px; padding-top: 10px; padding-right: 20px; padding-bottom: 10px; padding-left: 20px; background-color: rgb(217, 131, 166); color: rgb(255, 255, 255); text-align: center; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; font-family: Arial, Helvetica, sans-serif; font-weight: 500; text-decoration: underline;"</span><span class="nt">></span>인증하기<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
</code></pre></div></div>
<p>이메일의 템플릿은 일반 html 의 head 와 body 없이 body 안에 보여줘야 할 부분만 있으면 됩니다. grapejs 에서는 알아서 잘 만들어주기 때문에 그대로 복사해왔습니다. 다만 <strong>인증하기 버튼의 url 부분만 {{ url }} 로 변경</strong>해서 렌더링할 때마다 원하는 값으로 링크주소가 변경될 수 있도록 수정했습니다.</p>
<p>뷰에서는 템플릿을 렌더링해서 사용자마다 인증코드를 적용해서 보여주도록 수정해야 합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">render</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">verify_url</span> <span class="o">=</span> <span class="s">'/user/verify/'</span>
<span class="n">email_template_name</span> <span class="o">=</span> <span class="s">'user/email/registration_verification.html'</span>
<span class="n">token_generator</span> <span class="o">=</span> <span class="n">default_token_generator</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">send_verification_email</span><span class="p">(</span><span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">def</span> <span class="nf">send_verification_email</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
<span class="n">token</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">token_generator</span><span class="o">.</span><span class="n">make_token</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">build_verification_link</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="n">subject</span> <span class="o">=</span> <span class="s">'회원가입을 축하드립니다.'</span>
<span class="n">message</span> <span class="o">=</span> <span class="s">'다음 주소로 이동하셔서 인증하세요. {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">html_message</span> <span class="o">=</span> <span class="n">render</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">email_template_name</span><span class="p">,</span> <span class="p">{</span><span class="s">'url'</span><span class="p">:</span> <span class="n">url</span><span class="p">})</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">email_user</span><span class="p">(</span><span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">settings</span><span class="o">.</span><span class="n">EMAIL_HOST_USER</span><span class="p">,</span> <span class="n">html_message</span><span class="o">=</span><span class="n">html_message</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_verification_link</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'{}/user/{}/verify/{}/'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">META</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'HTTP_ORIGIN'</span><span class="p">),</span> <span class="n">user</span><span class="o">.</span><span class="n">pk</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>유심히 봐야 할 부분은 <code class="language-plaintext highlighter-rouge">render</code> 함수 호출부분인데, <strong><code class="language-plaintext highlighter-rouge">render</code> 함수는 <code class="language-plaintext highlighter-rouge">HttpResponse</code> 객체를 반환</strong>합니다. <code class="language-plaintext highlighter-rouge">HttpResponse</code> 객체의 <code class="language-plaintext highlighter-rouge">content</code> 속성에 렌더링된 메시지가 저장되어 있는데 <code class="language-plaintext highlighter-rouge">http</code>로 전송할 수 있도록 <code class="language-plaintext highlighter-rouge">byte</code> 로 인코딩되어 있습니다. <code class="language-plaintext highlighter-rouge">email_user</code> 메소드에 전달할 때 반드시 <strong><code class="language-plaintext highlighter-rouge">utf-8</code> 로 디코딩</strong>을 해줘야 합니다. <code class="language-plaintext highlighter-rouge">email_user</code> 메소드를 호출할 때 기존의 텍스트 메시지와 <code class="language-plaintext highlighter-rouge">html</code> 메시지 둘다 전달한 이유는 일부 이메일 클라이언트에서는 <code class="language-plaintext highlighter-rouge">html</code> 형식의 이메일을 지원하지 않을 수 있어서 <code class="language-plaintext highlighter-rouge">html</code> 메시지를 보여줄 수 없는 이메일 클라이언트에게 기본적으로 보여줄 내용으로 텍스트 메시지를 전달하는 것이 좋습니다.</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">django.template.loader.render_to_string</code> 함수를 이용하면 곧바로 렌더링된 <code class="language-plaintext highlighter-rouge">utf-8</code> 문자열을 출력합니다.</p>
</blockquote>
<h3 id="인증이메일-재발송">인증이메일 재발송</h3>
<p>어떠한 이유로 인증이메일을 삭제해서 복구가 불가능하거나 인증이메일에 문제가 생겼을 경우가 간혹 있을 수 있습니다. 그럴 경우 인증이메일을 재전송해서 이메일인증을 할 수 있도록 해야 합니다. 로그인 화면에서 인증메일 발송 링크를 추가해서 로그인폼을 재활용하는 방법도 있겠지만 사용자들에게는 기능이 모호할 수 있으니 따로 페이지를 만들어 제공하겠습니다.<br />
언제나 그렇듯 뷰 먼저 생성을 합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span><span class="p">,</span> <span class="n">FormView</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ResendVerifyEmailView</span><span class="p">(</span><span class="n">FormView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">VerificationEmailForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'user/resend_verify_email.html'</span>
<span class="n">email_template_name</span> <span class="o">=</span> <span class="s">'user/email/registration_verification.html'</span>
<span class="n">token_generator</span> <span class="o">=</span> <span class="n">default_token_generator</span>
<span class="k">def</span> <span class="nf">send_verification_email</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
<span class="n">token</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">token_generator</span><span class="o">.</span><span class="n">make_token</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">build_verification_link</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="n">subject</span> <span class="o">=</span> <span class="s">'회원가입을 축하드립니다.'</span>
<span class="n">message</span> <span class="o">=</span> <span class="s">'다음 주소로 이동하셔서 인증하세요. {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">html_message</span> <span class="o">=</span> <span class="n">render</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">email_template_name</span><span class="p">,</span> <span class="p">{</span><span class="s">'url'</span><span class="p">:</span> <span class="n">url</span><span class="p">})</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">email_user</span><span class="p">(</span><span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">EMAIL_HOST_USER</span><span class="p">,</span> <span class="n">html_message</span><span class="o">=</span><span class="n">html_message</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_verification_link</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'{}/user/{}/verify/{}/'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">META</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'HTTP_ORIGIN'</span><span class="p">),</span> <span class="n">user</span><span class="o">.</span><span class="n">pk</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s">'email'</span><span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
<span class="k">except</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">DoesNotExist</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'알 수 없는 사용자 입니다.'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">send_verification_email</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre></div></div>
<p>인증이메일 발송기능을 회원가입뷰와 동일하게 만들고 보니 나중에 <code class="language-plaintext highlighter-rouge">subject</code>가 변경된다거나 <code class="language-plaintext highlighter-rouge">message</code>가 변경될 때 동일한 작업을 두번 이상 해줘야 할 불편함이 앞섭니다. 다른 작업보다 먼저 중복된 코드를 한쪽에 몰아주는 리팩토링을 해주고 싶은데 <strong>중복되는 메소드와 클래스변수를 <code class="language-plaintext highlighter-rouge">mixin</code> 에 선언</strong>하고, <strong>회원가입뷰와 인증이메일 재발송하는 뷰에서는 해당 <code class="language-plaintext highlighter-rouge">mixin</code> 을 추가</strong>하도록 하겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/mixins.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">messages</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.tokens</span> <span class="kn">import</span> <span class="n">default_token_generator</span>
<span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">render</span>
<span class="kn">from</span> <span class="nn">minitutorial</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="k">class</span> <span class="nc">VerifyEmailMixin</span><span class="p">:</span>
<span class="n">email_template_name</span> <span class="o">=</span> <span class="s">'user/email/registration_verification.html'</span>
<span class="n">token_generator</span> <span class="o">=</span> <span class="n">default_token_generator</span>
<span class="k">def</span> <span class="nf">send_verification_email</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
<span class="n">token</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">token_generator</span><span class="o">.</span><span class="n">make_token</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">build_verification_link</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="n">subject</span> <span class="o">=</span> <span class="s">'회원가입을 축하드립니다.'</span>
<span class="n">message</span> <span class="o">=</span> <span class="s">'다음 주소로 이동하셔서 인증하세요. {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">html_message</span> <span class="o">=</span> <span class="n">render</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">email_template_name</span><span class="p">,</span> <span class="p">{</span><span class="s">'url'</span><span class="p">:</span> <span class="n">url</span><span class="p">})</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">email_user</span><span class="p">(</span><span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">EMAIL_HOST_USER</span><span class="p">,</span><span class="n">html_message</span><span class="o">=</span><span class="n">html_message</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_verification_link</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'{}/user/{}/verify/{}/'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">META</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'HTTP_ORIGIN'</span><span class="p">),</span> <span class="n">user</span><span class="o">.</span><span class="n">pk</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">VerifyEmailMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">verify_url</span> <span class="o">=</span> <span class="s">'/user/verify/'</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">send_verification_email</span><span class="p">(</span><span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">class</span> <span class="nc">ResendVerifyEmailView</span><span class="p">(</span><span class="n">VerifyEmailMixin</span><span class="p">,</span> <span class="n">FormView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">VerificationEmailForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'user/resend_verify_email.html'</span>
<span class="c1"># 생략
</span>
</code></pre></div></div>
<p>이제 편안해졌으니 <code class="language-plaintext highlighter-rouge">ResendVerifyEmailView</code> 을 다시 살펴보면 <code class="language-plaintext highlighter-rouge">form_class</code> 로 <code class="language-plaintext highlighter-rouge">VerificationEmailForm</code> 을 정의했습니다. 장고에서 제공하는 폼클래스가 없어서 새로 생성해야 하는 폼입니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/forms.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">VerificationEmailForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">EmailField</span><span class="p">(</span><span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">EmailInput</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s">'autofocus'</span><span class="p">:</span> <span class="bp">True</span><span class="p">}),</span> <span class="n">validators</span><span class="o">=</span><span class="p">(</span><span class="n">EmailField</span><span class="o">.</span><span class="n">default_validators</span> <span class="o">+</span> <span class="p">[</span><span class="n">RegisteredEmailValidator</span><span class="p">()]))</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">VerificationEmailForm</code> 은 <code class="language-plaintext highlighter-rouge">email</code> 필드 하나만 가지고 있고, 추가로 유효성 검증 필터를 하나 더 추가했습니다. 이미 인증된 이메일이나, 가입된 적 없는 이메일이 입력된 경우 에러를 발생시키는 기능을 합니다. 에러메시지를 필드에 표시하기 위해 뷰가 아닌 필드에서 유효성을 검증하도록 했습니다.<br />
유효성 검증필터는 <code class="language-plaintext highlighter-rouge">EmailField</code> 의 기본 필터에 추가하기 위해서 <strong><code class="language-plaintext highlighter-rouge">RegisteredEmailValidator()</code> 인스턴스를 <code class="language-plaintext highlighter-rouge">default_validators</code> 리스트에 추가</strong>했습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/validators.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.core.exceptions</span> <span class="kn">import</span> <span class="n">ValidationError</span>
<span class="k">class</span> <span class="nc">RegisteredEmailValidator</span><span class="p">:</span>
<span class="n">user_model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">code</span> <span class="o">=</span> <span class="s">'invalid'</span>
<span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">user_model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
<span class="k">except</span> <span class="bp">self</span><span class="o">.</span><span class="n">user_model</span><span class="o">.</span><span class="n">DoesNotExist</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ValidationError</span><span class="p">(</span><span class="s">'가입되지 않은 이메일입니다.'</span><span class="p">,</span> <span class="n">code</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">user</span><span class="o">.</span><span class="n">is_active</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ValidationError</span><span class="p">(</span><span class="s">'이미 인증되어 있습니다.'</span><span class="p">,</span> <span class="n">code</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">code</span><span class="p">)</span>
<span class="k">return</span>
</code></pre></div></div>
<p><strong>필드의 유효성 검증 필터는 반드시 <code class="language-plaintext highlighter-rouge">__call__</code> 메소드를 오버라이드 해줘야 합니다</strong>. 이 메소드는 인스턴스를 invoke 연산자(소괄호)로 호출시 실행하는 함수입니다. 폼의 필드는 유효성을 검증할 때 필드에 정의된 default_validators 리스트의 각 원소들을 입력된 값을 전달하여 함수처럼 호출합니다. 그렇기 때문에 <code class="language-plaintext highlighter-rouge">validators=(EmailField.default_validators + [RegisteredEmailValidator()])</code> 라고 필터에 인스턴스를 추가했지만 내부적으로 <code class="language-plaintext highlighter-rouge">for validator in default_validators: validator(email)</code> 이런 식으로 호출이 가능합니다.</p>
<blockquote>
<p>원래 클래스의 인스턴스는 invoke 연산자로 호출이 불가능합니다. 파이썬에서는 함수도 객체(인스턴스)인데, <code class="language-plaintext highlighter-rouge">__call__</code> 메소드가 구현되어 있다고 생각하시면 됩니다.</p>
</blockquote>
<p>이제 <code class="language-plaintext highlighter-rouge">ResendVerifyEmailView</code> 클래스에 폼의 유효성이 검증된 후 인증이메일을 발송하도록 구현하면 됩니다.<br />
<code class="language-plaintext highlighter-rouge">ResendVerifyEmailView</code> 에서는 폼클래스가 <code class="language-plaintext highlighter-rouge">UserRegistrationForm</code> 과는 다르게 모델폼이 아니기 때문에 데이터베이스에 저장할 것이 없고 단지 <code class="language-plaintext highlighter-rouge">success_url</code> 로 이동하는 역할만 합니다. 그래서 부모클래스의 메소드가 실행될 때까지 기다리지 않고 이메일을 전송하도록 했습니다.<br />
폼객체는 유효성검증 작업이 끝나면 <strong><code class="language-plaintext highlighter-rouge">cleaned_data</code> 라는 인스턴스변수에 각 필드 이름으로 사용자가 입력한 값들을 저장</strong>합니다. 즉 사용자가 입력한 이메일은 <code class="language-plaintext highlighter-rouge">form.cleaned_data['email']</code> 에서 확인할 수 있습니다. 이것으로 확인된 이메일을 통해 인증이메일을 보내도록 기능을 추가합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ResendVerifyEmailView</span><span class="p">(</span><span class="n">VerifyEmailMixin</span><span class="p">,</span> <span class="n">FormView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">VerificationEmailForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'user/resend_verify_email.html'</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s">'email'</span><span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
<span class="k">except</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">DoesNotExist</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'알 수 없는 사용자 입니다.'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">send_verification_email</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>특별한 기능은 없고 <code class="language-plaintext highlighter-rouge">email</code> 로 가입된 사용자 데이터를 불러와서 그것으로 이메일을 발송하는 기능을 추가했습니다. 템플릿은 <code class="language-plaintext highlighter-rouge">login_form.html</code> 템플릿을 복제해서 적절하게 텍스트를 바꿔줍니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/resend_verify_email.html --></span>
<span class="c"><!-- 생략 --></span>
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
인증이메일 보내기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
<span class="nt"><b</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>재발송할 이메일주소를 입력해주세요.<span class="nt"></b></span>
{% for field in form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group {% if field.errors|length > 0 %}has-error{%endif %}"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"{{ field.html_name }}"</span> <span class="na">id=</span><span class="s">"{{ field.id_for_lable }}"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">type=</span><span class="s">"{{ field.field.widget.input_type }}"</span> <span class="na">value=</span><span class="s">"{{ field.value|default_if_none:'' }}"</span><span class="nt">></span>
{% for error in field.errors %}
<span class="nt"><label</span> <span class="na">class=</span><span class="s">"control-label"</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ error }}<span class="nt"></label></span>
{% endfor %}
<span class="nt"></div></span>
{% endfor %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>인증이메일 보내기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p>특별히 폼필드들을 렌더링하기 전에 안내문구를 추가해줬습니다. <code class="language-plaintext highlighter-rouge">ResendVerifyEmailView</code> 뷰를 <code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 등록하고 화면이 정상적으로 나오는 지 확인해봅니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">user.views</span> <span class="kn">import</span> <span class="n">UserRegistrationView</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="p">,</span> <span class="n">ResendVerifyEmailView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">'article-list'</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/<pk>/verify/<token>/'</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/resend_verify_email/'</span><span class="p">,</span> <span class="n">ResendVerifyEmailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/login/'</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>정상적으로 화면이 출력된다면 이제 마지막 단계인데 로그인 템플릿에 인증이메일 재발송 페이지로 이동하는 링크를 추가합니다. 로그인 버튼 아래 추가하고 로그인 버튼과 거리가 가까우니 조정을 합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/templates/resend_verify_email.html --></span>
<span class="nt"><style></span>
<span class="nc">.registration</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">360px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.control-label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.registration</span> <span class="nc">.form-actions</span> <span class="o">></span> <span class="nt">button</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.link-below-button</span> <span class="p">{</span> <span class="nl">margin-top</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span> <span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;}</span>
<span class="nt"></style></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>로그인하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/resend_verify_email/"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"link-below-button"</span><span class="nt">></span>인증이메일 재발송<span class="nt"></div></span>
<span class="nt"></a></span>
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>로그인화면으로 이동해서 인증이메일 재발송 링크를 클릭해서 정상적으로 이동하는 지 확인해봅니다. 인증이메일 발송기능은 핵심 기능인 이메일 보내기와 토큰 생성 및 검증 기능을 장고에서 제공하기 때문에 크게 어렵지 않습니다. 특히 사용자에게 이메일을 보내는 기능은 빈번하게 사용되기 때문에 잘 알아두면 여러모로 도움이 됩니다.</p>
<h2 id="2-로그아웃">2. 로그아웃</h2>
<p>로그인까지는 문제없이 잘 동작 합니다. 사용자가 로그인 후 2주까지는 동일 브라우저로 접속할 경우 로그인 상태를 유지합니다. 하지만 공용 pc 를 사용하는 경우 사용자가 아닌 누군가가 로그인된 상태로 pc를 사용할 수 있으니 반드시 로그아웃 기능을 제공해서 사용자가 원하는 시간에 인증을 해제시켜둬야 합니다. 아주 간단한 방법으로 쿠키의 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 삭제하는 방법도 있으나 장고에서는 좀 더 우아한 방법을 제공합니다. 로그아웃은 화면도 필요없고 단지 세션을 정리해주는 기능만 필요합니다. 장고에서는 이 기능을 완벽하게 구현한 뷰를 제공하기 때문에 <code class="language-plaintext highlighter-rouge">urlpattern</code> 에 해당 핸들러를 등록해주기만 하면 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.views</span> <span class="kn">import</span> <span class="n">LogoutView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">'article-list'</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/<pk>/verify/<token>/'</span><span class="p">,</span> <span class="n">UserVerificationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/resend_verify_email/'</span><span class="p">,</span> <span class="n">ResendVerifyEmailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/login/'</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/logout/'</span><span class="p">,</span> <span class="n">LogoutView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>auth 프레임워크에서 제공하는 <code class="language-plaintext highlighter-rouge">LogoutView</code> 클래스에는 여러분이 원하는 모든 기능이 구현되어 있습니다. 단지 로그아웃버튼을 어떻게 배치시킬 지와 로그아웃된 이후에 이동할 페이지의 주소를 설정파일의 <code class="language-plaintext highlighter-rouge">LOGOUT_REDIRECT_URL</code> 에 정의해주시면 됩니다. 로그아웃버튼은 이미 <code class="language-plaintext highlighter-rouge">base.html</code> 파일에 있으니 해당버튼을 로그아웃 뷰로 연결시킬 것입니다. 로그아웃 이동될 화면은 어디로 이동시켜도 좋지만 사용자들이 어떤 작업을 많이 하면 좋겠느냐를 판단하시고 해당 <code class="language-plaintext highlighter-rouge">url</code> 을 결정하면 됩니다. 저는 bbs 앱의 기본기능인 게시물목록 보기 화면으로 이동시키겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">LOGOUT_REDIRECT_URL</span> <span class="o">=</span> <span class="s">'/article/'</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"collapse navbar-collapse"</span> <span class="na">id=</span><span class="s">"bs-example-navbar-collapse-1"</span><span class="nt">></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"nav navbar-nav navbar-right"</span><span class="nt">></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>
{% if not request.user.is_authenticated %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/create/"</span><span class="nt">></span>가입하기<span class="nt"></a></span>
{% endif %}
<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>
{% if request.user.is_authenticated %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/logout/"</span><span class="nt">></span>로그아웃<span class="nt"></a></span>
{% else %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/login/"</span><span class="nt">></span>로그인<span class="nt"></a></span>
{% endif %}
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"></div></span>
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>로그아웃은 너무 편하게 기능을 추가해서 어리둥절 할 수 있게지만 이제 로그인이 된 상태라면 언제나 로그아웃을 할 수 있게 되었습니다.</p>
<blockquote>
<p>장고의 auth 프레임워크에서 제공하는 뷰들이 어떻게 구현되어 있는 지 살펴보시면 새로운 뷰 개발에 도움이 되실 겁니다.</p>
<ol>
<li>django.contrib.auth.views.PasswordResetView</li>
<li>django.contrib.auth.views.PasswordResetDoneView</li>
<li>django.contrib.auth.views.PasswordResetConfirmView</li>
<li>django.contrib.auth.views.PasswordResetCompleteView</li>
<li>django.contrib.auth.views.PasswordChangeView</li>
<li>django.contrib.auth.views.PasswordChangeDoneView</li>
</ol>
</blockquote>
<p><del>복붙말고</del> 따라해보시면 생각보다 어렵지 않게 느껴지실 겁니다. 비밀번호 변경 기능과 비밀번호 초기화 기능은 auth 프레임워크에서 제공하니 이것은 템플릿만 만들면 로그인, 로그아웃 만큼이나 어렵지 않습니다. 한번 직접 만들어보세요.</p>
<blockquote>
<p>세상에는 두 종류의 사람들이 있다. 자신이 직접 코딩해낼 수 있다고 생각하는 사람과 해낼 수 없다고 생각하는 사람이다. 물론 두 사람 다 옳다. 그가 생각한대로 되기 때문이다.</p>
<p>swarf00, 확인하지 않을 숙제를 내주면서...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크에서 이메일인증 및 로그아웃 기능 구현방방을 알아봅니다.사용자인증(2)2018-12-10T00:00:00+09:002018-12-10T00:00:00+09:00https://swarf00.github.io/2018/12/10/login<h2 id="1-로그인-기능-생성">1. 로그인 기능 생성</h2>
<p>가입하기 기능을 구현하면서 장고의 모델폼과 템플릿에 대해 많이 익숙해졌습니다. 로그인 기능을 구현할 때는 이보다는 복잡하지 않을 겁니다. 로그인은 어느 상황에서도 할 수 있도록 화면 상단(내비게이션바)의 오른쪽에 보이도록 할 겁니다. 로그인이 된 상태라면 당연히 로그아웃 버튼으로 보이도록 해야 합니다. 또한 로그인 화면 안에는 아직 가입되어 있지 않은 사용자들을 위해 회원가입 링크도 제공해야 합니다.</p>
<p>일단 뷰를 생성하고 url 라우팅을 추가합니다. 뷰는 장고 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크에서 제공하는 <code class="language-plaintext highlighter-rouge">LoginView</code>를 상속받아 구현할 예정입니다. 사실 대부분 설정만 바꾸게 될 것 같습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.views</span> <span class="kn">import</span> <span class="n">LoginView</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.forms</span> <span class="kn">import</span> <span class="n">UserRegistrationForm</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span> <span class="c1"># 회원가입
</span> <span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/article/'</span>
<span class="k">class</span> <span class="nc">UserLoginView</span><span class="p">(</span><span class="n">LoginView</span><span class="p">):</span> <span class="c1"># 로그인
</span> <span class="n">template_name</span> <span class="o">=</span> <span class="s">'user/login_form.html'</span>
<span class="k">def</span> <span class="nf">form_invalid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'로그인에 실패하였습니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_invalid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre></div></div>
<p>로그인은 일단 화면이 나오게 하려면 <strong><code class="language-plaintext highlighter-rouge">LoginView</code> 를 상속받고 <code class="language-plaintext highlighter-rouge">template_name</code> 변수를 정의</strong>해주면 됩니다.
<code class="language-plaintext highlighter-rouge">LoginView</code> 는 뷰만 와 폼만 제공해주고, 템플릿은 제공해주지 않습니다. 기본값으로 <code class="language-plaintext highlighter-rouge">registraion/login.html</code> 로 설정되어 있는데 실제 찾아보면 존재하지 않는 파일입니다. 회원가입을 구현할 때 user 디렉토리에 템플릿을 저장했으니 로그인도 동일한 디렉토리에 저장하는 것으로 정했습니다. 또한 <code class="language-plaintext highlighter-rouge">LoginView</code> 가 <code class="language-plaintext highlighter-rouge">FormView</code> 의 서브클래스이기 때문에 <code class="language-plaintext highlighter-rouge">login_form.html</code> 이라고 했습니다. 이렇게 보니 회원가입 템플릿의 이름이 <code class="language-plaintext highlighter-rouge">user_form.html</code> 인데 좀 어색합니다. 이 부분은 나중에 수정하기로 하고 일단 urlpattern 에 뷰를 등록해줍니다.<br />
인증에 실패한 경우 아무런 메시지도 없기 때문에 form_invalid 메소드를 오버라이드 해서 로그인에 실패할 경우 메시지를 출력하도록 기능을 추가했습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="c1"># 생략
</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">'article-list'</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span> <span class="c1"># 회원가입
</span> <span class="n">path</span><span class="p">(</span><span class="s">'user/login/'</span><span class="p">,</span> <span class="n">UserLoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span> <span class="c1"># 로그인
</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>이제 템플릿이 필요한데 회원가입할 때 사용했었던 템플릿을 그대로 복붙해 넣습니다. 수정이 필요한 부분은 title과 panel-heading 부분 그리고 버튼의 텍스트를 <code class="language-plaintext highlighter-rouge">가입하기</code>에서 <code class="language-plaintext highlighter-rouge">로그인하기</code> 로 변경하는 것 입니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/login_form.html --></span>
<span class="c"><!-- 생략 --></span>
{% block title %}<span class="nt"><title></span>로그인<span class="nt"></title></span>{% endblock %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
로그인하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% for field in form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group {% if field.errors|length > 0 %}has-error{%endif %}"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"{{ field.html_name }}"</span> <span class="na">id=</span><span class="s">"{{ field.id_for_lable }}"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">type=</span><span class="s">"{{ field.field.widget.input_type }}"</span> <span class="na">value=</span><span class="s">"{{ field.value|default_if_none:'' }}"</span><span class="nt">></span>
{% for error in field.errors %}
<span class="nt"><label</span> <span class="na">class=</span><span class="s">"control-label"</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ error }}<span class="nt"></label></span>
{% endfor %}
<span class="nt"></div></span>
{% endfor %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>로그인하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p>일단 이렇게 되면 회원가입과 중복되는 부분이 많은데 중복된 부분을 최소화할 수 있게 변경하는 것이 필요합니다. 추가로 고려해야 할 부분이 있을 지도 모르니 기능이 먼저 동작하는 것을 해결하겠습니다. 장고를 실행시키고 브라우저에서 <code class="language-plaintext highlighter-rouge">http://localhost:8000/user/login/</code> 에 접속합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/loginview_01.png" alt="LoginView 기본 템플릿 적용 후" /></p>
<p>화면으로 볼 때 그렇게 나쁘지 않은데 렌더링된 html 코드를 살펴보니 <code class="language-plaintext highlighter-rouge">email</code> 태그의 <code class="language-plaintext highlighter-rouge">type</code> 이 <code class="language-plaintext highlighter-rouge">email</code> 로 되어 있지 않네요. 불편하진 않지만 브라우저 레벨의 email 유효성 검사를 하지 않고, 아마도 서버에서도 검증을 하지 않을 것 같습니다. 크게 중요하지 않은 문제이니 로그인을 성공시킨 후에 처리하도록 합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/loginview_02.png" alt="LoginView 기본 템플릿 로그인 성공 후 Page Not Found" /></p>
<p>정상적인 이메일과 비밀번호를 입력하고 로그인하기를 눌렀는데 오류가 발생했습니다. 오류내용을 보니 <strong><code class="language-plaintext highlighter-rouge">/accounts/profile/</code> 로 접속을 시도했으나 존재하지 않는 페이지</strong>라는 오류입니다. 아마도 <code class="language-plaintext highlighter-rouge">LoginView가</code> 기본적으로 어떤 처리를 하고 정상적일 경우 해당 url로 이동을 하는 것으로 추정됩니다. <code class="language-plaintext highlighter-rouge">LoginView</code>의 코드를 간단히 살펴보겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># django/contrib/auth/views.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">LoginView</span><span class="p">(</span><span class="n">SuccessURLAllowedHostsMixin</span><span class="p">,</span> <span class="n">FormView</span><span class="p">):</span>
<span class="s">"""
Display the login form and handle the login action.
"""</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">AuthenticationForm</span>
<span class="n">authentication_form</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">redirect_field_name</span> <span class="o">=</span> <span class="n">REDIRECT_FIELD_NAME</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'registration/login.html'</span>
<span class="n">redirect_authenticated_user</span> <span class="o">=</span> <span class="bp">False</span>
<span class="n">extra_context</span> <span class="o">=</span> <span class="bp">None</span>
<span class="o">@</span><span class="n">method_decorator</span><span class="p">(</span><span class="n">sensitive_post_parameters</span><span class="p">())</span>
<span class="o">@</span><span class="n">method_decorator</span><span class="p">(</span><span class="n">csrf_protect</span><span class="p">)</span>
<span class="o">@</span><span class="n">method_decorator</span><span class="p">(</span><span class="n">never_cache</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">redirect_authenticated_user</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">:</span>
<span class="n">redirect_to</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_success_url</span><span class="p">()</span>
<span class="k">if</span> <span class="n">redirect_to</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">path</span><span class="p">:</span>
<span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span>
<span class="s">"Redirection loop for authenticated user detected. Check that "</span>
<span class="s">"your LOGIN_REDIRECT_URL doesn't point to a login page."</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="n">redirect_to</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_success_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_redirect_url</span><span class="p">()</span>
<span class="k">return</span> <span class="n">url</span> <span class="ow">or</span> <span class="n">resolve_url</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_REDIRECT_URL</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_redirect_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s">"""Return the user-originating redirect URL if it's safe."""</span>
<span class="n">redirect_to</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">redirect_field_name</span><span class="p">,</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">redirect_field_name</span><span class="p">,</span> <span class="s">''</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">url_is_safe</span> <span class="o">=</span> <span class="n">is_safe_url</span><span class="p">(</span>
<span class="n">url</span><span class="o">=</span><span class="n">redirect_to</span><span class="p">,</span>
<span class="n">allowed_hosts</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_success_url_allowed_hosts</span><span class="p">(),</span>
<span class="n">require_https</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">is_secure</span><span class="p">(),</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">redirect_to</span> <span class="k">if</span> <span class="n">url_is_safe</span> <span class="k">else</span> <span class="s">''</span>
<span class="k">def</span> <span class="nf">get_form_class</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">authentication_form</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">form_class</span>
<span class="k">def</span> <span class="nf">get_form_kwargs</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">kwargs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_form_kwargs</span><span class="p">()</span>
<span class="n">kwargs</span><span class="p">[</span><span class="s">'request'</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span>
<span class="k">return</span> <span class="n">kwargs</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="s">"""Security check complete. Log the user in."""</span>
<span class="n">auth_login</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="o">.</span><span class="n">get_user</span><span class="p">())</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_success_url</span><span class="p">())</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">current_site</span> <span class="o">=</span> <span class="n">get_current_site</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="n">update</span><span class="p">({</span>
<span class="bp">self</span><span class="o">.</span><span class="n">redirect_field_name</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_redirect_url</span><span class="p">(),</span>
<span class="s">'site'</span><span class="p">:</span> <span class="n">current_site</span><span class="p">,</span>
<span class="s">'site_name'</span><span class="p">:</span> <span class="n">current_site</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="o">**</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">extra_context</span> <span class="ow">or</span> <span class="p">{})</span>
<span class="p">})</span>
<span class="k">return</span> <span class="n">context</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>원인을 찾아보기에 크게 복잡하지 않습니다. <code class="language-plaintext highlighter-rouge">form_vaild</code> 함수에서 <code class="language-plaintext highlighter-rouge">auth_login</code> 함수 실행 후 <code class="language-plaintext highlighter-rouge">self.get_success_url()</code> 메소드가 리턴하는 주소로 이동하는데 이때 <strong><code class="language-plaintext highlighter-rouge">self.get_success_url()</code> 메소드는 3가지의 값들을 순서대로 검색하며 가장 먼저 검색된 값을 반환</strong>합니다. 아무런 설정을 하지 않으면 <code class="language-plaintext highlighter-rouge">LOGIN_REDIRECT_URL</code> 에 정의된 <code class="language-plaintext highlighter-rouge">'/accounts/profile/'</code> 로 반환하게 됩니다.</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">FormView</code> 는 form 객체가 유효성이 모두 정상적일 경우 <code class="language-plaintext highlighter-rouge">form_vaild</code> 함수를 호출하고, 그렇지 않을 경우 <code class="language-plaintext highlighter-rouge">form_invalid</code> 함수를 호출합니다.</p>
</blockquote>
<ul>
<li>login redirection url 검색 순서
<ol>
<li>요청된 폼의 필드 중 next라는 이름을 가진 필드의 값. 빈 값인 경우 2번으로 패스</li>
<li>url의 query parameter 중 next 라는 이름을 가진 값. 빈 값인 경우 3번으로 패스</li>
<li>설정파일에 설정된 <code class="language-plaintext highlighter-rouge">LOGIN_REDIRECT_URL</code> 변수로 설정된 값. (기본값: <code class="language-plaintext highlighter-rouge">'/accounts/profile/'</code>)</li>
</ol>
</li>
</ul>
<p>이 조건을 근거로 로그인 후 여러분이 원하는 url로 이동시키기 위해서는 3가지의 조건 중 1가지 이상을 선택할 수 있습니다.</p>
<ul>
<li>login redirection url 설정 방법
<ol>
<li>form에 next라는 이름의 hidden 필드를 추가하고 <code class="language-plaintext highlighter-rouge">'/article/'</code> 값을 기본으로 세팅한다.</li>
<li>form의 action 속성에 <code class="language-plaintext highlighter-rouge">'/user/login/?next=/article/'</code> 이라는 값을 세팅한다.</li>
<li>설정 파일에 <code class="language-plaintext highlighter-rouge">LOGIN_REDIRECT_URL = '/article/'</code> 이라고 설정한다.</li>
<li>one more thing! <code class="language-plaintext highlighter-rouge">get_success_url()</code> 메소드를 오버라이드 해서 <code class="language-plaintext highlighter-rouge">'/article/'</code> 문자열을 반환한다.</li>
</ol>
</li>
</ul>
<p>현재 이 네가지 모두 사용해도 문제가 없습니다. 하지만 이 네가지 방법은 각각 사용해야 할 가장 좋은 케이스들이 있습니다. 누가 정해놓은 건 아니지만 장고로 개발된 대개의 서비스가 그렇게 사용하고 있다는 겁니다. 다시 4가지의 케이스를 나열해 볼테니 각각의 케이스가 몇번의 방법을 사용해야 좋을 지 생각을 해보세요.</p>
<ul>
<li>로그인 이후 redirection url 결정을 고려해야 할 케이스
<ol>
<li>대부분의 경우 사용자가 로그인 후 아무런 조건이 없을 때 이동할 페이지. 기본적인 REDIRECT URL</li>
<li>다양한 방식의 로그인을 제공해서 로그인 이후 이동할 페이지가 단순한 규칙으로 다를 경우.
<strong><em>예) 모바일과 PC 버전의 화면들을 각각 제공하며 url의 path에 따라 화면이 결정될 경우 /m/user/login/ => /m/article/, /user/login => /article/ 또는 다국어를 지원해서 언어별로 path를 구분하는 경우 /ko/user/login => /ko/article/, /en/user/login => /en/article/</em></strong></li>
<li>로그인하기 전에는 redirect url을 알 수 없을 경우.
<strong><em>예) 로그인한 사용자의 권한레벨에 따라 슈퍼유저인 경우 admin 사이트로, staff 권한인 경우 대시보드 화면으로 이동, 로그인한 사용자의 연령이 20세 미만일 경우 특정화면으로 이동</em></strong></li>
<li>어떤 화면으로 이동하려 했으나 인증된 사용자만 접근이 허락된 화면이어서 자동으로 로그인화면으로 이동한 경우
<strong><em>예) 로그인 하지 않은 사용자가 /admin/user/user/ 를 접근했으나 강제로 로그인 화면으로 이동되고, 로그인된 이후에 원래 사용자가 접근하려 했던 /admin/user/user/ 로 되돌려 보내야 하는 경우</em></strong></li>
</ol>
</li>
</ul>
<p>이 외에도 케이스가 더 다양하겠지만 이 정도가 실무적으로 가장 빈번하게 발생하는 케이스인 듯 합니다. 각 케이스마다 가장 괜찮은 방법을 찾는 것은 상황마다 조금씩 다를 수 있고 개발자마다 성향이 조금 다를 수 있습니다. 저의 경우 케이스별 방법을 매칭해보니 이렇습니다. 각 케이스별로 한 가지만 선택해야 하는 경우를 가정했습니다.</p>
<ol>
<li>=> 3번 방법</li>
<li>=> 2번 방법</li>
<li>=> 4번의 <code class="language-plaintext highlighter-rouge">get_success_url</code> 메소드를 케이스 별 처리하도록 오버라이딩</li>
<li>=> 2번 방법</li>
</ol>
<p>저의 선택에 나름대로 규칙이 있습니다. 가장 선호하는 방법은 3, 2, 1, 4 순서입니다. 4번(<code class="language-plaintext highlighter-rouge">get_success_url</code> 메소드 오버라이딩)은 장고에서 기본적으로 제공하는 루틴을 무시하고 재정의 하는 것이니 코드의 일관성을 해치는 방법입니다. <strong>4번 방법은 피할 수 있으면 피하세요</strong>. <del>ddong이라고 생각하세요.</del> <strong>3번 방법(설정파일에 <code class="language-plaintext highlighter-rouge">LOGIN_REDIRECT_URL</code> 변수 설정)은 무조건 설정</strong>하세요. 그리고 예외적인 케이스는 전부 2번 방법(url에 query 파라미터 추가)으로 처리합니다. 만일 2번 방법보다 1번의 방법이 코드가 효율적이거나 url로 redirect url이 노출되는 것이 싫은 경우에만 1번을 사용합니다.</p>
<blockquote>
<p>이해가 되셨는 지 모르겠습니다. 글을 업로드 하기 전 매번 2번이상 내용을 재확인하는데 읽을 때마다 보는 분들이 불편하겠구나 생각되는 부분들이 있습니다. 아래 <code class="language-plaintext highlighter-rouge">gitalk</code> 게시판이 있으니 질문을 남겨주세요. 게시판에 남겨주시면 다른 분들에게도 도움이 됩니다.</p>
</blockquote>
<p>결론은 3번 방법으로 <strong>설정파일에 <code class="language-plaintext highlighter-rouge">LOGIN_REDIRECT_URL = '/article/'</code> 를 추가</strong> 하겠습니다. 예외적인 상황이 생기면 2번 방법으로 처리하겠습니다. <del>이 한줄 설명하는 게 이렇게나 힘듭니다.</del></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settins.py
</span>
<span class="c1"># 생략
</span>
<span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s">'/static/'</span>
<span class="n">AUTH_USER_MODEL</span> <span class="o">=</span> <span class="s">'user.User'</span>
<span class="n">LOGIN_REDIRECT_URL</span> <span class="o">=</span> <span class="s">'/article/'</span> <span class="c1"># 로그인이 되면 /article/로 이동
</span></code></pre></div></div>
<p>이제 로그인 해보시면 정상적으로 게시글 목록 화면('/article/')으로 이동이 됩니다. 한가지 아쉬운 것은 로그인된 상태인지 아닌지 확인이 안된 다는 것입니다. 아까 말한대로 내비게이션바 오른쪽에 로그아웃 링크를 추가해서 <strong>로그인된 상태일 때는 로그아웃으로 보이고, 로그인이 안 된 상태에서는 로그인</strong>으로 보이도록 만들어 봅니다. 단지 상황에 따라 html만 변경되면 되는 것이니 템플릿만 수정하겠습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="c"><!-- 생략 --></span>
{% block header %}
<span class="nt"><nav</span> <span class="na">class=</span><span class="s">"navbar navbar-default"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"navbar-header"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"navbar-brand"</span> <span class="na">href=</span><span class="s">"/article/"</span><span class="nt">></span>게시글 목록<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"collapse navbar-collapse"</span> <span class="na">id=</span><span class="s">"bs-example-navbar-collapse-1"</span><span class="nt">></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"nav navbar-nav navbar-right"</span><span class="nt">></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>
{% if not request.user.is_authenticated %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/create/"</span><span class="nt">></span>가입하기<span class="nt"></a></span>
{% endif %}
<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">""</span><span class="nt">></span>
{% if request.user.is_authenticated %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/login/"</span><span class="nt">></span>로그아웃<span class="nt"></a></span>
{% else %}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/user/login/"</span><span class="nt">></span>로그인<span class="nt"></a></span>
{% endif %}
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></nav></span>
{% if messages %}
{% for message in messages %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"alert alert-{{ message.tags }} alert-dismissible"</span> <span class="na">role=</span><span class="s">"alert"</span><span class="nt">></span>
{{ message }}
<span class="nt"></div></span>
{% endfor %}
{% endif %}
{% endblock header %}
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<blockquote>
<p>장고의 미들웨어 중에 <code class="language-plaintext highlighter-rouge">'django.contrib.sessions.middleware.SessionMiddleware'</code> 와 <code class="language-plaintext highlighter-rouge">'django.contrib.auth.middleware.AuthenticationMiddleware'</code> 는 사용자 인증에 관한 처리를 담당합니다. <code class="language-plaintext highlighter-rouge">SessionMiddleware</code> 는 로그인함수(<code class="language-plaintext highlighter-rouge">auth_login</code>)를 통해 생성된 세션을 관리합니다. 세션이 유효한지 만료되었는지 판단을 해서 유효한 경우에는 request 객체에 session 이라는 변수에 세션정보를 저장합니다. <code class="language-plaintext highlighter-rouge">AuthenticationMiddleware</code>는 <code class="language-plaintext highlighter-rouge">request.session</code> 값을 가지고 어떤 사용자인지 확인을 합니다. 확인된 사용자는 <code class="language-plaintext highlighter-rouge">request.user</code> 객체에 해당 사용자의 모델 인스턴스를 저장합니다.</p>
</blockquote>
<p>우선 로그인과 로그아웃 링크만 보이도록 만들었고 모두 로그인 페이지로 이동하도록 링크를 설정했습니다. 버튼들이 정상적으로 작동하는 지 클릭해서 확인해봅니다. 언제나 클릭을 하면 로그인 화면으로 이동하는데 로그아웃 링크를 눌러도 로그인버튼으로 변경되지 않습니다. 로그아웃 기능을 구현하면 해결된 문제이니 지금은 로그인의 나머지 기능에 집중합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/loginview_03.png" alt="LoginView 기본 템플릿 로그인 로그아웃 링크 추가 후" /></p>
<p>로그인 기능은 구현했지만 지난번에 만들었던 게시판 앱에 아직 인증된 사용자만 접근허용 기능을 연동시키지 않아 로그인을 하지 않은 상태에서도 어떠한 화면으로든 접근할 수 있습니다. 현재 게시글을 보는 건 비공개로 설정하지 않았지만 <strong>글을 쓸 때와 수정할 때는 인증된 사용자만 가능</strong>하도록 변경할 것 입니다.</p>
<p>장고의 auth 프레임워크에는 인증된 사용자만 뷰의 핸들를 호출할 수 있도록 하는 기능이 이미 구현되어 있습니다. 일반 웹과 관련된 기능은 대부분 구현되어 있습니다. 비지니스 로직에 맞게 잘 조립해 사용하시면 됩니다.</p>
<p>FBV에서는 <code class="language-plaintext highlighter-rouge">login_required</code> 라는 데코레이터를 핸들러에 wrapping 해주면 되는데 CBV에서는 <code class="language-plaintext highlighter-rouge">LoginRequiredMixin</code> 믹스인을 뷰에 추가해주면 됩니다. 단 로그인이 되어 있지 않은 경우에 로그인 url로 이동시켜야 하는데 login_required 데코레이터에서는 <code class="language-plaintext highlighter-rouge">login_url</code> 이라는 파라미터에 전달하면 되고, <code class="language-plaintext highlighter-rouge">LoginRequiredMixin</code> 에서는 <code class="language-plaintext highlighter-rouge">login_url</code> 이라는 클래스변수를 선언해주거나 설정파일에 <code class="language-plaintext highlighter-rouge">LOGIN_URL</code> 변수에 url을 정의하면 됩니다. 복잡하지 않으니 일단 코드를 보시면 됩니다. 로그인 url은 프로젝트 내에서 공통적으로 사용하는 것이니 기본적으로 설정파일에 변수를 추가하고 뷰에도 클래스변수로 추가하도록 하겠습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settins.py
</span>
<span class="c1"># 생략
</span>
<span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s">'/static/'</span>
<span class="n">AUTH_USER_MODEL</span> <span class="o">=</span> <span class="s">'user.User'</span>
<span class="n">LOGIN_REDIRECT_URL</span> <span class="o">=</span> <span class="s">'/article/'</span>
<span class="n">LOGIN_URL</span> <span class="o">=</span> <span class="s">'/user/login/'</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span><span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">):</span>
<span class="n">login_url</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">LOGIN_URL</span> <span class="c1"># 설정파일의 값으로 설정
</span> <span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_update.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>현재는 로그인된 상태이니 아무 문제없이 접속이 될 겁니다. 아직 로그아웃 기능을 구현하지 않았기 때문에 꼼수를 사용합니다. 장고는 기본적으로 세션정보를 데이터베이스에 저장합니다. 아직은 개발단계이므로 <del>무식하게</del> 모든 세션데이터를 삭제해도 상관이 없습니다. 쿠키에서 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 얻는 방법을 아신다면 해당 <code class="language-plaintext highlighter-rouge">sessionid</code> 값과 <code class="language-plaintext highlighter-rouge">django_session</code> 테이블의 session_key 값을 비교해서 동일한 레코드만 삭제하셔도 괜찮습니다. 저는 크롬브라우저의 개발자도구를 이용해서 쿠키에 있는 <code class="language-plaintext highlighter-rouge">sessionid</code> 알아냈고, 제 session 정보만 삭제했습니다. <del>그냥 쿠키의 <code class="language-plaintext highlighter-rouge">sessionid</code> 정보를 삭제하셔도 됩니다.</del></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> DELETE FROM django_session WHERE session_key <span class="o">=</span> <span class="s1">'eyar6aan2nmmuelypubu1r1exvht....'</span><span class="p">;</span>
</code></pre></div></div>
<p>이제 새로고침을 하면 로그아웃이 로그인으로 변경되어 있을 겁니다. 또 <code class="language-plaintext highlighter-rouge">/article/create/</code> 주소로 접속하면 로그인 화면으로 이동되고 url에 <code class="language-plaintext highlighter-rouge">/user/login/?next=/article/create/</code>처럼 query 파라미터가 추가되어 있는 것을 확인할 수 있습니다.</p>
<p><img src="https://swarf00.github.io/snapshots/loginview_04.png" alt="LoginView 세션 로그인되지 않은 상태에서 게시글 작성 화면으로 이동했을 때" /></p>
<p>이제 로그인기능이 정상적으로 동작한다는 것을 확인했습니다. 로그인은 특별히 어려운 점이 없었던 것 같은데 인증 매카니즘을 구체적으로 설명하지 않았던 것 같습니다.</p>
<blockquote>
<p>이미 장고에 많은 것들이 구현되어 있고, 어떤 기능은 누군가에 의해 개발되어 오픈소스로 공개되어 있는 기능도 많이 있습니다. 결국 장고를 잘 다룬다는 것은 <strong>이미 만들어져 있는 것들을 잘 찾아서 그것들을 적절히 연동</strong>하는 것 입니다. 이런 능력은 <del>특별한 건 아니고</del> 용기있게 소스코드를 들여다 보다보면 길러지는게 됩니다. <del>시간이 웬수</del> 열심히 남들이 만든 기술들을 보고 이해하고 따라해보세요.</p>
</blockquote>
<p>이제 아까 미뤄뒀던 <code class="language-plaintext highlighter-rouge">email</code> 필드를 정상적인 <code class="language-plaintext highlighter-rouge">email</code> 형식의 <code class="language-plaintext highlighter-rouge">input</code> 태그로 변경하겠습니다. 굳이 하지 않아도 문제가 없지만 <code class="language-plaintext highlighter-rouge">email</code> 형식으로 변경하면 모바일에서 자판이 <code class="language-plaintext highlighter-rouge">email</code> 용으로 나타나는 장점이 있습니다. 템플릿은 그대로 두고 폼클래스를 수정해서 <code class="language-plaintext highlighter-rouge">CharField</code>로 선언된 부분을 <code class="language-plaintext highlighter-rouge">EmailField</code>로 변경해줍니다.</p>
<p>폼부터 정의할 건데 <code class="language-plaintext highlighter-rouge">LoginView</code> 에서 <code class="language-plaintext highlighter-rouge">form_class</code> 로 설정된 <code class="language-plaintext highlighter-rouge">AuthenticationFrom</code> 을 상속받아 LoginForm 이라는 폼클래스를 정의하고, <code class="language-plaintext highlighter-rouge">username</code> 이라는 클래스를 <code class="language-plaintext highlighter-rouge">EmailField</code>로 변경하고 내부의 widget을 <code class="language-plaintext highlighter-rouge">EmailInput</code>으로 변경합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/forms.py
</span>
<span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.forms</span> <span class="kn">import</span> <span class="n">UserCreationForm</span><span class="p">,</span> <span class="n">AuthenticationForm</span>
<span class="kn">from</span> <span class="nn">django.forms</span> <span class="kn">import</span> <span class="n">EmailField</span>
<span class="k">class</span> <span class="nc">UserRegistrationForm</span><span class="p">(</span><span class="n">UserCreationForm</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">LoginForm</span><span class="p">(</span><span class="n">AuthenticationForm</span><span class="p">):</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">EmailField</span><span class="p">(</span><span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">EmailInput</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s">'autofocus'</span><span class="p">:</span> <span class="bp">True</span><span class="p">}))</span>
</code></pre></div></div>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Field</code>와 <code class="language-plaintext highlighter-rouge">Widget</code>의 역할이 궁금하실 수 있는데 <strong>Field는 유효성 검증과 위젯의 기능들을 호출</strong>하는 역할을 합니다. <strong>Widget은 필드의 실제 렌더링</strong>과 관련된 역할을 합니다. <code class="language-plaintext highlighter-rouge">Widget에</code> 따라 input_type 클래스변수에 email 인지 text 인지 password 인지가 정의되어 있습니다.</p>
</blockquote>
<p>이제 새로 생성된 <code class="language-plaintext highlighter-rouge">LoginForm</code>을 <code class="language-plaintext highlighter-rouge">UserLoginView</code> 에 설정해주면 되는데, <code class="language-plaintext highlighter-rouge">LoginView</code> 처럼 <code class="language-plaintext highlighter-rouge">form_class</code> 클래스변수에 정의해서 오버라이드 하는 방법도 있으나 권장하는 방법은 <strong><code class="language-plaintext highlighter-rouge">authentication_form</code> 이라는 클래스변수에 <code class="language-plaintext highlighter-rouge">LoginForm</code>을 설정</strong>하는 것 입니다. 강제사항은 아니나 <code class="language-plaintext highlighter-rouge">LoginForm</code> 내부적으로 <code class="language-plaintext highlighter-rouge">authentication_form</code> 을 먼저 확인하고 없으면 <code class="language-plaintext highlighter-rouge">form_class</code>를 이용하도록 되어 있습니다. 즉 커스터마이징 할 거라면 <code class="language-plaintext highlighter-rouge">authentication_form</code> 를 사용하라는 원 제작자의 의도가 있습니다. 왜 이렇게 하기를 원하는 지 <del>저의 수준으로는</del> 알 수 없습니다만 프레임워크는 그 제작자가 설계한 의도대로 따라 주는게 가장 문제가 생기지 않는 방법입니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.views</span> <span class="kn">import</span> <span class="n">LoginView</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.forms</span> <span class="kn">import</span> <span class="n">UserRegistrationForm</span><span class="p">,</span> <span class="n">LoginForm</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/article/'</span>
<span class="k">class</span> <span class="nc">UserLoginView</span><span class="p">(</span><span class="n">LoginView</span><span class="p">):</span>
<span class="n">authentication_form</span> <span class="o">=</span> <span class="n">LoginForm</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'user/login_form.html'</span>
<span class="k">def</span> <span class="nf">form_invalid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'로그인에 실패하였습니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_invalid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre></div></div>
<p>장고가 다시 재시작되면 로그인화면을 접속해서 확인해보세요. 정상적으로 이메일 형식의 <code class="language-plaintext highlighter-rouge">input</code> 태그로 변경되었음을 알 수 있습니다.</p>
<pre><code class="language-mermaid">classDiagram
Form <|-- AuthenticationForm
AuthenticationForm <|-- LoginForm
AuthenticationForm : username = UsernameField(widget=TextInput)
AuthenticationForm : password = CharField(widget=PasswordInput)
AuthenticationForm : clean()
AuthenticationForm : confirm_login_allowed()
AuthenticationForm : get_user()
AuthenticationForm : get_invalid_login_error()
LoginForm : username = EmailField(widget=EmailInput)
SuccessURLAllowedHostsMixin <|-- LoginView
FormView <|-- LoginView
LoginView <|-- UserLoginView
LoginView : form_class = AuthenticationForm
LoginView : authentication_form = None
LoginView : redirect_field_name = 'next'
LoginView : template_name = 'registration/login.html'
LoginView : redirect_authenticated_user = False
LoginView : get_success_url()
LoginView : get_redirect_url()
LoginView : get_form_class()
LoginView : get_form_kwargs()
LoginView : form_valid()
LoginView : get_context_data()
UserLoginView : authentication_form = LoginForm
UserLoginView : template_name = 'user/login_form.html'
</code></pre>
<h2 id="2-세션관리">2. 세션관리</h2>
<p>로그인정보를 삭제할 때 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 브라우저의 쿠키에서 삭제만 해도 사라진다고 설명 했었는데, 반대로 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 다른 브라우저에 세팅을 하면 어떻게 될까요? 현재로서는 해당세션이 동일하게 복제가 됩니다. 예를 크롬으로 로그인한 뒤 쿠키의 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 복사해서, safari 브라우저 <code class="language-plaintext highlighter-rouge">sessionid</code> 라는 쿠키를 저장하게 되면 safari 브라우저에서도 로그인된 것처럼 사용하실 수 있습니다. 이러한 헛점들이 있는데 장고에서는 최대한 다양한 옵션으로 악의적인 행위를 차단하고 있습니다.</p>
<p>이렇게 쿠키값이 유출이 된다면 자신만의 개인정보가 누출되거나, 자신의 권한으로 제3자가 내가 원하지 않는 어떠한 행위를 할 수 있습니다. 그래서 장고의 <code class="language-plaintext highlighter-rouge">SessionMiddleware</code> 에서는 세션관리를 위한 다양한 옵션을 제공합니다. 모든 것을 다 소개할 필요는 없고 자세한 사항은 장고 <a href="https://docs.djangoproject.com/en/2.1/topics/http/sessions/">공식문서</a>에 설명하고 있으니 여기서는 자주 사용하는 몇 가지만 소개하고 실제 적용해보겠습니다.</p>
<h3 id="세션-미들웨어">세션 미들웨어</h3>
<p>먼저 세션이 무엇인지 알아야 하는데요. <strong>세션은 로그인한 사용자에게만 발급하는 일종의 <del>표딱지</del> 사용권</strong>입니다. 사용자에게는 이 사용권의 <code class="language-plaintext highlighter-rouge">sessionid</code> 만 알려주고(사용자 쿠키에 저장합니다.), 장고는 해당 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 누구에게 발급해줬는지 세션이라는 객체에 사용자의 인증정보를 저장합니다. 이 세션객체는 기본적으로 데이터베이스에 저장되도록 되어 있지만 다양한 방식으로 관리할 수 있습니다.</p>
<p>세션 미들웨어는 세션정보를 어디에 어떤 방식으로 저장하는지에 대한 부분만 관여합니다. 세션에 어떤 데이터가 있는지는 관심이 없습니다. 세션 미들웨어는 여러가지 세션 백엔드 중 하나를 선택해서 <strong>백엔드에게 실제 저장기능을 위임</strong>합니다.</p>
<table>
<thead>
<tr>
<th>세션 백엔드 모듈이름</th>
<th>기능</th>
</tr>
</thead>
<tbody>
<tr>
<td>django.contrib.sessions.backends.db</td>
<td>데이터베이스에 저장하는 백엔드</td>
</tr>
<tr>
<td>django.contrib.sessions.backends.cache</td>
<td>캐시에 저장하는 백엔드</td>
</tr>
<tr>
<td>django.contrib.sessions.backends.cache_db</td>
<td>캐시와 데이터베이스를 병행하는 백엔드</td>
</tr>
<tr>
<td>django.contrib.sessions.backends.file</td>
<td>파일에 저장하는 백엔드</td>
</tr>
<tr>
<td>django.contrib.sessions.backends.signed_cookie</td>
<td>쿠키에 저장하는 백엔드</td>
</tr>
</tbody>
</table>
<p>쿠키에 세션을 저장하는 것은 사용자에게 모든 정보를 노출하는 것이기 때문에 그다지 좋지 않습니다. 또한 쿠키의 크기가 커지니 네트워크 부하에도 영향이 있습니다. 사용자에게 세션데이터를 노출해야만 하는 경우에만 사용합니다. 세션데이터에 민감한 내용이 들어있다면 사용해서는 안됩니다.</p>
<p>가장 흔하게 사용하는 방법은 데이터베이스 백엔드와 캐시 백엔드입니다. 데이터베이스는 아주 큰 용량이 비교적 빠른 성능을 보장하므로 범용적으로 사용하기에 좋습니다. 캐시 백엔드는 장고의 전역 캐시 백엔드에 세션데이터를 저장합니다. 캐시 백엔드가 메모리 기반이라면 캐시서버가 리부팅될 때 모든 데이터가 초기화 될 수 있으니 메모리 기반의 캐시 백엔드를 사용한다면 데이터베이스와 병행하도록 설정해야 합니다.</p>
<p>더 자세히 알고 싶다면 장고 <a href="https://docs.djangoproject.com/en/2.1/topics/http/sessions/#configuring-the-session-engine">공식문서</a>를 참고하세요.</p>
<p>세션미들웨어는 사용자가 요청을 할 때마다 쿠키의 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 확인 후 세션 백엔드 세션데이터를 불러와 <code class="language-plaintext highlighter-rouge">request.session</code> 객체에 저장합니다. 만일 세션데이터가 유효하지 않다면(expire_date, path 등) 빈 세션데이터만 저장합니다. <code class="language-plaintext highlighter-rouge">request.session</code> 객체를 접근할 수 있는 어디서든 객체에 추가적인 데이터를 저장하거나 삭제, 수정이 가능합니다. 대게 빈번하게 사용하는 사용자의 데이터를 캐시의 목적으로 저장하거나 커스텀 인증 미들웨어를 사용할 때 세션객체를 수정합니다. 그 외에 임의로 삭제, 수정은 하지 않도록 주의하셔야 합니다.</p>
<blockquote>
<p>세션데이터는 사용자가 매 요청마다 불러와야 하는 값이고 응답할 때까지 메모리에 저장하고 있어야 하기 때문에 너무 많은 데이터를 가지고 있으면 서버의 가용성에 지장이 생길 수 있습니다. 대부분의 사용자가 매번 필요한 데이터일 경우에만 세션객체에 추가하셔야 합니다.</p>
</blockquote>
<h3 id="안전한-세션-관리-방법">안전한 세션 관리 방법</h3>
<p>아까 전에 설명한대로 <code class="language-plaintext highlighter-rouge">sessionid</code> 가 누출됐을 경우도 안전하지 못한 상황이 되는데 이 문제를 어떻게 해결하는 것이 좋을까요? 장고에서 여러가지 방법을 제공합니다만 기본적으로 <strong><code class="language-plaintext highlighter-rouge">secure 쿠키</code>로 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 저장</strong>하는 방법과 <strong>session 의 유효기간을 설정</strong>해서 유효기간이 지난 이후에는 세션데이터를 무효화 시키는 기능을 제공합니다.</p>
<p><code class="language-plaintext highlighter-rouge">secure 쿠키</code>는 javascipt로는 세션값을 불러올 수 없고, 브라우저의 개발자도구를 열어 확인하거나, 브라우저를 바이러스 또는 activex 등으로 메모리의 값을 찾는 것 외에는 <code class="language-plaintext highlighter-rouge">sessionid</code> 값을 알 수 없게 하는 것 입니다. 즉 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 브라우저를 실제로 보고 있는 사람 또는 서버 외에서는 볼 수 없게 하는 것입니다. <code class="language-plaintext highlighter-rouge">secure 쿠키</code>설정은 설정파일에 <strong><code class="language-plaintext highlighter-rouge">SESSION_COOKIE_SECURE</code> 변수에 True로 설정</strong>하면 활성화가 되는데 기본적으로 True가 설정되어 있습니다. <strong><em>꼭 필요한 경우 외에는 False로 변경하지 마세요.</em></strong> <del>반드시 False로 변경해야 하는 경우는 없습니다.</del></p>
<p>세션의 유효기간이 설정되면 쿠키도 동일한 유효기간이 설정됩니다. 브라우저에서는 유효기간이 지난 쿠키는 자동삭제를 합니다. 만일 쿠키를 조작해서 유효기간을 충분히 사용가능하게 변경하더라도 서버에 저장된 유효기간으로 유효성을 검사하기 때문에 유효기간이 짧으면 짧을 수록 보안강도가 높아집니다. 문제는 세션의 유효기간이 지나면 로그인되지 않은 상태로 인식하기 때문에 다시 로그인을 해줘야 합니다. <strong>유효기간 설정은 설정파일에 <code class="language-plaintext highlighter-rouge">SESSION_COOKIE_AGE</code> 값에 초단위의 기간을 설정</strong>하면 됩니다. 기본값으로 1209600(2주)가 설정되어 있으니 서비스 정책에 맞게 수정하시면 됩니다. 테스트 삼아 <code class="language-plaintext highlighter-rouge">SESSION_COOKIE_AGE = 10</code>으로 설정하고 재로그인을 해봅니다. 10초 이후에 다시 접근하면 로그인이 풀려있게 됩니다.</p>
<p>또다른 간편한 방법으로 쿠키의 세션id 이름을 변경하는 것입니다. 설정파일의 <code class="language-plaintext highlighter-rouge">SESSION_COOKIE_NAME</code> 값을 누군가 쉽게 알 수 없는 이름으로 변경하면 악의적인 목적을 가진 사람에게는 조금 불편함을 줄 수 있습니다. 이 방법은 보안강도로 표현하면 아주 미세하게 효과를 줍니다. 보통 보안목적보다는 <code class="language-plaintext highlighter-rouge">sessionid</code> 라는 이름이 중복이 되거나 어쩔 수 없이 변경해야 할 경우에 사용합니다.</p>
<p>이 외에 강력한 방법으로 매 <strong>요청 때마다 <code class="language-plaintext highlighter-rouge">request.session</code> 객체의 <code class="language-plaintext highlighter-rouge">cycle_key()</code> 메소드를 호출</strong>하는 겁니다. <code class="language-plaintext highlighter-rouge">cycle_key()</code> 메소드가 호출될 때마다 <code class="language-plaintext highlighter-rouge">sessionid</code> 가 변경되고 변경된 값이 쿠키에 저장이 됩니다. <code class="language-plaintext highlighter-rouge">sessionid</code> 가 노출되었더라도 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 악의적인 목적으로 사용하기 전에 또다른 요청을 한다면 이전의 <code class="language-plaintext highlighter-rouge">sessionid</code> 값은 유효하지 않은 것이 됩니다. 하지만 매 요청마다 세션 백엔드가 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 다시 저장해야 하기 때문에 서버의 가용성이 조금 떨어진다는 것이 문제입니다. 서버의 성능보다는 안정성이 중요하다면 이 방법을 사용해도 좋습니다.</p>
<h2 id="2-인증관리">2. 인증관리</h2>
<p><strong>인증이라는 것은 정당한 절차를 통해 접속했는 지 확인하고 접속자가 누구인지를 증명하는 절차</strong>를 의미합니다. 세션이 증명서라면 증명서에 있는 인증은 증명서를 생성하는 절차를 담당하고, 누군가 증명서를 들고 왔을 때 누구의 증명서인 지를 확증하는 것입니다. 장고의 인증은 기본적으로 데이터베이스를 기반으로 하고 있으나 추가적인 라이브러리를 설치하거나 별도의 모듈을 구현한다면 소셜로그인과 같은 외부의 api를 연동해서 인증을 하고 사용자의 정보를 공개된 범위 내에서 제공받을 수도 있습니다.</p>
<p>로그인을 하면 세션 미들웨어가 생성한 <code class="language-plaintext highlighter-rouge">request.session</code> 객체에 세션정보를 저장합니다. 이 때 저장한 세션정보의 내용은 세션 백엔드에 의해 자동으로 저장됩니다. 즉 <strong>로그인의 최종목적은 사용자의 세션객체를 <code class="language-plaintext highlighter-rouge">request.session</code> 객체에 저장</strong>하는 것입니다.</p>
<h3 id="인증-미들웨어">인증 미들웨어</h3>
<pre><code class="language-mermaid">graph TD
user((사용자)) ==> django[장고 http 서버]
django ==> session[세션미들웨어]
session -- 세션객체 불러와 --- session_backend(세션 백엔드)
session == request.session ==> auth[인증미들웨어]
auth -- 사용자정보 불러와 --- auth_backend(인증 백엔드)
auth == request.user ==> view{뷰}
view -.-> auth
auth -.-> session
session -. 세션저장, 쿠키설정 .-> django
django -.-> user
caption(인증 과정)
style caption fill: #ffffffff
</code></pre>
<p>세션 미들웨어가 <code class="language-plaintext highlighter-rouge">sessionid</code> 를 가지고 <code class="language-plaintext highlighter-rouge">request.session</code> 객체를 저장하면 인증 미들웨어(<code class="language-plaintext highlighter-rouge">AuthenticationMiddleware</code>)는 이 세션객체의 내용을 확인하고 사용자 정보를 불러와 request.user 객체에 저장합니다. 실질적으로 미들웨어가 직접 사용자의 신원을 확인하지 않고 인증 백엔드에게 위임합니다. 인증 백엔드는 사용자 정보를 가져올 때 세션객체의 내용과 사용자 정보의 내용을 비교하여 세션정보가 사용자 정보와 일치하지 않을 경우 <code class="language-plaintext highlighter-rouge">request.user</code> 객체에 <code class="language-plaintext highlighter-rouge">AnonymousUser</code> 객체를 저장합니다.</p>
<blockquote>
<p>정확히 설명하면 사용자 모델을 매번 불러오는 것이 아니라 미들웨어는 일단 <code class="language-plaintext highlighter-rouge">SimpleLazyObject</code> 객체를 반환합니다. <code class="language-plaintext highlighter-rouge">request.user = SimpleLazyObject(lambda: get_user(request))</code> <code class="language-plaintext highlighter-rouge">request.user</code> 가 가리키는 <code class="language-plaintext highlighter-rouge">SimpleLazyObject</code> 객체는 처음에는 빈 객체이지만 <code class="language-plaintext highlighter-rouge">request.user</code> 객체에서 특정 속성(<code class="language-plaintext highlighter-rouge">is_authenticated</code>, <code class="language-plaintext highlighter-rouge">is_superuser</code>, <code class="language-plaintext highlighter-rouge">is_staff</code> 등)을 접근할 때 실제 데이터를 불러와 캐싱합니다. 즉 <code class="language-plaintext highlighter-rouge">request.user</code> 객체의 속성값을 읽으려 하기 전까지는 장고(인증 미들웨어)는 사용자가 누구인지 모르고 세션객체의 내용만 알 수 있습니다. 이렇게 설계된 이유는 모든 요청 때마다 사용자 정보를 불러온다면 사용하지도 않을 사용자 정보 때문에 서버 자원을 소모하지 않게 하기 위함입니다. <code class="language-plaintext highlighter-rouge">SimpleLazyObject</code> 는 인증 외의 많은 부분에서도 유용하게 사용할 수 있으니 사용법을 반드시 기억해주시길 바랍니다.</p>
</blockquote>
<p>장고에서 기본적으로 세션객체에 <code class="language-plaintext highlighter-rouge">_auth_user_id</code>, <code class="language-plaintext highlighter-rouge">_auth_user_backend</code>, <code class="language-plaintext highlighter-rouge">_auth_user_hash</code> 이 세가지 정보를 딕셔너리 형태로 저장합니다.</p>
<table>
<thead>
<tr>
<th>key</th>
<th>데이터</th>
</tr>
</thead>
<tbody>
<tr>
<td>_auth_user_id</td>
<td>사용자 정보 id</td>
</tr>
<tr>
<td>_auth_user_backend</td>
<td>로그인할 때 사용한 인증 백엔드</td>
</tr>
<tr>
<td>_auth_user_hash</td>
<td>사용자정보 테이블에 저장된 패스워드값의 해시값</td>
</tr>
</tbody>
</table>
<p>인증미들웨어는 인증 백엔드를 통해 사용자를 식별하는 하도록 인증기능을 위임합니다. 소셜로그인 같은 외부의 인증 api를 사용하는 경우는 각 api 별로 백엔드가 달라질 수 있습니다. 장고는 기본적으로 데이터베이스의 사용자 정보 모델을 기반으로 인증을 처리하는 백엔드를 제공합니다. 모델 인증 백엔드는 <code class="language-plaintext highlighter-rouge">_auth_user_id</code> 를 가지고 사용자가 누구인지 식별하고 사용자 모델의 객체를<code class="language-plaintext highlighter-rouge"> request.user</code> 객체에 저장합니다.</p>
<p>그리고 사용자의 요청에 의해 비밀번호가 변경되었을 경우 변경된 비밀번호(데이터베이스에 저장된 값)의 해시값과 <code class="language-plaintext highlighter-rouge">_auth_user_hash</code> 값을 비교하면 같지 않을테니 이전 세션은 유효하지 않게 됩니다. 즉 <strong>비밀번호를 변경하면 이전의 세션은 유효하지 않고 로그아웃상태로 전환</strong>됩니다.</p>
<h3 id="안전한-인증-방법">안전한 인증 방법</h3>
<p>로그인은 위에서 설명드린 대로 장고의 auth 프레임워크를 이용하시면 됩니다. 위에서 설명드린 방식 말고 특정한 케이스에 로그인폼을 통하지 않고도 인증을 하거나 재인증을 하기 원할 때 auth 프레임워크의 <code class="language-plaintext highlighter-rouge">login</code> 함수를 사용하시면 됩니다. 아무리 장고에서 안전하고 정확하게 인증을 한다하더라도 장고 앱으로 데이터가 전송되기 전인 네트워크에서 이동하는 과정 중에 노출된 사용자 정보는 장고가 보호해줄 수 없습니다. 예를 들어 로그인 할 때 이메일과 비밀번호를 입력하는데 http에서는 입력한 문자(이메일, 비밀번호 등) 그대로 네트워크를 통해 전송이 됩니다. 잘 알려진대로 공용 네트워크에서는 동일한 네트워크 안에 있는 누군가에게 해당 내용이 노출이 될 여지가 많이 있습니다. 그래서 <strong>어떠한 안전한 인증 기능을 제공하기 이전에 반드시 https를 제공</strong>해야 합니다. 할 수만 있다면 https를 반드시 사용하시기 바랍니다.</p>
<blockquote>
<p>https를 제공할 수 없다면 소셜로그인 기능을 연동하는 것도 좋은 방법입니다. 소셜로그인은 대부분(제가 아는 한 모든) 프로바이더들이 https를 제공합니다. 다만 반드시 프로바이더가 정한 규칙 또는 oauth의 규칙을 준수하셔서 연동하시기 바랍니다.</p>
</blockquote>
<p>인증된 사용자에게만 접속을 허용하기 뷰클래스에 <code class="language-plaintext highlighter-rouge">LoginRequiredMixin</code> 이 추가(CBV)되었거나 핸들러 함수가 <code class="language-plaintext highlighter-rouge">login_required</code> 데코레이터로 wrapping 을 합니다. 이것들은 내부적으로 <code class="language-plaintext highlighter-rouge">requests.user.is_authenticated</code> 값을 비교합니다. 여러분들도 별도의 프로세스로 사용자의 인증여부를 확인해야 하는 경우 가급적 <code class="language-plaintext highlighter-rouge">requests.user.is_authenticated</code> 의 값을 이용하시면 오류의 가능성이 현저히 줄어듭니다.</p>
<blockquote>
<p>http만 제공하는 웹서비스는 손에 면도칼을 든 사람들이 새로 왁스 칠한 마룻바닥 위에서 빠른 속도로 춤을 추는 것과 같다.
swarf00, 나는야 춤추는 개발자...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크에서 기본 제공하는 auth 프레임워크를 이용하여 사용자 로그인을 구현하는 방법을 설명합니다. 또한 모델폼을 이용하여 쉽게 템플릿을 구현하는 방법을 알아봅니다.사용자인증(1)2018-12-07T00:00:00+09:002018-12-07T00:00:00+09:00https://swarf00.github.io/2018/12/07/registration<h2 id="1-새로운-사용자모델-정의">1. 새로운 사용자모델 정의</h2>
<p>기본적인 게시판 기능이 완성되었는데, 누구라도 작성이 가능하고 누구라도 자신이 작성하지 않은 게시글까지 수정이 가능한 상황입니다. 또한 어뷰징을 하는 사용자가 있다면 그런놈들은 영구적으로 사용을 하지 못하도록 하고 싶습니다. 이럴 때 접속자들에게 꼬리표를 붙여주면 됩니다. 만일 꼬리표를 마음대로 떼버린다면 기능에 제한해버리면 됩니다. 이것을 <code class="language-plaintext highlighter-rouge">사용자인증</code> 이라고 합니다. 이제부터는 장고의 다양한 기본 기능들을 폭 넓게 활용해도록 하겠습니다.</p>
<h3 id="장고-auth-프레임워크-소개">장고 auth 프레임워크 소개</h3>
<p>장고 admin 사이트에 접속할 때 생성했던 슈퍼유저가 기억나실 겁니다. 이것이 장고에서 기본적으로 제공하는 인증기능입니다. <code class="language-plaintext highlighter-rouge">id</code>와 <code class="language-plaintext highlighter-rouge">비밀번호</code>를 포함한 모든 사용자정보는 데이터베이스에 기록이 되고 로그인을 할 때 입력한 <code class="language-plaintext highlighter-rouge">id</code>와 <code class="language-plaintext highlighter-rouge">비밀번호</code>가 동일한 경우 해당 <code class="language-plaintext highlighter-rouge">id</code>의 사용자가 맞다고 판단하게 됩니다.
장고 auth 프레임워크는 크게 <code class="language-plaintext highlighter-rouge">가입</code>, <code class="language-plaintext highlighter-rouge">로그인</code>, <code class="language-plaintext highlighter-rouge">로그아웃</code> 세가지의 기능을 제공합니다. 앞으로 각 기능이 어떻게 구현되어 있는지 메커니즘을 이해하며 공부를 하면 좀 더 안전한 웹서비스를 구축할 수 있습니다.</p>
<p>사용자 인증이라는 기능은 모두들 아시는 기능이겠지만 엔지니어링적으로 해석해보면 <strong>사용자정보를 데이터베이스에 저장</strong>하고, 저장된 데이터를 구분할 수 있는 <strong>유일한(누구와도 중복되지 않는) 키(key)를 지정해서 사용자를 식별(구별)</strong>하는 기능입니다. 그렇기 때문에 가입의 핵심기능은 사용자를 구분할 수 있는 키를 사용자로부터 얻어오고 이것이 중복되지 않도록 하는 것입니다. 추가로 우리는 로그인 기능을 제공할 것이기 때문에 비밀번호도 사용자에게 입력받아야 합니다. 비밀번호는 <del>사회공학적으로</del> 여러 사이트에서 동일하게 사용하는 경우도 있기 때문에 현재의 웹사이트가 어떠한 문제로 또는 내부자의 악의적인 행위에 의해 데이터베이스가 노출될 경우 사용자에게 큰 피해를 줄 수 있습니다. 때문에 보통 비밀번호는 암호화를 해서 해당 웹서비스에서만 알아볼 수 있게 하거나, 해싱함수를 통해서 원래의 비밀번호를 알아볼 수 없도록 만들어 저장합니다.</p>
<p>암호화를 하는 방법은 성능적으로도 크게 차이나지 않지만 좋지않은 편이고, 암호화키를 알고있거나 소스코드를 볼 수 있다면 원래의 비밀번호를 알 수 있기 때문에 그리 좋은 방법은 아닙니다. 다만 어쩔 수 없이 원래의 비밀번호를 사용자에게 제공해야 할 경우에 한 해서만 사용할 수 있습니다.</p>
<p><strong>대부분의 웹사이트는 비밀번호를 해싱함수를 통해 원래의 비밀번호를 알아낼 수 없도록 저장</strong>합니다. 혹시라도 데이터베이스가 해킹을 당한다 하더라도 원래의 비밀번호는 <del>해시 알고리즘의 보안강도와 키길이에 따라</del> 알아낼 수 없습니다.</p>
<p>해시된 비밀번호를 사용하는 방법은 로그인 기능을 구현할 때 알아보도록 하고 지금은 장고에서 비밀번호가 <strong>해시함수로 원래의 비밀번호를 알아볼 수 없게 저장한다는 사실을 기억</strong>해두면 됩니다.</p>
<blockquote>
<p>암호화와 해시</p>
<ol>
<li>공통점 - 원래의 텍스트(평문)을 알아볼 수 없는 텍스트(암호문)으로 변경시켜줍니다.</li>
<li>차이점 - <strong>암호화는 암호문에서 평문으로 되돌릴 수 있지만 해시는 되돌릴 수 없습니다</strong>.</li>
<li>암호 알고리즘의 종류 - <code class="language-plaintext highlighter-rouge">des</code>, <code class="language-plaintext highlighter-rouge">aes</code>, <code class="language-plaintext highlighter-rouge">seed</code>, <code class="language-plaintext highlighter-rouge">rsa</code> 등</li>
<li>해시 알고리즘의 종류 - <code class="language-plaintext highlighter-rouge">md5</code>, <code class="language-plaintext highlighter-rouge">sha1</code>, <code class="language-plaintext highlighter-rouge">sha256</code>, <code class="language-plaintext highlighter-rouge">sha256</code> 등</li>
</ol>
</blockquote>
<p>장고의 기본 제공 모델들도 실제 데이터베이스에 마이그레이션이 되어야 동작이 되는데, 이 모델들이 언제 마이그레이션이 되었을 지 궁금하지 않으시더라도 알려드립니다. <a href="https://swarf00.github.io/build-model.html#모델-수정">모델 만들기</a>를 보시면 <code class="language-plaintext highlighter-rouge">migrate</code> 명령어를 실행할 때 <code class="language-plaintext highlighter-rouge">Applying auth.000******</code> 이런식의 출력을 보셨을 겁니다. 이것이 장고의 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크의 모델들이 마이그레이션되었다는 로그입니다. 그럼 실제 db.sqlite3 파일을 열어서 테이블이 생성되었는지 <del>귀찮더라도</del> 확인을 해봅니다. <code class="language-plaintext highlighter-rouge">db.sqlite3</code> 파일은 <code class="language-plaintext highlighter-rouge">sqlite3</code> 이라는 유틸리티(또는 그외 <code class="language-plaintext highlighter-rouge">sqlite3</code> 지원 유틸리티)를 통해서 확인하셔야 합니다. 일반 텍스트에디터로는 내용을 확인할 수 없습니다. <strong><code class="language-plaintext highlighter-rouge">sqlite</code> 파일을 살펴보는 부분은 건너뛰셔도 상관</strong>없습니다.</p>
<p><code class="language-plaintext highlighter-rouge">sqlite3</code> 로 파일을 열면 프롬프트가 출력이 됩니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>sqlite3 db.sqlite3
SQLite version 3.22.0 2018-01-22 18:45:57
Enter <span class="s2">".help"</span> <span class="k">for </span>usage hints.
sqlite>
</code></pre></div></div>
<p>프롬프트에 <code class="language-plaintext highlighter-rouge">.tables</code> 라는 명령어를 입력하고 엔터를 입력하면 테이블 목록이 출력됩니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> .tables
auth_group bbs_article
auth_group_permissions django_admin_log
auth_permission django_content_type
auth_user django_migrations
auth_user_groups django_session
auth_user_user_permissions
</code></pre></div></div>
<p>테이블 목록에 여러 테이블 이름이 출력됩니다. 모든 테이블 이름이 <code class="language-plaintext highlighter-rouge">applabel_modelname</code>으로 구성되어 있습니다. 즉 <code class="language-plaintext highlighter-rouge">auth</code> 앱에는 6개의 테이블, <code class="language-plaintext highlighter-rouge">bbs</code> 앱에는 1개의 테이블, <code class="language-plaintext highlighter-rouge">django</code> 앱에는 3개의 테이블이 생성되어 있습니다. <code class="language-plaintext highlighter-rouge">auth</code> 앱의 <code class="language-plaintext highlighter-rouge">user</code>모델 즉 <code class="language-plaintext highlighter-rouge">auth_user</code> 테이블이 어떤 필드들이 있는지 확인해보면 10개의 필드로 구성되어 있다는 것을 알 수 있습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> PRAGMA table_info<span class="o">(</span>auth_user<span class="o">)</span><span class="p">;</span>
0|id|integer|1||1
1|password|varchar<span class="o">(</span>128<span class="o">)</span>|1||0
2|last_login|datetime|0||0
3|is_superuser|bool|1||0
4|username|varchar<span class="o">(</span>150<span class="o">)</span>|1||0
5|first_name|varchar<span class="o">(</span>30<span class="o">)</span>|1||0
6|email|varchar<span class="o">(</span>254<span class="o">)</span>|1||0
7|is_staff|bool|1||0
8|is_active|bool|1||0
9|date_joined|datetime|1||0
10|last_name|varchar<span class="o">(</span>150<span class="o">)</span>|1||0
</code></pre></div></div>
<p>특별히 sqlite3 유틸리티를 종료(빠져나오는) 방법을 알려드리겠습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> .quit
</code></pre></div></div>
<blockquote>
<p>sqlite3 다운로드</p>
<p>맥은 기본설치되어 있으나 리눅스에서는 apt-get 또는 yum으로 설치하시면 됩니다. 윈도우의 경우에만 특별히 <a href="https://www.sqlite.org/download.html">다운로드</a>를 하시면 됩니다.</p>
</blockquote>
<p><strong>여기까지는 굳이 확인하지 않아도 되는 부분</strong>이지만 아래 부터는 꼭 건너뛰지 마시고 잘 살펴보시길 바랍니다.</p>
<p>가입기능을 구현하기에 앞서 장고 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크의 <code class="language-plaintext highlighter-rouge">User</code> 모델을 그대로 사용할 지 결정해야 합니다. 우선 어떻게 정의되었는 지 확인해봅니다. 장고와 그 외의 라이브러리는 가상환경의 <code class="language-plaintext highlighter-rouge">lib/python3.6/site-packages</code> 디렉토리에 설치가 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test-venv-36/lib/python3.6/site-packages/django/contrib/auth/models.py
</span>
<span class="k">class</span> <span class="nc">AbstractUser</span><span class="p">(</span><span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">):</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span>
<span class="n">_</span><span class="p">(</span><span class="s">'username'</span><span class="p">),</span>
<span class="n">max_length</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>
<span class="n">unique</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="n">help_text</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s">'Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.'</span><span class="p">),</span>
<span class="n">validators</span><span class="o">=</span><span class="p">[</span>
<span class="n">validators</span><span class="o">.</span><span class="n">RegexValidator</span><span class="p">(</span>
<span class="s">r'^[\w.@+-]+$'</span><span class="p">,</span>
<span class="n">_</span><span class="p">(</span><span class="s">'Enter a valid username. This value may contain only '</span>
<span class="s">'letters, numbers '</span> <span class="s">'and @/./+/-/_ characters.'</span><span class="p">)</span>
<span class="p">),</span>
<span class="p">],</span>
<span class="n">error_messages</span><span class="o">=</span><span class="p">{</span>
<span class="s">'unique'</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s">"A user with that username already exists."</span><span class="p">),</span>
<span class="p">},</span>
<span class="p">)</span>
<span class="n">first_name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">'first name'</span><span class="p">),</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">last_name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">'last name'</span><span class="p">),</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">EmailField</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">'email address'</span><span class="p">),</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">is_staff</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span>
<span class="n">_</span><span class="p">(</span><span class="s">'staff status'</span><span class="p">),</span>
<span class="n">default</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
<span class="n">help_text</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s">'Designates whether the user can log into this admin site.'</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">is_active</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span>
<span class="n">_</span><span class="p">(</span><span class="s">'active'</span><span class="p">),</span>
<span class="n">default</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="n">help_text</span><span class="o">=</span><span class="n">_</span><span class="p">(</span>
<span class="s">'Designates whether this user should be treated as active. '</span>
<span class="s">'Unselect this instead of deleting accounts.'</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">date_joined</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">'date joined'</span><span class="p">),</span> <span class="n">default</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">verbose_name</span> <span class="o">=</span> <span class="n">_</span><span class="p">(</span><span class="s">'user'</span><span class="p">)</span>
<span class="n">verbose_name_plural</span> <span class="o">=</span> <span class="n">_</span><span class="p">(</span><span class="s">'users'</span><span class="p">)</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">def</span> <span class="nf">get_full_name</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s">"""
Returns the first_name plus the last_name, with a space in between.
"""</span>
<span class="n">full_name</span> <span class="o">=</span> <span class="s">'</span><span class="si">%</span><span class="s">s </span><span class="si">%</span><span class="s">s'</span> <span class="o">%</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">last_name</span><span class="p">)</span>
<span class="k">return</span> <span class="n">full_name</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_short_name</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="s">"Returns the short name for the user."</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">first_name</span>
<span class="k">def</span> <span class="nf">email_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="s">"""
Sends an email to this User.
"""</span>
<span class="n">send_mail</span><span class="p">(</span><span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="p">,</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">email</span><span class="p">],</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractUser</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">(</span><span class="n">AbstractUser</span><span class="o">.</span><span class="n">Meta</span><span class="p">):</span>
<span class="n">swappable</span> <span class="o">=</span> <span class="s">'AUTH_USER_MODEL'</span>
</code></pre></div></div>
<p>복잡하게 생겼지만 조금씩 뜯어보면 복잡한 내용은 없습니다. <code class="language-plaintext highlighter-rouge">AbstractUser</code>, <code class="language-plaintext highlighter-rouge">User</code> 두개의 클래스가 정의되어 있는데 <strong><code class="language-plaintext highlighter-rouge">User</code> 클래스는 <code class="language-plaintext highlighter-rouge">AbstractUser클래스를</code> 상속받아 정의</strong>하고 <code class="language-plaintext highlighter-rouge">Meta</code> 클래스를 제외한 다른 부분은 오버라이딩한 것이 없네요. <code class="language-plaintext highlighter-rouge">AbstractUser는</code> inner 클래스인 <code class="language-plaintext highlighter-rouge">Meta</code> 클래스를 보면 <code class="language-plaintext highlighter-rouge">abstract = True</code>로 옵션이 설정되어 있는 것이 확인됩니다. <strong><code class="language-plaintext highlighter-rouge">abstract</code> 옵션이 <code class="language-plaintext highlighter-rouge">True</code> 로 설정된 클래스는 <code class="language-plaintext highlighter-rouge">makemigrations</code> 커맨드 실행시에 무시</strong>합니다. <code class="language-plaintext highlighter-rouge">abstract</code> 클래스를 보통 비슷한 여러 개의 클래스를 정의할 때 사용합니다. <code class="language-plaintext highlighter-rouge">abstract</code> 모델 클래스의 서브클래스(상속받은 클래스)는 상속받은 필드와 메소드는 정의할 필요없고 추가되는 필드와 메소드만 정의하면 됩니다. <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크는 여러개의 서브클래스를 제공하는 것은 아니지만 아마도 장고를 사용하는 여러분을 위해서 <code class="language-plaintext highlighter-rouge">abstract</code>로 제공하는 것 같습니다. 만일 여러 종류의 <strong>사용자 모델이 필요하다면 이 <code class="language-plaintext highlighter-rouge">AbstractUser</code> 클래스를 상속</strong>받아 사용하시면 편리합니다.</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Meta</code> 클래스는 <code class="language-plaintext highlighter-rouge">outer</code> 클래스의 옵션을 설정합니다. 가장 빈번하게 사용하는 옵션은 <code class="language-plaintext highlighter-rouge">ordering</code>, <code class="language-plaintext highlighter-rouge">indexes</code>, <code class="language-plaintext highlighter-rouge">unique_together</code>, <code class="language-plaintext highlighter-rouge">index_together</code> 가 있습니다.</p>
<ol>
<li><a href="https://docs.djangoproject.com/ko/2.1/ref/models/options/#ordering">ordering</a> - 검색(SELECT) 시 기본 정렬 기준입니다.</li>
<li><a href="https://docs.djangoproject.com/ko/2.1/ref/models/options/#indexes">indexes</a> - 테이블의 인덱스를 정의하는 옵션입니다. 인덱스는 database에서 검색이 빠르게 하는 기능이라고 생각하시면 됩니다. 메모리를 많이 소모하니 빈번하게 검색되는 경우에만 사용합니다. <strong>index_together는 deprecate 상태여서 추후 업데이트시 사라질 수 있으니 indexes를 사용</strong>하면 된다고 생각하시면 됩니다.</li>
<li><a href="https://docs.djangoproject.com/ko/2.1/ref/models/options/#unique-together">unique_together</a> - 데이터의 중복을 막기 위해 사용하는 옵션으로 두개 이상의 필드를 조합할 수 있습니다. database 단에서 unique index를 생성합니다.</li>
<li><a href="https://docs.djangoproject.com/ko/2.1/ref/models/options/#index-together">index_togeter</a> - 데이터베이스에서 index를 생성합니다. 두개 이상의 필드로 정의할 수 있습니다.</li>
</ol>
<p>사용법은 <a href="https://docs.djangoproject.com/en/2.1/ref/models/options/">참고문서</a>를 보시면 됩니다.</p>
<p>이 외도 클래스 옵션이 많이 있으니 <a href="https://docs.djangoproject.com/en/2.1/ref/models/options/">참고문서</a>의 내용을 다 외우지는 마시고 이런 기능들이 있구나 정도만 기억하시면 됩니다.</p>
</blockquote>
<h3 id="커스텀-사용자-모델user">커스텀 사용자 모델(User)</h3>
<p><code class="language-plaintext highlighter-rouge">AbstractUser</code> 모델에 불필요한 필드도 있고, 변경하고 싶은 필드가 있습니다. 예를들면 우리는 사용자 식별자로 <code class="language-plaintext highlighter-rouge">username</code> 이 아니라 <code class="language-plaintext highlighter-rouge">email</code> 을 사용하고 싶습니다. 그러면 중복될 일도 없고, 사용자별로 알림을 보내야 할 때 <code class="language-plaintext highlighter-rouge">email</code> 필드를 사용할 수 있겠죠. 또한 한국사람은 <code class="language-plaintext highlighter-rouge">first_name</code> 과 <code class="language-plaintext highlighter-rouge">last_name</code> 을 따로 구분할 필요가 없는데 불필요하게 구분되어 삭제하면 좋겠습니다. 그 외의 필드들은 필요하거나 있으면 나쁘지 않은 것 같습니다. 그럼 <del>굳이</del> <code class="language-plaintext highlighter-rouge">AbstractUser</code>를 사용하지 않고 새로 사용자 정보 모델을 정의하도록 하겠습니다. <code class="language-plaintext highlighter-rouge">bbs</code> 앱에 사용자 모델을 추가할 수도 있으나 <code class="language-plaintext highlighter-rouge">bbs</code> 앱을 제 3의 프로젝트에서도 사용할 수 있게 하려면 <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크처럼 별도의 앱으로 분리하는 것이 좋을 것 같습니다. 이미 <strong>제 3의 프로젝트에서 기존의 사용자 테이블을 사용하고 있다면 bbs 앱과 사용자정보가 호환되지 않아 문제가 될 수도 있습니다. 또 새로운 프로젝트에 개발할 때 이번에 정의한 사용자 모델을 가져다 쓸 수도 있겠죠</strong>. <del>이렇게 세심하게 앱을 분리하는 모습이 기특합니다.</del></p>
<p>먼저 사용자 앱을 만들겠습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py startapp user
</code></pre></div></div>
<p>생성시키면 <code class="language-plaintext highlighter-rouge">bbs</code> 앱과 같이 <code class="language-plaintext highlighter-rouge">user</code> 디렉토리와 파일들이 생성될 것입니다. 먼저 <code class="language-plaintext highlighter-rouge">AbstractUser</code> 모델을 참고해서 새로운 <code class="language-plaintext highlighter-rouge">User</code> 모델을 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/models.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.models</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">,</span> <span class="n">UserManager</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">django.utils.translation</span> <span class="kn">import</span> <span class="n">ugettext_lazy</span> <span class="k">as</span> <span class="n">_</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">):</span>
<span class="n">email</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">EmailField</span><span class="p">(</span><span class="s">'email'</span><span class="p">,</span> <span class="n">unique</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'이름'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">is_staff</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span><span class="s">'스태프 권한'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">is_active</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span><span class="s">'사용중'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">date_joined</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'가입일'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">)</span>
<span class="n">objects</span> <span class="o">=</span> <span class="n">UserManager</span><span class="p">()</span>
<span class="n">USERNAME_FIELD</span> <span class="o">=</span> <span class="s">'email'</span> <span class="c1"># email을 사용자의 식별자로 설정
</span> <span class="n">REQUIRED_FIELDS</span> <span class="o">=</span> <span class="p">[</span><span class="s">'name'</span><span class="p">]</span> <span class="c1"># 필수입력값
</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">verbose_name</span> <span class="o">=</span> <span class="n">_</span><span class="p">(</span><span class="s">'user'</span><span class="p">)</span>
<span class="n">verbose_name_plural</span> <span class="o">=</span> <span class="n">_</span><span class="p">(</span><span class="s">'users'</span><span class="p">)</span>
<span class="n">swappable</span> <span class="o">=</span> <span class="s">'AUTH_USER_MODEL'</span>
<span class="k">def</span> <span class="nf">email_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="c1"># 이메일 발송 메소드
</span> <span class="n">send_mail</span><span class="p">(</span><span class="n">subject</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">from_email</span><span class="p">,</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">email</span><span class="p">],</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</code></pre></div></div>
<p>모델을 추가한 뒤 <code class="language-plaintext highlighter-rouge">user</code> 앱을 등록합니다. 한가지 더 세팅해줘야 할 것이 있는데 사용자 모델은 여러 앱들에서 참조하고 있는데 장고에서는 <strong>커스터마이징 될 것을 대비해서 <code class="language-plaintext highlighter-rouge">AUTH_USER_MODEL</code> 이라는 설정으로 현재 사용자 모델이 무엇인지 설정</strong>할 수 있도록 했습니다. 물론 우리가 만들 앱에서도 이 설정을 참조해서 사용자 모델을 사용할 것입니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'bbs'</span><span class="p">,</span>
<span class="s">'user'</span><span class="p">,</span>
<span class="s">'django.contrib.admin'</span><span class="p">,</span>
<span class="s">'django.contrib.auth'</span><span class="p">,</span>
<span class="s">'django.contrib.contenttypes'</span><span class="p">,</span>
<span class="s">'django.contrib.sessions'</span><span class="p">,</span>
<span class="s">'django.contrib.messages'</span><span class="p">,</span>
<span class="s">'django.contrib.staticfiles'</span><span class="p">,</span>
<span class="p">]</span>
<span class="c1"># 생략
</span>
<span class="n">AUTH_USER_MODEL</span> <span class="o">=</span> <span class="s">'user.User'</span> <span class="c1"># '앱label.모델명'
</span></code></pre></div></div>
<p>이제 <code class="language-plaintext highlighter-rouge">makemigrations</code> 커맨드로 마이그레이션 파일을 생성합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py makemigrations
Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
File <span class="s2">"./manage.py"</span>, line 15, <span class="k">in</span> <module>
execute_from_command_line<span class="o">(</span>sys.argv<span class="o">)</span>
<span class="c"># 생략</span>
del kwargs[<span class="s1">'editable'</span><span class="o">]</span>
KeyError: <span class="s1">'editable'</span>
</code></pre></div></div>
<p>오류가 발생하는데 <code class="language-plaintext highlighter-rouge">Article</code> 모델의 <strong><code class="language-plaintext highlighter-rouge">created_at</code> 필드의 속성을 강제로 <code class="language-plaintext highlighter-rouge">editable</code>로 변경해서 생긴 문제</strong>입니다. <strong><code class="language-plaintext highlighter-rouge">auto_now_add</code> 대신 디폴트값으로 현재시간을 저장하도록 수정</strong>하면 자동으로 <code class="language-plaintext highlighter-rouge">created_at</code> 값이 생성될 뿐만 아니라 <code class="language-plaintext highlighter-rouge">editable</code> 속성도 <code class="language-plaintext highlighter-rouge">True</code> 로 설정되기 때문에 일석이조입니다. <del>진작에 이렇게 하지</del> 여러모로 장고에 다양한 기능이 있음을 실감합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'[{}] {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</code></pre></div></div>
<p>다시 <code class="language-plaintext highlighter-rouge">makemigration</code> 커맨드를 실행하면 정상적으로 마이그레이션 파일이 생성이 됩니다. <code class="language-plaintext highlighter-rouge">migrate</code> 커맨드까지 이어서 실행하면 마이그레이션이 완료됩니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py makemigrations
Migrations <span class="k">for</span> <span class="s1">'user'</span>:
user/migrations/0001_initial.py
- Create model User
<span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py migrate
Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
File <span class="s2">"./manage.py"</span>, line 15, <span class="k">in</span> <module>
execute_from_command_line<span class="o">(</span>sys.argv<span class="o">)</span>
<span class="c"># 생략</span>
connection.alias,
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency user.0001_initial on database <span class="s1">'default'</span><span class="nb">.</span>
</code></pre></div></div>
<p>이번에는 <code class="language-plaintext highlighter-rouge">admin</code> 의 마이그레이션 파일이 <code class="language-plaintext highlighter-rouge">user</code> 앱의 <code class="language-plaintext highlighter-rouge">0001_initial</code> 마이그레이션 파일에 의존적이다는 메시지인데...우리가 사용하는 <code class="language-plaintext highlighter-rouge">admin</code> 사이트에도 모델이 있는데 이것이 <code class="language-plaintext highlighter-rouge">AUTH_USER_MODEL에</code> 의존적이어서 문제가 생가는 겁니다. <code class="language-plaintext highlighter-rouge">bbs</code> 앱을 <code class="language-plaintext highlighter-rouge">migrate</code> 할 때는 빈 데이터베이스에 <code class="language-plaintext highlighter-rouge">admin</code> 앱과 <code class="language-plaintext highlighter-rouge">bbs</code> 앱이 같이 마이그레이션 되어서 문제가 없었는데, 이미 <strong><code class="language-plaintext highlighter-rouge">admin</code> 앱이 마이그레이션 된 상태에서 커스텀 유저 모델을 마이그레이션 하려니 문제</strong>가 되는 상황입니다. 특별한 경우이기 때문에 이해가 되지 않아도 상관없습니다. 넘어가세요. 아래 해결 방법만 잘 따라 하시면 됩니다.</p>
<p>해결방법은 <strong><code class="language-plaintext highlighter-rouge">admin</code> 앱을 비활성화</strong> 시키면 됩니다. <strong>커스텀 사용자 모델을 마이그레이션할 동안만 비활성화</strong> 합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'bbs'</span><span class="p">,</span>
<span class="s">'user'</span><span class="p">,</span>
<span class="c1"># 'django.contrib.admin',
</span> <span class="s">'django.contrib.auth'</span><span class="p">,</span>
<span class="s">'django.contrib.contenttypes'</span><span class="p">,</span>
<span class="s">'django.contrib.sessions'</span><span class="p">,</span>
<span class="s">'django.contrib.messages'</span><span class="p">,</span>
<span class="s">'django.contrib.staticfiles'</span><span class="p">,</span>
<span class="p">]</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>admin 앱이 장고에서 사용되지 않도록 변경했으니 <strong>urls.py에 어드민 핸들러로 라우팅하는 부분도 잠깐만 비활성화</strong> 시킵니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">bbs.views</span> <span class="kn">import</span> <span class="n">hello</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="c1"># path('admin/', admin.site.urls),
</span><span class="p">]</span>
</code></pre></div></div>
<p>이제 다시 <code class="language-plaintext highlighter-rouge">migrate</code> 커맨드를 실행해보시면 정상적으로 마이그레이션이 실행됩니다.</p>
<p>새로운 사용자 모델이 생성되었으니 <code class="language-plaintext highlighter-rouge">admin</code> 사이트를 접속할 수 있는 슈퍼유저 계정을 다시 생성해줘야 합니다. 기존 테이블(<code class="language-plaintext highlighter-rouge">auth_user</code>)는 이제 사용하지 않기 때문에 새로운 테이블(<code class="language-plaintext highlighter-rouge">user_user</code>)에 다시 만들어 줍니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py migrate
Operations to perform:
Apply all migrations: user
Running migrations:
Applying user.0001_initial... OK
</code></pre></div></div>
<p>정상적으로 돌아왔다면 <strong>비활성화 했던 admin 설정을 되돌려</strong>주면 마이그레이션 성공입니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py createsuperuser
Email address: swarf00@gmail.com
Password:
Password <span class="o">(</span>again<span class="o">)</span>:
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? <span class="o">[</span>y/N]: y
Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
File <span class="s2">"./manage.py"</span>, line 15, <span class="k">in</span> <module>
execute_from_command_line<span class="o">(</span>sys.argv<span class="o">)</span>
<span class="c"># 생략</span>
self.UserModel._default_manager.db_manager<span class="o">(</span>database<span class="o">)</span>.create_superuser<span class="o">(</span><span class="k">**</span>user_data<span class="o">)</span>
TypeError: create_superuser<span class="o">()</span> missing 1 required positional argument: <span class="s1">'username'</span>
</code></pre></div></div>
<p>새로운 사용자모델에서 슈퍼유저를 생성하는 메소드에 <code class="language-plaintext highlighter-rouge">username</code> 이라는 필드가 필수로 설정되어 있다고 하는군요. <code class="language-plaintext highlighter-rouge">auth</code> 프레임워크에서 사용하던 매니저 코드를 살펴보니 살짝만 수정해주면 될 것 같습니다. 모든 사용자 생성 메소드에 <code class="language-plaintext highlighter-rouge">username</code> 필드가 필수로 정의되어 있는데 <code class="language-plaintext highlighter-rouge">username</code> 파라미터는 사용하지 않으니 삭제하면 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/models.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.base_user</span> <span class="kn">import</span> <span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">BaseUserManager</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.models</span> <span class="kn">import</span> <span class="n">PermissionsMixin</span>
<span class="kn">from</span> <span class="nn">django.core.mail</span> <span class="kn">import</span> <span class="n">send_mail</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="k">class</span> <span class="nc">UserManager</span><span class="p">(</span><span class="n">BaseUserManager</span><span class="p">):</span>
<span class="n">use_in_migrations</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">def</span> <span class="nf">_create_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">email</span><span class="p">:</span>
<span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">'The given email must be set'</span><span class="p">)</span>
<span class="n">email</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">normalize_email</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">set_password</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">using</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
<span class="k">def</span> <span class="nf">create_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">):</span>
<span class="n">extra_fields</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'is_staff'</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
<span class="n">extra_fields</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'is_superuser'</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_create_user</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">create_superuser</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">):</span>
<span class="n">extra_fields</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'is_staff'</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span>
<span class="n">extra_fields</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">'is_superuser'</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span>
<span class="k">if</span> <span class="n">extra_fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'is_staff'</span><span class="p">)</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">True</span><span class="p">:</span>
<span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">'Superuser must have is_staff=True.'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">extra_fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'is_superuser'</span><span class="p">)</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">True</span><span class="p">:</span>
<span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">'Superuser must have is_superuser=True.'</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_create_user</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">)</span>
<span class="c1"># 생략
</span>
</code></pre></div></div>
<p>이제 다시 <code class="language-plaintext highlighter-rouge">createsuperuser</code> 커맨드를 실행하시면 정상적으로 슈퍼유저가 생성되고, 생성된 이메일로 로그인하면 정상적으로 <code class="language-plaintext highlighter-rouge">admin</code> 사이트에 접속이 가능합니다. 하지만 원래 있던 <code class="language-plaintext highlighter-rouge">Users</code> 모델이 사라지고, 새로운 사용자 모델이 보이지 않아서 <strong>admin 사이트에 새로운 사용자모델을 추가</strong>해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/admin.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">User</span>
<span class="o">@</span><span class="n">admin</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">User</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">UserAdmin</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
<span class="n">list_display</span> <span class="o">=</span> <span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">,</span> <span class="s">'joined_at'</span><span class="p">,</span> <span class="s">'last_login_at'</span><span class="p">,</span> <span class="s">'is_superuser'</span><span class="p">,</span> <span class="s">'is_active'</span><span class="p">)</span>
<span class="n">list_display_links</span> <span class="o">=</span> <span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="s">'email'</span><span class="p">)</span>
<span class="n">exclude</span> <span class="o">=</span> <span class="p">(</span><span class="s">'password'</span><span class="p">,)</span> <span class="c1"># 사용자 상세 정보에서 비밀번호 필드를 노출하지 않음
</span>
<span class="k">def</span> <span class="nf">joined_at</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
<span class="k">return</span> <span class="n">obj</span><span class="o">.</span><span class="n">date_joined</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">last_login_at</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">obj</span><span class="o">.</span><span class="n">last_login</span><span class="p">:</span>
<span class="k">return</span> <span class="s">''</span>
<span class="k">return</span> <span class="n">obj</span><span class="o">.</span><span class="n">last_login</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d </span><span class="si">%</span><span class="s">H:</span><span class="si">%</span><span class="s">M"</span><span class="p">)</span>
<span class="n">joined_at</span><span class="o">.</span><span class="n">admin_order_field</span> <span class="o">=</span> <span class="s">'-date_joined'</span> <span class="c1"># 가장 최근에 가입한 사람부터 리스팅
</span> <span class="n">joined_at</span><span class="o">.</span><span class="n">short_description</span> <span class="o">=</span> <span class="s">'가입일'</span>
<span class="n">last_login_at</span><span class="o">.</span><span class="n">admin_order_field</span> <span class="o">=</span> <span class="s">'last_login_at'</span>
<span class="n">last_login_at</span><span class="o">.</span><span class="n">short_description</span> <span class="o">=</span> <span class="s">'최근로그인'</span>
</code></pre></div></div>
<p>이렇게 하면 기본 사용자 정보 모델의 정의는 완료됐습니다. 사용자 모델을 만들었으니 이제부터 <code class="language-plaintext highlighter-rouge">가입</code>, <code class="language-plaintext highlighter-rouge">로그인</code>, <code class="language-plaintext highlighter-rouge">로그아웃</code>을 하나씩 만들어 나가면 되겠습니다.</p>
<h2 id="2-회원가입">2. 회원가입</h2>
<h3 id="회원가입-뷰-생성">회원가입 뷰 생성</h3>
<p>이미 회원가입을 위한 모델은 만들어져 있으니 먼저 뷰를 만들어 봅니다. 이번에는 <code class="language-plaintext highlighter-rouge">TemplateView</code> 대신 <code class="language-plaintext highlighter-rouge">CreateView</code> 를 이용할 예정입니다. <code class="language-plaintext highlighter-rouge">CreateView</code> 은 <code class="language-plaintext highlighter-rouge">TemplateView</code> 보다 많은 믹스인들이 추가되어 훨씬 다양한 기능들을 제공합니다. 이 기능들을 모두 사용하려면 <strong>규칙에 따라 설정</strong>해야 할 것들이 몇가지 있습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.models</span> <span class="kn">import</span> <span class="n">User</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">User</span> <span class="c1"># 자동생성 폼에서 사용할 모델
</span> <span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">,</span> <span class="s">'password'</span><span class="p">)</span> <span class="c1"># 자동생성 폼에서 사용할 필드
</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">TemplateView</code> 를 상속받아 정의하던 때랑 달라진 부분이 크게 <strong><code class="language-plaintext highlighter-rouge">model</code>, <code class="language-plaintext highlighter-rouge">fields</code> 클래스변수가 추가되고, <code class="language-plaintext highlighter-rouge">template_name</code> 이라는 클래스변수가 사라진 점</strong>입니다. <code class="language-plaintext highlighter-rouge">CreateView</code> 의 <code class="language-plaintext highlighter-rouge">model</code> 클래스변수에 참조할 모델클래스를 정의하면 앞으로 이 뷰에서는 데이터 관련된 부분은 이 모델을 이용할 것이라는 의미입니다.(폼을 만들 때 <code class="language-plaintext highlighter-rouge">model</code> 변수에 정의한 클래스를 참조하고, 폼의 필드들은 모델의 필드의 속성을 참조합니다.)</p>
<p><code class="language-plaintext highlighter-rouge">model</code> 이 정의되면 내부적으로 Form 객체를 자동 생성하는데 이 때 모델의 모든 필드를 이용해서 폼을 만드는 것이 아니라 <code class="language-plaintext highlighter-rouge">fields</code> 라는 클래스변수를 참조해서 정의되어 있는 필드만 이용합니다. Form 클래스도 커스터마이징이 가능합니다. <code class="language-plaintext highlighter-rouge">django.forms.forms.ModelFrom</code> 클래스를 상속받아 정의한 Form 클래스는 <code class="language-plaintext highlighter-rouge">form_class</code> 라는 클래스변수에 정의해서 <code class="language-plaintext highlighter-rouge">CreateView</code> 가 새로 만들지 않고 우리가 만든 Form 클래스를 사용하도록 설정할 수 있습니다. <code class="language-plaintext highlighter-rouge">UserRegistrationView</code> 에서는 기본 생성 폼을 이용할 예정입니다.</p>
<p><code class="language-plaintext highlighter-rouge">template_name</code> 이라는 클래스변수를 정의하지 않으면 <code class="language-plaintext highlighter-rouge">CreateView</code> 변수에서는 자동으로 <strong>해당 앱의 <code class="language-plaintext highlighter-rouge">templates</code> 디렉토리에서 앱이름의 디렉토리 하위의 <code class="language-plaintext highlighter-rouge">모델명_form.html</code> 파일을 템플릿으로 사용</strong>합니다. 우리의 예제에서는 <code class="language-plaintext highlighter-rouge">user/template/user/user_model.html</code> 파일을 검색하게 되는 겁니다.</p>
<blockquote>
<p>이번 <code class="language-plaintext highlighter-rouge">UserRegistrationView</code> 에서는 사용하지 않는 <code class="language-plaintext highlighter-rouge">template_suffix</code> 클래스변수를 정의하면 template 파일명의 <code class="language-plaintext highlighter-rouge">_form</code> 대신에 다른 문자열로 대치도 가능합니다. 예를들어 <code class="language-plaintext highlighter-rouge">template_suffix</code> 를 <code class="language-plaintext highlighter-rouge">'_registration'</code> 으로 변경하면 <code class="language-plaintext highlighter-rouge">user/template/user/user_registration.html</code> 파일을 찾게 되는 것이죠.</p>
</blockquote>
<p>get, post 요청을 처리할 핸들러 메소드도 정의하지 않았지만 이것 또한 <code class="language-plaintext highlighter-rouge">CreateView</code> 에서 기본적인 것들은 처리해줍니다.</p>
<p>그러면 url <code class="language-plaintext highlighter-rouge">UserRegistrationView</code> 뷰를 라이팅할 수 있도록 <code class="language-plaintext highlighter-rouge">urlpatterns</code> 에 추가를 하고 <code class="language-plaintext highlighter-rouge">user_model.html</code> 이라는 템플릿도 뼈대만 생성해서 정상적으로 뷰가 동작하는 지 확인해봅니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">bbs.views</span> <span class="kn">import</span> <span class="n">hello</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span>
<span class="kn">from</span> <span class="nn">user.views</span> <span class="kn">import</span> <span class="n">UserRegistrationView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'user/create/'</span><span class="p">,</span> <span class="n">UserRegistrationView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>url 패턴들이 조금 많아져서 보기 좋게 빈줄로 앱들을 구분지었습니다. 이것도 맘에 들지 않지만 이 정도는 일단 참습니다. 로그아웃을 구현할 때 저 url 패턴들을 관리하기 편하게 분리하면 되니 일단 내버려둡니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
{% endblock content %}
</code></pre></div></div>
<p>부트스트랩의 css도 매번 나오는게 거슬리지만 우선은 두고 로그아웃 섹션에서 base.html로 옮기겠습니다. runserver 커맨드로 실행하고 브라우저에서 접속하면 정상적으로 아주 깨끗한 화면이 나옵니다. content 블록에 아무것도 넣지 않았으니 이것이 정상입니다.</p>
<h3 id="회원가입-템플릿-생성">회원가입 템플릿 생성</h3>
<p>이제 템플릿에 form 태그와 input 태그들을 입력하면 될 듯 합니다. 이전 <a href="https://swarf00.github.io/build-template.html">Template 만들기</a>에서 이런 방식은 해봤으니 이번은 조금 다르게 장고의 기본 form을 이용해서 template 을 만들어 보겠습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{{ form.as_p }}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p>장고의 기본 생성 Form 으로 템플릿을 만들어 봤습니다. 버튼과 부트스트랩 wrapper 컴포넌트를 제외하면 코드는 몇 줄 안되지만 화면이 생성되었습니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_01.png" alt="CreateView 기본폼 적용후" /></p>
<p>안타깝게도 기본 폼에서 미적 감각을 채워주진 않습니다. 아직 삐뚤빼뚤 볼품없는 화면이지만 css를 조금 추가하면 좀 더 나아질 것 같습니다. 먼저 렌더링된 html을 살펴보고 다음에 style을 맞추도록 하겠습니다.</p>
<blockquote>
<p>렌더링(rendering)은 사전적으로 번역이라는 의미도 가지고 있는데, 이렇게 생각해보시면 이해가 빠르실 겁니다. <strong>form(python) 데이터를 html 데이터로 번역</strong>한다. 즉 장고가 이해하는 데이터를 브라우저가 이해할 수 있는 데이터 형태로 변환하는 작업을 장고템플릿에서 렌더링이라고 합니다. 실제로는 폼이 렌더링 역할을 하지 않고 각 필드들에게 렌더링을 위임하고 각 필드들을 관리합니다. 자세한 내용은 나중에 소개하겠습니다.</p>
</blockquote>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- /user/create/ --></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"csrfmiddlewaretoken"</span> <span class="na">value=</span><span class="s">"SDFHZlmawXy3KhWbQB6rSwqKV3u3houNZDlHP4zMLcNgp2EaKNH3N9K2iXXyOl1P"</span><span class="nt">></span>
<span class="nt"><p></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"id_email"</span><span class="nt">></span>Email address:<span class="nt"></label></span> <span class="nt"><input</span> <span class="na">type=</span><span class="s">"email"</span> <span class="na">name=</span><span class="s">"email"</span> <span class="na">maxlength=</span><span class="s">"254"</span> <span class="na">required=</span><span class="s">""</span> <span class="na">id=</span><span class="s">"id_email"</span><span class="nt">></span>
<span class="nt"></p></span>
<span class="nt"><p></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"id_name"</span><span class="nt">></span>Name:<span class="nt"></label></span> <span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"name"</span> <span class="na">maxlength=</span><span class="s">"30"</span> <span class="na">id=</span><span class="s">"id_name"</span><span class="nt">></span>
<span class="nt"></p></span>
<span class="nt"><p></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"id_password"</span><span class="nt">></span>Password:<span class="nt"></label></span> <span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"password"</span> <span class="na">maxlength=</span><span class="s">"128"</span> <span class="na">required=</span><span class="s">""</span> <span class="na">id=</span><span class="s">"id_password"</span><span class="nt">></span>
<span class="nt"></p></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>각 필드마다 <code class="language-plaintext highlighter-rouge">p</code> 태그로 둘러싸였고 각 <code class="language-plaintext highlighter-rouge">input</code> 들은 <code class="language-plaintext highlighter-rouge">label</code> 태그와 짝을 이루었습니다. 각 태그들은 관련된 필드의 이름으로 <code class="language-plaintext highlighter-rouge">name</code> 속성과 <code class="language-plaintext highlighter-rouge">id</code> 속성이 설정되어 있습니다. 좀 더 정확하게 <code class="language-plaintext highlighter-rouge">id</code> 는 <code class="language-plaintext highlighter-rouge">'id_' + field.name</code> 으로 되어있습니다.</p>
<p>좀 보기 좋게 하려면 모든 <code class="language-plaintext highlighter-rouge">label</code> 의 너비를 동일하게 하고, 모든 <code class="language-plaintext highlighter-rouge">input</code> 들의 너비를 동일하게 맞추면 좀 더 깔끔해보일 듯 합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
<span class="nt"><style></span>
<span class="nc">.registration</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">360px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.form-action</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{{ form.as_p }}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p>flex로 간단하게 해결했다가 혹시나 <del>구시대의 폐물</del> ie 구버전을 사용하시는 분들이 계실까봐 비교적 새로운 css는 적용하지 않았습니다. 디자인에 관심이 없는 관계로 크롬에 최적화된 디자인으로 예시를 설명합니다. css의 적용과정은 <del>가르쳐드릴 정도의 실력이 안되므로</del> 빠른 진행을 위해 생략합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_02.png" alt="CreateView 기본폼 style 정리 후" /></p>
<p>하앍~ 깔꼼하당.^^. 이제 실제 이메일과 이름, 비밀번호를 입력하고 가입하기 버튼을 누르려고 했는데, 비밀번호가 적나라하게 보이네요ㅠㅠ. 이 부분은 일단 넘어가고 기능이 정상적으로 동작하는 지 버튼을 눌러 확인해 봅니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_03.png" alt="CreateView 가입 오류" /></p>
<p>살짝 깜빡임과 함께 화면이 상단에 메시지가 나타났습니다.저것은 email 주소가 중복된다는 오류메시지인데 제 기억으로는 슈퍼유저계정만 만들었고, <code class="language-plaintext highlighter-rouge">swarfkim@gmail.com</code> 이라는 계정으로 사용자를 생성한 적이 없는데 뭔가 이상합니다. 실제로 db 파일을 확인해보도록 합니다.</p>
<blockquote>
<p>장고 기본폼은 모델폼을 상속받아 생성하는데, 모델폼은 각 필드들이 정상적인 값들인지 검증을 하기도 하고 오류가 있을 경우 <code class="language-plaintext highlighter-rouge">messages</code> 프레임워크를 통해 각 필드에 메시지를 전달합니다. 또한 폼객체를 처리할 때 오류가 발생할 경우에는 오류가 발생한 폼의 필드에 오류메시지를 전달합니다.</p>
</blockquote>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">></span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">user_user</span><span class="p">;</span>
<span class="mi">1</span><span class="o">|</span><span class="n">pbkdf2_sha256</span><span class="err">$</span><span class="mi">120000</span><span class="err">$</span><span class="n">Fpn8scH5jxYJ</span><span class="err">$</span><span class="n">BXnUQBZLQXw6ZWf4oVqlS9U5UMSCYbaYZqleWSNTWgU</span><span class="o">=|</span><span class="mi">2018</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">16</span><span class="p">:</span><span class="mi">49</span><span class="p">:</span><span class="mi">24</span><span class="p">.</span><span class="mi">068214</span><span class="o">|</span><span class="mi">1</span><span class="o">|</span><span class="n">swarf00</span><span class="o">@</span><span class="n">gmail</span><span class="p">.</span><span class="n">com</span><span class="o">||</span><span class="mi">1</span><span class="o">|</span><span class="mi">1</span><span class="o">|</span><span class="mi">2018</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">16</span><span class="p">:</span><span class="mi">45</span><span class="p">:</span><span class="mi">10</span><span class="p">.</span><span class="mi">608484</span>
<span class="mi">2</span><span class="o">|</span><span class="mi">7</span><span class="n">h1515myp455w0d</span><span class="o">||</span><span class="mi">0</span><span class="o">|</span><span class="n">swarfkim</span><span class="o">@</span><span class="n">gmail</span><span class="p">.</span><span class="n">com</span><span class="o">|</span><span class="n">swarf</span><span class="p">.</span><span class="n">kim</span><span class="o">|</span><span class="mi">0</span><span class="o">|</span><span class="mi">1</span><span class="o">|</span><span class="mi">2018</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">06</span> <span class="mi">09</span><span class="p">:</span><span class="mi">30</span><span class="p">:</span><span class="mi">37</span><span class="p">.</span><span class="mi">181559</span>
</code></pre></div></div>
<p>확인해보니 방금 입력한 값이 정상적으로 저장이 되었네요. 오류표시가 잘못된 것 같습니다. 한가지 심각한 문제점이 더 보이는데, 비밀번호가 제가 입력한 그대로(7h1515myp455w0d) 저장이 되어 있습니다. 바로 전에 발견한 문제점인 비밀번호를 화면에 그대로 노출시키는 문제와 입력한 그대로 데이터베이스에 저장하는 두가지의 문제를 종합해 볼 때 장고의 <strong>기본폼은 비밀번호 필드를 일반 CharField와 구분할 수 없다</strong>는 사실을 알 수 있습니다. 어쩔 수 없이 가입폼을 새롭게 정의해야 합니다. <del>OK 계획대로 되고 있어.</del></p>
<p>일단 비밀번호가 그대로 저장되어 있는 민망한 레코드는 삭제하도록 합니다.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">></span> <span class="k">DELETE</span> <span class="k">FROM</span> <span class="n">user_user</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="n">sqlite</span><span class="o">></span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">user_user</span><span class="p">;</span>
<span class="mi">1</span><span class="o">|</span><span class="n">pbkdf2_sha256</span><span class="err">$</span><span class="mi">120000</span><span class="err">$</span><span class="n">Fpn8scH5jxYJ</span><span class="err">$</span><span class="n">BXnUQBZLQXw6ZWf4oVqlS9U5UMSCYbaYZqleWSNTWgU</span><span class="o">=|</span><span class="mi">2018</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">16</span><span class="p">:</span><span class="mi">49</span><span class="p">:</span><span class="mi">24</span><span class="p">.</span><span class="mi">068214</span><span class="o">|</span><span class="mi">1</span><span class="o">|</span><span class="n">swarf00</span><span class="o">@</span><span class="n">gmail</span><span class="p">.</span><span class="n">com</span><span class="o">||</span><span class="mi">1</span><span class="o">|</span><span class="mi">1</span><span class="o">|</span><span class="mi">2018</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">16</span><span class="p">:</span><span class="mi">45</span><span class="p">:</span><span class="mi">10</span><span class="p">.</span><span class="mi">608484</span>
</code></pre></div></div>
<h3 id="회원가입-폼-생성">회원가입 폼 생성</h3>
<p>가입폼을 새롭게 밑바닥부터 만들려면 알아야 할 것들도 많고, 해야 할 것들이 너무나 많습니다. <del>그럴 줄 알고</del> 장고 auth 프레임워크에서 기본적으로 제공하는 <code class="language-plaintext highlighter-rouge">UserCreationForm</code> 을 가져다 사용합니다. 추가로 우리도 <code class="language-plaintext highlighter-rouge">User</code> 모델을 직접 임포트해서 model 변수에 정의했는데 좀 더 유연한 방법으로 가져오도록 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.forms</span> <span class="kn">import</span> <span class="n">UserCreationForm</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.models</span> <span class="kn">import</span> <span class="n">User</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">User</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserCreationForm</span>
</code></pre></div></div>
<p>삭제된 <code class="language-plaintext highlighter-rouge">fields</code> 클래스변수는 장고에서 기본으로 생성되는 모델폼을 사용할 경우만 필요합니다. 모델폼 객체가 자동으로 생성될 때 참조하는 모델의 모든 필드를 폼의 필드로서 생성하지 않고 필요한 필드들만 생성시켜야 하는데 이 때 <strong><code class="language-plaintext highlighter-rouge">fields</code> 라는 클래스변수가 꼭 폼에서 생성시켜야 하는 모델의 필드명</strong> 입니다. 자동생성되는 폼이다보니 뷰에서 전달해주지 않으면 폼에서 생성시켜야 할 필드들이 무엇인지 알 수 없었던 것 입니다. 다시 브라우저에서 <code class="language-plaintext highlighter-rouge">/user/create/</code> 주소로 접속합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_04.png" alt="CreateView UserCreationForm 적용 후" /></p>
<p>지저분합니다ㅠㅠ. 각 필드마다 밑에 무언가를 설명하는 여러가지 문구들이 있는 듯 한데 <del>영어는 까막눈이라</del> 보기가 싫습니다. 이유는 <strong><code class="language-plaintext highlighter-rouge">UserCreationForm</code> 내부에서 폼 생성의 기준이 되는 모델을 <code class="language-plaintext highlighter-rouge">auth</code> 의 모델로 하드코드되어 있기 때문</strong>입니다. <code class="language-plaintext highlighter-rouge">settings.py</code> 에 설정한 <code class="language-plaintext highlighter-rouge">AUTH_USER_MODEL</code> 로 되어 있다면 이런 일이 없겠지만 이 기회에 <code class="language-plaintext highlighter-rouge">UserCreationForm</code> 을 상속받아 새로운 폼을 만들어 볼 기회를 <del>삽질(+1)을 획득하셨습니다.</del> 가져볼 수 있습니다.</p>
<p>우선 폼클래스는 보통 <code class="language-plaintext highlighter-rouge">forms.py</code> 파일에 구현합니다. 새로운 <code class="language-plaintext highlighter-rouge">forms.py</code> 파일을 생성 후 <code class="language-plaintext highlighter-rouge">UserCreationForm</code> 클래스를 상속받아 새로운 폼을 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/forms.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.forms</span> <span class="kn">import</span> <span class="n">UserCreationForm</span>
<span class="k">class</span> <span class="nc">UserRegistrationForm</span><span class="p">(</span><span class="n">UserCreationForm</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">)</span>
</code></pre></div></div>
<p>model 변수를 <code class="language-plaintext highlighter-rouge">get_user_model</code> 이라는 함수를 호출해서 정의했는데, <strong>settings 파일에서 <code class="language-plaintext highlighter-rouge">AUTH_USER_MODEL</code> 이 가리키는 모델을 자동으로 찾아주는 유용한 함수</strong>입니다. <code class="language-plaintext highlighter-rouge">Meta</code> 클래스의 <code class="language-plaintext highlighter-rouge">fields</code> 라는 변수는 정의된 모델에서 폼에 보여줄 필드들을 정의하는 변수입니다. <code class="language-plaintext highlighter-rouge">password</code> 는 자동으로 <code class="language-plaintext highlighter-rouge">UserCreationForm</code> 에서 생성하기 때문에 <code class="language-plaintext highlighter-rouge">email</code> 과 <code class="language-plaintext highlighter-rouge">name</code> 필드만 추가하면 됩니다.</p>
<p>이제 뷰에서도 새로 생성된 폼을 사용하도록 변경합니다. 뷰에서도 마찬가지로 <code class="language-plaintext highlighter-rouge">get_user_model</code> 함수를 이용해서 model 변수를 지정하도록 합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.forms</span> <span class="kn">import</span> <span class="n">UserRegistrationForm</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
</code></pre></div></div>
<p>이제 다시 접속해서 어떻게 달라졌는 지 확인해 봅니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_05.png" alt="CreateView 폼 참조필드 수정" /></p>
<p>여전히 비밀번호에는 지저분하게 텍스트들이 보이는데 <code class="language-plaintext highlighter-rouge">UserCreateForm</code> 의 필드들에 정의되어 있는 help_text 속성이 출력되기 때문입니다. <code class="language-plaintext highlighter-rouge">UserCreationFrom</code> 이 상속받는 <strong><code class="language-plaintext highlighter-rouge">ModelForm</code> 은 <code class="language-plaintext highlighter-rouge">as_p</code>(또는 <code class="language-plaintext highlighter-rouge">as_table</code>, <code class="language-plaintext highlighter-rouge">as_ul</code>) 함수 호출 시 폼객체 자신 또는 폼객체가 참조하는 모델의 필드에 정의된 <code class="language-plaintext highlighter-rouge">help_text</code> 속성을 설명문구로서 친철하게 출력</strong>해줍니다. 이것들을 보이지 않게 하기 위해 템플릿을 수정하도록 하겠습니다. 폼에서 <code class="language-plaintext highlighter-rouge">as_p</code> 함수를 호출하지 않고 각 필드들을 직접 렌더링 하도록 하는 것이 좋겠습니다. 왜냐하면 어떻게 보여야 하느냐는 템플릿에서 담당하는 역할이기 때문입니다. 또한 이렇게 하면 화면을 좀 더 세밀하게 디자인할 수도 있게 됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
<span class="nt"><style></span>
<span class="nc">.registration</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">360px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.registration</span> <span class="nc">.form-actions</span> <span class="o">></span> <span class="nt">button</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% for field in form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"id_{{ field.html_name}}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
{{ field }}
<span class="nt"></div></span>
{% endfor %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p><strong>form 객체를 iterate 시키면 각 필드들이 출력</strong>이 됩니다. field 들을 출력하면 각 필드에 맞는 태그로 렌더링 됩니다. 필드의 레이블도 <code class="language-plaintext highlighter-rouge">field.label_tag</code> 값으로 렌더링 할 수 있으나 이렇게 하면 필드 레이블 뒤에 콜론(:)이 자동으로 붙게되서 어쩔 수 없이 원초적으로 태그를 직접 하드코딩 해줬습니다. 각 필드들의 name 속성은 <code class="language-plaintext highlighter-rouge">field.html_name</code> 으로 접근이 가능하고 레이블은 <code class="language-plaintext highlighter-rouge">field.label</code> 으로 접근할 수 있습니다.</p>
<blockquote>
<p>field는 BoundField의 인스턴스입니다. BoundField의 각 속성들을 알면 템플릿에서 좀 더 편리하게 렌더링하실 수 있습니다.</p>
<ol>
<li><strong>field.id_for_label</strong> - field의 tag에서 사용될 id 값으로 보통 'id_ + field.name'</li>
<li>field.initial - 모델에서의 default 속성의 값</li>
<li>field.is_hidden - hidden 속성이 있다면 True 그렇지 않으면 False</li>
<li><strong>field.errors</strong> - field의 유효성 검증할 때 발견된 오류들</li>
<li>field.html_name - 렌더링될 tag의 name 속성의 값. 즉, 'form.prefix + field.name'로 폼클래스에 prefix 변수가 선언되어 있지 않으면 field.name 과 동일</li>
<li>field.help_text - 도움말의 역할을 하는 텍스트로 form 필드에 해당 속성이 없으면 model 필드에서 참조</li>
<li><strong>field.label</strong> - 모델의 verbose_name과 동일한 데이터로 해당 필드를 사람이 이해하기 쉬게 부르는 호칭</li>
<li>field.label_tag - field.label 을 렌더링한 태그</li>
<li>field.name - field 의 이름. 폼에 선언된 field의 변수명과 동일</li>
<li><strong>field.value</strong> - field에 저장된 값</li>
</ol>
</blockquote>
<p><img src="https://swarf00.github.io/snapshots/createview_form_06.png" alt="CreateView 템플릿 커스터마이징" /></p>
<p>이렇게 해도 거의 완벽한데 label 값이 영어라서 참 많이 애석합니다. <code class="language-plaintext highlighter-rouge">email</code> 과 <code class="language-plaintext highlighter-rouge">name</code> 부분은 모델에서 수정하면 되는데, <code class="language-plaintext highlighter-rouge">password1</code>과 <code class="language-plaintext highlighter-rouge">password2</code>는 <code class="language-plaintext highlighter-rouge">UserCreationForm</code> 에 정의되어 있는 부분이어서 override 해줘야 합니다. 그런데 유심히 모델과 폼의 label에 해당하는 값들을 보시면 <code class="language-plaintext highlighter-rouge">_('msgid')</code> 형식으로 선언되어 있을 겁니다. 예를 들어 모델에서 <code class="language-plaintext highlighter-rouge">email</code> 필드를 보시면 <code class="language-plaintext highlighter-rouge">_('email address')</code>로 선언되어 있습니다. 이 <code class="language-plaintext highlighter-rouge">_</code>라는 함수는 <code class="language-plaintext highlighter-rouge">ugettext_lazy</code> 함수의 별칭(별명) 입니다. 이 함수는 <strong>언어설정에 따라 출력되는 문자열을 변환해</strong>주는 함수입니다. 설정파일의 <code class="language-plaintext highlighter-rouge">LANGUAGE_CODE</code> 만 변경시켜주면 장고에서 미리 번역해둔 문자열들로 치환되어 출력됩니다.</p>
<blockquote>
<p>다국어 설정은 아무런 문자나 자동으로 변환(번역)이 되는 것이 아니라 번역파일에 미리 정의 해놓은 문자들만 변환이 됩니다. msgid에 대응하는 문자들을 언어별로 작성해야 합니다. ./manage.py 유틸리티의 <code class="language-plaintext highlighter-rouge">makemessages</code> 커맨드는 프로젝트내의 <code class="language-plaintext highlighter-rouge">ugettext_lazy</code> 함수의 인자들을 검색 후 언어별로 번역파일을 생성합니다. 번역파일에 해당 msgid 에 대응하는 msgstr 을 정의해주면 번역파일이 생성이 되고, 번역단어가 많고 여러 언어로 설정할 경우 검색하는 시간이 오래 걸려 전체적으로 서비스의 속도가 굉장히 저하됩니다. 그렇기 때문에 장고에서 읽기 편하게 미리 컴파일을 해둬야 하는데 이것이 ./manage.py 유틸리티의 <code class="language-plaintext highlighter-rouge">compilemessages</code> 커맨드입니다. 다국어 설정은 <del>인싸가 되기 위해</del> 중요하지만 여기서 길게 설명하지 않으니 더 자세한 설명은 <a href="https://docs.djangoproject.com/en/2.1/topics/i18n/translation/">공식문서</a>를 참고하시기 바랍니다.</p>
</blockquote>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/settings.py
</span>
<span class="c1"># 생략
</span>
<span class="n">LANGUAGE_CODE</span> <span class="o">=</span> <span class="s">'ko-KR'</span>
<span class="n">TIME_ZONE</span> <span class="o">=</span> <span class="s">'Asia/Seoul'</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<h3 id="한글화국제화-aka-i18n-설정">한글화(국제화 a.k.a. i18n) 설정</h3>
<p>기본언어 설정 후 다시 접속해보면 한글로 정상적으로 출력이 됩니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_07.png" alt="CreateView 템플릿 한글화 후" /></p>
<p>이제 화면은 우리가 원하는대로 출력이 되는 것 같습니다. 이제 실제 가입하기가 정상적으로 작동하는 지 다시 확인해봅니다. 간단하게 인증정보를 입력하여 회원가입을 해봅니다.
어떤 케이스에는 화면만 깜박이고 비밀번호가 지워진 상태로 다시 화면이 나타나고, 또 다른 어떤 케이스는 오류가 발생합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/createview_form_08.png" alt="CreateView 가입실패" /></p>
<p>화면만 깜박이는 케이스는 db파일을 확인해보니 아무것도 저장이 되지 않았고, <code class="language-plaintext highlighter-rouge">ImproperlyConfirue</code> 오류가 발생한 경우에는 데이터가 정상적으로 저장된 것으로 확인됩니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> SELECT <span class="k">*</span> FROM user_user<span class="p">;</span>
1|pbkdf2_sha256<span class="nv">$120000$Fpn8scH5jxYJ$BXnUQBZLQXw6ZWf4oVqlS9U5UMSCYbaYZqleWSNTWgU</span><span class="o">=</span>|2018-12-05 16:49:24.068214|1|swarf00@gmail.com||1|1|2018-12-05 16:45:10.608484
3|pbkdf2_sha256<span class="nv">$120000$ZicY7sWoTpG5$u2TOhjtfiNZrP2XgN9iDgH3D45</span>+O/0oBBZKsNQLfNvU<span class="o">=||</span>0|swarkim@gmail.com|swarf00|0|1|2018-12-06 19:56:33.057128
</code></pre></div></div>
<p>우선 깜박이는 경우부터 원인을 짚어보겠습니다. 데이터베이스에 저장이 되지 않았다는 것은 <strong>폼에서 입력된 값의 유효성 검사를 통과하지 못했다는 의미</strong>인데, 어떤 문제가 있었는지 화면에 출력하지 않아서 알 수가 없는 상황입니다. 각 필드마다 <code class="language-plaintext highlighter-rouge">errors</code> 속성이 있는데 이 속성을 출력하면 해당 필드에서 발생한 문제들이 줄줄이 출력이 됩니다. 템플릿에 <code class="language-plaintext highlighter-rouge">errors</code> 를 출력하도록 수정하겠습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
<span class="c"><!-- 생략 --></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% for field in form %}
{{ field.errors }} <span class="nt"><</span><span class="err">!</span> <span class="na">--</span> <span class="err">필드</span> <span class="err">상단에</span> <span class="err">오류</span> <span class="err">메시지</span> <span class="err">출력</span> <span class="na">--</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
{{ field }}
<span class="nt"></div></span>
{% endfor %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="c"><!-- 생략 --></span>
</code></pre></div></div>
<p>다시 가입을 해봤더니 오류메시지가 잘 나타나는 군요. 오류 메시지가 별로 눈에 띄지 않는데 css변경은 모든 오류를 수정한 후에 하기로 하고 우선 가입하기를 성공시키도록 하겠습니다.</p>
<p><code class="language-plaintext highlighter-rouge">UserCreationForm</code> 에서 password2의 필드에 비밀번호 검증하는 루틴이 추가되어 있는데 내부를 살펴보면 결국 설정파일의 <code class="language-plaintext highlighter-rouge">AUTH_PASSWORD_VALIDATORS</code> 리스트에 정의된 validator들을 모두 통과시키도록 되어 있습니다. 기본적으로 4개의 validator 들이 정의되어 있는데 4가지 모두 통과시켜 <strong>오류가 발생하면 모두 필드객체의 <code class="language-plaintext highlighter-rouge">errors</code> 변수에 오류내용이 추가되고 폼은 데이터를 저장하지 않습니다</strong>. validator가 귀찮다면 <code class="language-plaintext highlighter-rouge">AUTH_PASSWORD_VALIDATORS</code> 에서 해당 항목을 삭제하시고, 더 많은 검증이 필요하다면 더 추가하셔도 상관없습니다. 다른 필드들은 정상적으로 입력하고 비밀번호를 '1'이라고만 입력하고 가입하기를 시도해보면 모든 검증 패턴의 오류내용을 보실 수 있습니다.</p>
<blockquote>
<p>4가지 중 오류 메시지가 발생된 3가지는 쉽게 이해하실 수 있으리라 생각됩니다. 필터에 걸리지 않은 <code class="language-plaintext highlighter-rouge">UserAttributeSimiarityValidator</code> 의 기능에 대해 궁금하지 않으신 분은 이 부분을 건너 뛰셔도 됩니다.</p>
<p><code class="language-plaintext highlighter-rouge">UserAttributeSimiarityValidator</code> 는 사용자 모델의 속성(즉, 필드)와 비교해서 유사한 경우 오류를 발생시키는 유틸리티입니다. 기본으로 비교하는 필드는 정해져 있지만 수정이 가능합니다. <code class="language-plaintext highlighter-rouge">username</code>, <code class="language-plaintext highlighter-rouge">first_name</code>, <code class="language-plaintext highlighter-rouge">last_name</code>, <code class="language-plaintext highlighter-rouge">email</code> 이 네 가지를 비교하는데 <code class="language-plaintext highlighter-rouge">email</code>을 제외하고 나머지 3개의 필드들은 새로운 사용자 모델에서 삭제했었죠. 그러니 <code class="language-plaintext highlighter-rouge">email</code>과 <code class="language-plaintext highlighter-rouge">name</code> 두 가지의 필드를 <code class="language-plaintext highlighter-rouge">user_attributes</code> 이라는 이름의 옵션으로 전달해주면 됩니다. 또한 유사도를 나타내는 <code class="language-plaintext highlighter-rouge">max_similarity</code> 옵션도 있지만 기본값으로 0.7이라는 값이 설정되어 있는데 굳이 변경할 필요가 없어 보입니다. validator에 옵션을 전달하는 방법은 설정파일의 <code class="language-plaintext highlighter-rouge">AUTH_PASSWORD_VALIDATORS</code> 변수에 옵션을 전달해주면 됩니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
<span class="s">'NAME'</span><span class="p">:</span> <span class="s">'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'</span><span class="p">,</span>
<span class="s">'OPTION'</span><span class="p">:</span> <span class="p">{</span><span class="s">'user_attributes'</span><span class="p">:</span> <span class="p">(</span><span class="s">'email'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">)},</span>
<span class="p">},</span>
</code></pre></div> </div>
</blockquote>
<p>그럼 두번째로 <code class="language-plaintext highlighter-rouge">ImproperlyConfirue</code> 오류가 발생하는 원인을 살펴봐야 하는데 오류 메시지를 보니 <strong>redirect 될 URL이 정의되어 있지 않다</strong>고 합니다. 데이터가 정상적으로 저장되는 것으로 보아 회원가입까지는 성공이 되었는데 어디로 이동해야 할 지 몰라서 발생하는 오류로 보여집니다. <code class="language-plaintext highlighter-rouge">CreateView</code> 에서 내부적으로 폼을 처리한 이후 <code class="language-plaintext highlighter-rouge">get_success_url()</code> 함수를 호출해서 이동할 페이지의 주소를 결정하는데 뷰의 클래스변수인 <code class="language-plaintext highlighter-rouge">success_url</code> 을 참조합니다. 그렇다면 <strong>UserRegistrationView에 success_url 클래스변수를 정의</strong>하면 해결될 것 입니다. <del>참 간단하쥬?</del> 장고는 왜 이렇게 복잡하게 설정할 것이 많은 지 모르겠습니다.</p>
<p>어쨌든 회원가입이 성공한 뒤를 생각해보지 않았지만 일단 게시글 목록보기 화면으로 이동하도록 하고 <code class="language-plaintext highlighter-rouge">success_url</code> 에 게시물 목록보기의 주소 <code class="language-plaintext highlighter-rouge">'/article/'</code> 을 설정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># user/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">get_user_model</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">CreateView</span>
<span class="kn">from</span> <span class="nn">user.forms</span> <span class="kn">import</span> <span class="n">UserRegistrationForm</span>
<span class="k">class</span> <span class="nc">UserRegistrationView</span><span class="p">(</span><span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">UserRegistrationForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s">'/article/'</span>
</code></pre></div></div>
<p>다시 한번 정상적인 입력값으로 회원가입을 시도해봅니다. 이번에는 정상적으로 게시글 목록 화면으로 이동했습니다.</p>
<blockquote>
<p>보통 다른 블로그나 책을 보면 <code class="language-plaintext highlighter-rouge">success_url</code> 은 <code class="language-plaintext highlighter-rouge">reverse</code> 또는 <code class="language-plaintext highlighter-rouge">reverse_lazy</code> 함수를 이용해서 세팅하곤 합니다. 이번 예제는 꼭 그럴 필요없다는 것을 보여드리고 싶어서 url을 하드코딩하는 예제를 보여드립니다. <code class="language-plaintext highlighter-rouge">reverse</code> 또는 <code class="language-plaintext highlighter-rouge">reverse_lazy</code> 함수는 <strong>여러 개의 앱과 다양한 3rd 파티 앱들을 사용할 때 유용</strong>합니다. 모든 앱들의 url들을 다 외울 수 없으니 앱이름과 라우팅 이름만 가지고 편리하게 사용할 수 있는 기능입니다. 라우팅 이름은 보통 핸들러 함수(또는 뷰클래스) 이름과 유사하게 정의하기 때문에 쉽게 기억할 수 있습니다. 반대로 앱이 복잡하지 않고 url이 명확하다면 굳이 라우팅 이름을 정의할 필요도 없고 굳이 함수를 거칠 필요 없습니다. 장고의 기능은 너무나 방대해서 한가지의 기능을 구현하기 위해 다양한 방법들을 사용할 수 있습니다. <code class="language-plaintext highlighter-rouge">why</code>를 많이 알면 알수록 장고를 좀 더 <code class="language-plaintext highlighter-rouge">효율적</code>이고 <code class="language-plaintext highlighter-rouge">효과적</code>으로 사용할 수 있을 것 입니다.</p>
</blockquote>
<p>마지막으로 아까 약속했던 각 필드들의 errors출력에 style을 입히는 작업을 하겠습니다. 뾰롱뾰롱~뾰로롱~뿅~</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- user/template/user/user_model.html --></span>
{% extends 'base.html' %}
{% load i18n %}
{% block title %}<span class="nt"><title></span>회원 가입<span class="nt"></title></span>{% endblock %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
<span class="nt"><style></span>
<span class="nc">.registration</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">360px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span> <span class="p">{</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">left</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.control-label</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.registration</span> <span class="nc">.form-actions</span> <span class="o">></span> <span class="nt">button</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
{% endblock css %}
{% block content %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel panel-default registration"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-heading"</span><span class="nt">></span>
가입하기
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"panel-body"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
{% csrf_token %}
{% for field in form %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group {% if field.errors|length > 0 %}has-error{%endif %}"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ field.label }}<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"{{ field.html_name }}"</span> <span class="na">id=</span><span class="s">"{{ field.id_for_lable }}"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">type=</span><span class="s">"{{ field.field.widget.input_type }}"</span> <span class="na">value=</span><span class="s">"{{ field.value|default_if_none:'' }}"</span><span class="nt">></span>
{% for error in field.errors %}
<span class="nt"><label</span> <span class="na">class=</span><span class="s">"control-label"</span> <span class="na">for=</span><span class="s">"{{ field.id_for_label }}"</span><span class="nt">></span>{{ error }}<span class="nt"></label></span>
{% endfor %}
<span class="nt"></div></span>
{% endfor %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary btn-large"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>가입하기<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
{% endblock content %}
</code></pre></div></div>
<p><img src="https://swarf00.github.io/snapshots/createview_form_09.png" alt="CreateView 에러 CSS 적용 후" /></p>
<p>참 쉽죠? 이제 에러가 발생하면 에러가 발생한 필드 아래에 메시지가 빨간 표시되고, 해당 필드도 빨간색으로 표시되도록 변경했습니다. label이 필드의 상단으로 이동되도록 변경했습니다.</p>
<p>300라인 정도면 가입하기 내용을 다 설명할 수 있을 것이라 생각했는데 예상보다 2배가 분량이 되었습니다. 더 자세히 설명하다간 가입하기 본질을 놓치고 장고 프레임워크만 <del>코딱지 파듯이</del> 파다가 코피 퐝 쏟을 것 같습니다. <code class="language-plaintext highlighter-rouge">Responsive Web Design</code>도 적용하려 했으나 중요해 보이지 않는 것들은 분량이 적을 것 같은 로그아웃 편으로 다 몰아버릴 예정입니다. 다음 편인 로그인을 구현할 때도 좀 새로운 기능들과 속성들을 사용할 건데 얼마나 오래 걸릴 지 모르겠으나 잠시만 안녕 ~</p>
<blockquote>
<p>잔소리가 적은 개발자가 좋은 개발자다.</p>
<p>swarf00, 그래 나 설명충이다...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크에서 기본 제공하는 auth 프레임워크를 이용하여 사용자가입을 구현하는 방법을 설명합니다. 또한 모델폼을 이용하여 쉽게 템플릿을 구현하는 방법을 알아봅니다.모델 만들기2018-11-23T00:00:00+09:002018-11-23T00:00:00+09:00https://swarf00.github.io/2018/11/23/build-model<h2 id="1-모델-설계">1. 모델 설계</h2>
<p>이제부터 진짜 게시판 앱을 만들 것입니다.</p>
<p>게시판 데이터를 저장하기 위해서 가장 먼저 모델부터 설계해야 합니다. 설계에 앞서 사용자들이 게시판을 어떻게 사용할 지 가정하여 나열합니다.</p>
<blockquote>
<ol>
<li>게시판(게시글 목록)에는 게시글들의 목록이 나열됩니다.</li>
<li>게시글들은 제목과 작성자 표시됩니다.</li>
<li>게시글을 (클릭해서) 들어가면 게시글 상세화면으로 이동하고 제목, 내용, 작성일이 출력합니다.</li>
<li>게시글 상세화면에서 수정하기 버튼을 누르면 수정하는 화면으로 이동합니다.</li>
<li>게시글 수정화면에서 저장하기 버튼을 누르면 수정된 내용이 저장되고 게시판으로 이동합니다.</li>
<li>게시글 수정화면에서 삭제하기 버튼을 누르면 게시글이 삭제되고 게시판으로 이동합니다.</li>
<li>게시판에서 새글쓰기 버튼을 누르면 새로운 게시글을 입력할 수 있는 화면이 출력됩니다.</li>
<li>게시글을 작성하고 저장하기 버튼을 누르면 수정된 내용이 저장되고 게시판으로 이동합니다.</li>
</ol>
</blockquote>
<p>게시글(Article)이라는 모델을 만들고 <code class="language-plaintext highlighter-rouge">제목(title)</code>, <code class="language-plaintext highlighter-rouge">내용(content)</code>, <code class="language-plaintext highlighter-rouge">작성자(author)</code>, <code class="language-plaintext highlighter-rouge">작성일(created_at)</code> 속성을 정의해주면 될 것 같습니다. 그러면 이 게시글들을 모두 불러와서 게시글 목록에서 보여주고, 게시글 상세화면과 수정화면에서는 해당하는 하나의 게시글만 불러와서 보여주면 될 것 같습니다.</p>
<h2 id="2-모델-생성">2. 모델 생성</h2>
<h3 id="모델-정의">모델 정의</h3>
<p>모델은 <code class="language-plaintext highlighter-rouge">Article</code>이라는 이름으로 models.py에 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs.models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">auther</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">models</code> 모듈은 장고의 모델과 관련된 모든 기능이 구현된 모듈입니다.</p>
<p>우리가 구현할 모델 클래스는 <code class="language-plaintext highlighter-rouge">models.Model</code> 클래스를 상속받습니다. 모델이 데이터베이스와 연결하는 모든 기능들이 이미 구현되어 있습니다.</p>
<blockquote>
<p>모델의 속성은 장고ORM에서 제공하는 기본 필드로 구현합니다. 예제에서 다루는 필드 이외에 더 많은 종류의 필드와 옵션들이 있습니다.</p>
<ul>
<li>CharField - sql에서의 <code class="language-plaintext highlighter-rouge">varchar</code> 자료형으로 변환됩니다. 글자수 제한있는 문자열 데이터를 저장합니다.</li>
<li>TextField - sql에서의 <code class="language-plaintext highlighter-rouge">text</code> 자료형으로 변환됩니다. 길이수 제한없는 문자열 데이터를 저장합니다.</li>
<li>DateTimeField - sql에서의 <code class="language-plaintext highlighter-rouge">datetime</code> 자료형으로 변환됩니다. 날짜와 시간이 utc 시간으로 저장됩니다.</li>
</ul>
</blockquote>
<h3 id="데이터베이스-설정">데이터베이스 설정</h3>
<p>정의된 모델이 실제 데이터베이스에 저장되도록 하기 위한 설정을 하기 위해 프로젝트의 settings.py 파일을 수정합니다. 우리는 <code class="language-plaintext highlighter-rouge">sqlite3</code>를 이용해 데이터를 저장할 것이므로 설정을 그대로 유지합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># settings.py
</span>
<span class="c1"># 생략
</span><span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s">'ENGINE'</span><span class="p">:</span> <span class="s">'django.db.backends.sqlite3'</span><span class="p">,</span> <span class="c1"># 데이터베이스는 sqlite3 사용
</span> <span class="s">'NAME'</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s">'db.sqlite3'</span><span class="p">),</span> <span class="c1"># 루트 디렉토리에 db.sqlite3 파일로 데이터 저장
</span> <span class="p">}</span>
<span class="p">}</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p>데이터베이스가 sqlite3로 설정된 상태이기 때문에 <code class="language-plaintext highlighter-rouge">Article</code> 모델을 sqlite3의 테이블로 생성시켜주기만 하면 됩니다.</p>
<p>manage.py 에서 제공하는 <code class="language-plaintext highlighter-rouge">makemigrations</code> 커맨드를 이용하면 migrations 디렉토리의 migration 파일들을 비교해서 어떻게 변경됐는 지 확인 후 새로운 <strong>migration 파일을 생성하여 변경된 내용을 기록</strong>합니다.</p>
<p><code class="language-plaintext highlighter-rouge">Article</code> 모델은 새로 생성한 모델이기 때문에 비교없이 새로운 migration 파일을 생성합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py makemigrations
No changes detected
</code></pre></div></div>
<p>...... 어떻게 된 일인지 변경된 게 없다고 출력이 되며 bbs/migrations 디렉토리에 생성된 파일이 없습니다.</p>
<h3 id="앱-등록하기">앱 등록하기</h3>
<p>이유는 app을 생성하는 <code class="language-plaintext highlighter-rouge">startapp</code> 커맨드는 앱디렉토리와 관련 파일들을 생성할 뿐 앱관련 설정은 프로젝트에 추가하지 않기 때문입니다. settings.py 파일에 <strong>bbs 앱을 등록</strong>해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># settings.py
</span>
<span class="c1"># 생략
</span><span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span>
<span class="s">'bbs'</span><span class="p">,</span> <span class="c1"># 등록할 앱이름
</span> <span class="s">'django.contrib.admin'</span><span class="p">,</span> <span class="c1"># 장고 어드민 앱
</span> <span class="s">'django.contrib.auth'</span><span class="p">,</span> <span class="c1"># 장고 인증 앱
</span> <span class="s">'django.contrib.contenttypes'</span><span class="p">,</span> <span class="c1"># 다양한 종류의 모델데이터를 관리할 수 있게 도와주는 앱.
</span> <span class="s">'django.contrib.sessions'</span><span class="p">,</span> <span class="c1"># 클라이언트 정보를 세션에서 관리하도록 하는 프레임워크
</span> <span class="s">'django.contrib.messages'</span><span class="p">,</span> <span class="c1"># 컨트롤러에서 발생한 정보를 뷰에서 쉽게 접근하도록 연결하는 프레임워크
</span> <span class="s">'django.contrib.staticfiles'</span><span class="p">,</span> <span class="c1"># html, css, js 파일등의 정적파일 들을 관리해주는 프레임워크
</span><span class="p">]</span>
<span class="c1"># 생략
</span>
<span class="n">TIME_ZONE</span> <span class="o">=</span> <span class="s">'Asia/Seoul'</span> <span class="c1"># 시간대를 서울로 변경
</span>
<span class="n">USE_TZ</span> <span class="o">=</span> <span class="bp">False</span> <span class="c1"># 기본 시간대(UTC)를 사용하지 않겠다고 변경
# 생략
</span></code></pre></div></div>
<p>bbs를 제외한 다른 앱들은 아직까지 필요하지 않지만 장고에서 기본적으로 필요한 프레임워크들입니다. 모두</p>
<p>이제 다시 makemigrations 커맨드로 <code class="language-plaintext highlighter-rouge">migration</code> 파일을 생성합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">test</span><span class="o">-</span><span class="n">venv</span><span class="o">-</span><span class="mi">36</span><span class="p">)</span> <span class="err">$</span> <span class="o">./</span><span class="n">manage</span><span class="o">.</span><span class="n">py</span> <span class="n">makemigrations</span>
<span class="n">Migrations</span> <span class="k">for</span> <span class="s">'bbs'</span><span class="p">:</span>
<span class="n">bbs</span><span class="o">/</span><span class="n">migrations</span><span class="o">/</span><span class="mi">0001</span><span class="n">_initial</span><span class="o">.</span><span class="n">py</span>
<span class="o">-</span> <span class="n">Create</span> <span class="n">model</span> <span class="n">Article</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">bbs/migraions/0001_initial.py</code> 파일이 생성되었습니다. migration 파일의 간단한 내용이 표시되어 있습니다. 상세한 내용은 migration 파일에 기록되어 있습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/migrations/0001_initial.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span><span class="p">,</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">initial</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">CreateModel</span><span class="p">(</span>
<span class="n">name</span><span class="o">=</span><span class="s">'Article'</span><span class="p">,</span>
<span class="n">fields</span><span class="o">=</span><span class="p">[</span>
<span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="n">models</span><span class="o">.</span><span class="n">AutoField</span><span class="p">(</span><span class="n">auto_created</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">serialize</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">verbose_name</span><span class="o">=</span><span class="s">'ID'</span><span class="p">)),</span>
<span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">verbose_name</span><span class="o">=</span><span class="s">'제목'</span><span class="p">)),</span>
<span class="p">(</span><span class="s">'content'</span><span class="p">,</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">verbose_name</span><span class="o">=</span><span class="s">'내용'</span><span class="p">)),</span>
<span class="p">(</span><span class="s">'auther'</span><span class="p">,</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">verbose_name</span><span class="o">=</span><span class="s">'작성자'</span><span class="p">)),</span>
<span class="p">(</span><span class="s">'created_at'</span><span class="p">,</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">verbose_name</span><span class="o">=</span><span class="s">'작성일'</span><span class="p">)),</span>
<span class="p">],</span>
<span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">operation</code> 리스트를 보면 <code class="language-plaintext highlighter-rouge">migrations.CreateModel</code> 이 추가되어 있는데 여기에 생성될 테이블의 내용이 기록되어 있습니다.</p>
<p><code class="language-plaintext highlighter-rouge">id</code>라는 필드가 자동으로 들어가 있습니다. <code class="language-plaintext highlighter-rouge">primary key</code>를 설정하지 않으면 자동으로 <code class="language-plaintext highlighter-rouge">id</code>라는 필드를 생성하고 <code class="language-plaintext highlighter-rouge">pk</code>로 설정을 합니다.
field를 자세히 보니 author가 아닌 auther로 입력되었습니다. 직접 타이핑을 하지 않고 복붙(copy & paste)를 하신 <del>게으름뱅이</del>분들은 동일한 오타가 있을 겁니다. <strong>migration 파일은 가급적 수정 및 삭제를 해서는 안됩니다</strong>. migration 파일을 수동으로 변경할 경우 이후의 스키마 동기화 작업에 문제가 생길 수 있습니다.</p>
<h3 id="모델-수정">모델 수정</h3>
<p>모델 파일에서 오타난 부분을 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs.models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="c1"># auther => author 로 수정
</span> <span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">makemigrations</code> 커맨드를 다시 실행합니다. 이 때 이름을 변경할 것인지를 묻는데 <code class="language-plaintext highlighter-rouge">y</code>(yes)를 입력합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py makemigrations
Did you rename article.auther to article.author <span class="o">(</span>a CharField<span class="o">)</span>? <span class="o">[</span>y/N] y
Migrations <span class="k">for</span> <span class="s1">'bbs'</span>:
bbs/migrations/0002_auto_20181121_1545.py
- Rename field auther on article to author
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">bbs/migrations/0002_auto_20181121_1545.py</code> 파일이 생성되고 수정된 내용의 요약이 출력됩니다. 새로 생성된 migration 파일을 다시 확인합니다.</p>
<blockquote>
<p>파일명에 생성 날짜와 시간(0002_auto_yyyymmdd_HHMM.py)이 기록되기 때문에 완전히 동일한 파일명이 아닙니다.</p>
</blockquote>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/migrations/0002_auto_20181121_1545.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span>
<span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s">'bbs'</span><span class="p">,</span> <span class="s">'0001_initial'</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RenameField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s">'article'</span><span class="p">,</span>
<span class="n">old_name</span><span class="o">=</span><span class="s">'auther'</span><span class="p">,</span>
<span class="n">new_name</span><span class="o">=</span><span class="s">'author'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p><del>아직 <code class="language-plaintext highlighter-rouge">migrate</code>를 하지 않은 상태이기 때문에 migration에 익숙하신 분들은 삭제하거나 모델파일과 마이그레이션 파일의 오타부분을 수정하셔도 됩니다.</del></p>
<p>manage.py의 <code class="language-plaintext highlighter-rouge">migrate</code> 커맨드를 이용하면 모든 migration 파일을 추적 후 아직 적용하지 않은 migration을 적용시켜 줍니다. 변경된 migration 파일이 없다면 아무런 작업도 하지 않을 것입니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, bbs, contenttypes
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 bbs.0001_initial... OK
Applying bbs.0002_auto_20181121_1545... OK
Applying sessions.0001_initial... OK
</code></pre></div></div>
<p>bbs 뿐만 아니라 등록된 모든 앱들의 모델들도 migration 됐습니다.</p>
<h2 id="3-장고-쉘에서-테스트">3. 장고 쉘에서 테스트</h2>
<p>manage.py의 shell 커맨드를 실행하면 파이썬 쉘이 실행되고, 현재 장고가 실행할 때 가져오는 환경변수들을 동일하게 가져옵니다. 이 쉘을 통해 간단하게 모델이 정상적으로 동작하는 지 확인할 수 있습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py shell
<span class="o">>>></span>
</code></pre></div></div>
<blockquote>
<p>ipython을 설치하시면 좀더 강력한 파이썬 쉘을 경험할 수 있습니다. 문법에 맞게 코드에 색깔을 입혀주기도 하고, tab 키를 누르면 자동완성도 제공합니다. 설치는 pip로 간단하게 할 수 있습니다.</p>
<p>(test-venv-36) $ pip install ipython</p>
</blockquote>
<h3 id="데이터-저장">데이터 저장</h3>
<p>파이썬 쉘 프롬프트가 출력되면 Article 모델을 불러와서 CRUD 명령을 실행해볼 수 있습니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 데이터 저장
</span><span class="o">>>></span> <span class="kn">from</span> <span class="nn">bbs.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="o">>>></span> <span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">'How to create a article'</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s">'1. import Article class</span><span class="se">\n</span><span class="s">2. invoke </span><span class="se">\'</span><span class="s">create</span><span class="se">\'</span><span class="s"> method of Article</span><span class="se">\'</span><span class="s">s manager.'</span><span class="p">,</span> <span class="n">author</span><span class="o">=</span><span class="s">'swarf00'</span><span class="p">,</span> <span class="n">created_at</span><span class="o">=</span><span class="s">'2018-11-22'</span><span class="p">)</span>
<span class="o">>>></span> <span class="k">print</span><span class="p">(</span><span class="n">article</span><span class="p">)</span>
<span class="n">Article</span> <span class="nb">object</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="o">>>></span> <span class="k">print</span><span class="p">(</span><span class="s">'{} title: {}, content: {}, author: {} created_at: {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">article</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="n">article</span><span class="o">.</span><span class="n">title</span><span class="p">,</span> <span class="n">article</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="n">article</span><span class="o">.</span><span class="n">author</span><span class="p">,</span> <span class="n">article</span><span class="o">.</span><span class="n">created_at</span><span class="p">))</span>
<span class="n">title</span><span class="p">:</span> <span class="n">How</span> <span class="n">to</span> <span class="n">create</span> <span class="n">a</span> <span class="n">article</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="mf">1.</span> <span class="kn">import</span> <span class="nn">Article</span> <span class="k">class</span>
<span class="err">2. </span><span class="nc">invoke</span> <span class="s">'create'</span> <span class="n">method</span> <span class="n">of</span> <span class="n">Article</span><span class="s">'s manager., author: swarf00 created_at: 2018-11-22 01:15:21.135315</span><span class="err">
</span></code></pre></div></div>
<p>Article 모델을 관리하는 objects 매니저는 Article 클래스가 상속받은 models.Model 클래스에 기본 내장되어 있습니다 기본 매니저를 통해 CRUD를 실행할 수 있는데 데이터 생성은 create 메소드를 이용합니다.</p>
<p>create 함수는 positional argument도 지원하지만 인자가 여러개이거나 한눈에 보기 어려운 경우 keyword arguemnt로 전달하는 것이 보기 좋습니다.</p>
<h3 id="데이터-표시-형식-변경">데이터 표시 형식 변경</h3>
<p>article은 생성된 데이터 객체입니다. print 함수로 출력해보면 <code class="language-plaintext highlighter-rouge">Article object (1)</code>이라고 출력됩니다.
<code class="language-plaintext highlighter-rouge">string formatter</code>를 이용해 저장된 데이터를 확인해보니 created_at값이 입력한 값과 다르게 출력이 되었습니다. <code class="language-plaintext highlighter-rouge">created_at</code> 필드에 <code class="language-plaintext highlighter-rouge">auto_now_add</code>를 <code class="language-plaintext highlighter-rouge">True</code>로 설정하면 <code class="language-plaintext highlighter-rouge">create</code>메소드가 호출될 때 항상 현재시간(01시 15분 실화냐?)이 기록됩니다.
앞으로 자주 디버깅해야 할 오브젝트인데 매번 <code class="language-plaintext highlighter-rouge">string formatter</code>를 이용하는게 불편합니다. 그래서 모델에 <code class="language-plaintext highlighter-rouge">__str__</code>메소들르 오버라이드 해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'타이틀'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'[{}] {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="데이터-검색-수정-저장">데이터 검색, 수정, 저장</h3>
<p>field가 변경된 것이 아니고 메소드만 변경된 것이니 migration을 해줄 필요는 없습니다.
다시 <code class="language-plaintext highlighter-rouge">shell</code> 커맨드를 실행하셔서 아까 전에 생성한 데이터를 검색해서 <code class="language-plaintext highlighter-rouge">created_at</code> 값을 변경해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">>>></span> <span class="kn">from</span> <span class="nn">bbs.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="o">>>></span> <span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># id가 1인 Article 데이터 검색. 없거나 2개 이상일 경우 에러발생
</span><span class="o">>>></span> <span class="k">print</span><span class="p">(</span><span class="n">article</span><span class="p">)</span>
<span class="o"><</span><span class="n">Article</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="n">How</span> <span class="n">to</span> <span class="n">create</span> <span class="n">a</span> <span class="n">article</span><span class="o">></span>
<span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">created_at</span> <span class="o">=</span> <span class="s">'2018-11-22 01:15'</span>
<span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span> <span class="c1"># 변경된 값 저장. `time formatter('%Y-%m-%d %H:%M')` 형식의 문자열은 DateTimeField에서 자동으로 시간 데이터로 변환해줍니다.
</span><span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">created_at</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">'</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d'</span><span class="p">)</span> <span class="c1"># 변경된 created_at 값을 time fomatter를 이용해 출력해보지만 에러발생
</span><span class="o">---------------------------------------------------------------------------</span>
<span class="nb">AttributeError</span> <span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">)</span>
<span class="o"><</span><span class="n">ipython</span><span class="o">-</span><span class="nb">input</span><span class="o">-</span><span class="mi">16</span><span class="o">-</span><span class="mi">560946</span><span class="n">d1936a</span><span class="o">></span> <span class="ow">in</span> <span class="o"><</span><span class="n">module</span><span class="o">></span>
<span class="o">----></span> <span class="mi">1</span> <span class="n">article</span><span class="o">.</span><span class="n">created_at</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">'</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d'</span><span class="p">)</span>
<span class="nb">AttributeError</span><span class="p">:</span> <span class="s">'str'</span> <span class="nb">object</span> <span class="n">has</span> <span class="n">no</span> <span class="n">attribute</span> <span class="s">'strftime'</span>
<span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">refresh_from_db</span><span class="p">()</span> <span class="c1"># db로 부터 새로 검색
</span><span class="o">>>></span> <span class="n">article</span><span class="o">.</span><span class="n">created_at</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">'</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d'</span><span class="p">)</span> <span class="c1"># 정상출력
</span><span class="s">'2018-11-22'</span>
</code></pre></div></div>
<p>모델의 데이터 검색은 기본 매니저의 <code class="language-plaintext highlighter-rouge">get</code> 메소드로 검색할 수 있습니다. sql의 <code class="language-plaintext highlighter-rouge">where</code>에 해당하는 내용을 keyword argument로 전달하면 됩니다. 만일 데이터가 아무것도 발견되지 않거나 2개 이상이 발견될 경우 에러가 발생합니다. get 메소드 대신 <code class="language-plaintext highlighter-rouge">QuerySet</code>이라는 이터레이터를 반환하는 <code class="language-plaintext highlighter-rouge">filter</code> 메소드를 이용하면 레코드가 0개 이거나 2개 이상이더라도 오류가 발생하지 않습니다.</p>
<p><code class="language-plaintext highlighter-rouge">QuerySet</code> 객체는 이터레이터의 모든 기능을 사용할 수도 있습니다. 특별히 첫번째 데이터와 마지막 데이터의 경우 <code class="language-plaintext highlighter-rouge">first</code>, <code class="language-plaintext highlighter-rouge">last</code> 메소드로도 접근이 가능하고 <code class="language-plaintext highlighter-rouge">QuerySet</code>이 검색결과가 0개인 경우 <code class="language-plaintext highlighter-rouge">None</code>을 반환합니다..</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">>>></span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">author</span><span class="o">=</span><span class="s">'swarf00'</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span> <span class="c1"># author='swarf00'인 첫번째 레코드 검색
</span><span class="o">>>></span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">author</span><span class="o">=</span><span class="s">'swarf00'</span><span class="p">)</span><span class="o">.</span><span class="n">last</span><span class="p">()</span> <span class="c1"># author='swarf00'인 마지막 레코드 검색
</span><span class="o">>>></span> <span class="n">Article</span><span class="o">.</span><span class="n">first</span><span class="p">()</span> <span class="c1"># Article 테이블에서 조건없이 첫번째 레코드 검색
</span><span class="o">>>></span> <span class="n">Article</span><span class="o">.</span><span class="n">last</span><span class="p">()</span> <span class="c1"># Article 테이블에서 조건없이 마지막 레코드 검색
</span></code></pre></div></div>
<h2 id="4-admin-사이트">4. Admin 사이트</h2>
<h3 id="관리자-등록하기">관리자 등록하기</h3>
<p>manage.py의 <code class="language-plaintext highlighter-rouge">shell</code> 커맨드도 database를 관리하기에 편리하지만 장고에서는 웹기반의 <strong>admin 사이트</strong>를 제공합니다. <strong>장고ORM</strong>이 익숙하지 않거나 불편하실 경우 <strong>admin 사이트</strong>를 이용하세요.</p>
<p>admin 사이트는 데이터베이스 내의 각종 데이터를 접근 및 수정이 가능하기 때문에 인증기능이 필수로 구현되어 있습니다.
manage.py의 <code class="language-plaintext highlighter-rouge">createsuperuser</code> 커맨드를 이용하면 간단하게 <code class="language-plaintext highlighter-rouge">superuser</code> 권한의 사용자를 생성할 수 있습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>./manage.py createsuperuser
Username <span class="o">(</span>leave blank to use <span class="s1">'swarf00'</span><span class="o">)</span>:
Email address:
Password:
Password <span class="o">(</span>again<span class="o">)</span>:
Superuser created successfully.
</code></pre></div></div>
<p>4가지를 적절하게 입력하면 admin 사이트로 <code class="language-plaintext highlighter-rouge">username</code> 과 <code class="language-plaintext highlighter-rouge">password</code> 를 이용해서 접속할 수 있습니다.</p>
<h3 id="admin-사이트-접속">Admin 사이트 접속</h3>
<p>장고를 재시작하고 <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/admin</code> 으로 접속합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_login.png" alt="로그인 화면" class="border rounded shadow" /></p>
<p>superuser 계정으로 로그인하시면 현재 admin 사이트에 등록된 앱과 모델들이 나타납니다.
장고2부터는 admin 사이트도 ResponsiveWebDesign이 지원됩니다. pc화면에서는 좀 더 시원한 화면을 볼 수 있습니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_home.png" alt="로그인 후 화면" class="border rounded shadow" /></p>
<p>로그인을 하고 admin 사이트의 웰컴페이지가 나타납니다. admin 사이트에 등록된 모든 모델들이 앱별로 보여줍니다. 아직까지 bbs의 모델은 admin 사이트에 등록하지 않은 상태여서 보이지 않습니다. 기본 등록된 모델을 빠르게 보겠습니다.</p>
<p>AUTHENTICATION AND AUTHORIZATION(인증과 권한) 앱에 <code class="language-plaintext highlighter-rouge">Groups</code>, <code class="language-plaintext highlighter-rouge">Users</code> 두 모델이 등록되어 있는 것이 확인됩니다. Recent actions라고 가장 최근 작업에 대한 기록도 출력됩니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_aa_users.png" alt="User 모델 화면" class="border rounded shadow" /></p>
<p><code class="language-plaintext highlighter-rouge">Groups</code> 에는 아무런 레코드도 없고, <code class="language-plaintext highlighter-rouge">Users</code>를 보니 아까 생성한 <code class="language-plaintext highlighter-rouge">superuser</code>의 정보가 보입니다. 다행히 비밀번호는 보이지 않습니다.(사실 비밀번호는 해시로 인코딩 되어 있어 안전합니다.)</p>
<h3 id="admin-사이트에-등록">Admin 사이트에 등록</h3>
<p><code class="language-plaintext highlighter-rouge">Article</code> 모델도 admin 사이트에 등록하기 위해 <code class="language-plaintext highlighter-rouge">bbs/admin.py</code>를 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/admin.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">Article</span><span class="p">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">admin.site.register</code> 함수를 이용하면 간단하게 <code class="language-plaintext highlighter-rouge">Article</code> 모델을 admin 사이트에 등록할 수 있습니다. 장고를 재시작하고 다시 admin 사이트에 접속합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_home_article.png" alt="Article 모델 추가화면" class="border rounded shadow" /></p>
<p>BBS 앱에 <code class="language-plaintext highlighter-rouge">Article</code> 모델이 추가된 걸 확인 할 수 있습니다. <code class="language-plaintext highlighter-rouge">Article</code> 모델을 클릭하면 레코드리스트가 보입니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_article.png" alt="Article 리스트 변경전" class="border rounded shadow" /></p>
<h3 id="admin-사이트-커스터마이징">Admin 사이트 커스터마이징</h3>
<p>하나의 데이터만 생성시켰었기 때문에 하나의 레코드만 보입니다. 데이터가 문자열형태로 표현되는데 보기가 불편합니다. admin 사이트는 손쉽게 출력화면을 커스터마이징할 수 있는 방법을 제공합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/admin.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="o">@</span><span class="n">admin</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">Article</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleAdmin</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
<span class="n">list_display</span> <span class="o">=</span> <span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="s">'title'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">,</span> <span class="s">'date_created'</span><span class="p">)</span> <span class="c1"># date_created는 아래 정의한 메소드
</span> <span class="n">list_display_links</span> <span class="o">=</span> <span class="p">(</span><span class="s">'id'</span><span class="p">,</span> <span class="s">'title'</span><span class="p">)</span> <span class="c1"># 상세페이지로 이동할 수 있는 필드 리스트
</span>
<span class="k">def</span> <span class="nf">date_created</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span> <span class="c1"># create_at 필드의 출력형식을 변경해주는 메소드
</span> <span class="k">return</span> <span class="n">obj</span><span class="o">.</span><span class="n">created_at</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"</span><span class="si">%</span><span class="s">Y-</span><span class="si">%</span><span class="s">m-</span><span class="si">%</span><span class="s">d"</span><span class="p">)</span>
<span class="n">date_created</span><span class="o">.</span><span class="n">admin_order_field</span> <span class="o">=</span> <span class="s">'created_at'</span> <span class="c1"># date_created 컬럼 제목을 클릭시 실제 어떤 데이터를 기준으로 정렬할 지 결정
</span> <span class="n">date_created</span><span class="o">.</span><span class="n">short_description</span> <span class="o">=</span> <span class="s">'작성일'</span> <span class="c1"># date_created 컬럼 제목에 보일 텍스트
</span></code></pre></div></div>
<p><img src="https://swarf00.github.io/snapshots/admin_article_new.png" alt="Article 리스트 변경후" class="border rounded shadow" /></p>
<p>이전과 다르게 ArticleAdmin이란 클래스를 생성 후 @admin.register(Article) 데코레이터로 wrapping을 했습니다. admin.site.register 함수를 사용하는 것보다 번거롭지만 상세한 설정이 가능합니다.</p>
<p><code class="language-plaintext highlighter-rouge">list_display</code> 속성에 필드명을 문자열의 튜플로 저장해주면 admin 사이트의 리스트에서 튜플의 순서대로 테이블의 컬럼을 생성해줍니다. <code class="language-plaintext highlighter-rouge">created_at</code> 필드를 그대로 사용하면 장고의 기본 포멧대로 출력되기 때문에 <code class="language-plaintext highlighter-rouge">date_created</code> 함수를 생성해서 <strong>출력형식을 변경</strong>해줬습니다. <code class="language-plaintext highlighter-rouge">date_created</code> 함수는 admin 사이트에서 필드처럼 사용됩니다. 하지만 모델에 존재하는 필드가 아니기 때문에 <strong>컬럼에 대한 속성을 새로 지정</strong>해줘야 합니다.</p>
<p><code class="language-plaintext highlighter-rouge">list_display_links</code> 는 레코드의 상세내용을 확인하거나 수정하기 위해 이동할 수 있는 링크를 어떤 필드에 제공할 것인가에 대한 속성입니다. 기본은 첫번째 필드인 <code class="language-plaintext highlighter-rouge">id</code>에만 링크가 걸려있지만 좀 더 클릭하기 편하게 <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">title</code> 에 클릭할 수 있도록 설정했습니다.</p>
<p>변경한 대로 작성일 컬럼이 추가되었고 출력형식은 <code class="language-plaintext highlighter-rouge">%Y-%m-%d</code> 형식으로 출력됩니다. 또한 제목을 클릭하면 작성일을 기준으로 정렬을 할 수 있습니다. 제목을 클릭하여 상세 페이지로 이동할 수 있습니다.</p>
<p><img src="https://swarf00.github.io/snapshots/admin_article_detail.png" alt="Article 리스트 변경후" class="border rounded shadow" /></p>
<p>상세 페이지에서 작성일이 보이지 않는데 <code class="language-plaintext highlighter-rouge">DateTimeField</code>의 <code class="language-plaintext highlighter-rouge">auto_now_add</code> 속성이 <code class="language-plaintext highlighter-rouge">True</code>로 설정이 되면 <code class="language-plaintext highlighter-rouge">editable</code> 속성이 자동으로 <code class="language-plaintext highlighter-rouge">False</code>가 설정됩니다. <code class="language-plaintext highlighter-rouge">editable</code> 속성을 <code class="language-plaintext highlighter-rouge">True</code>로 변경해 주면 디테일 화면에서 확인 및 수정이 가능해집니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/models.py
</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'제목'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">126</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="s">'내용'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s">'작성자'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="s">'작성일'</span><span class="p">,</span> <span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">created_at</span><span class="o">.</span><span class="n">editable</span> <span class="o">=</span> <span class="bp">True</span> <span class="c1"># created의 editable 속성에 True를 설정했습니다.
</span>
<span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s">'[{}] {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="nb">id</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</code></pre></div></div>
<p><img src="https://swarf00.github.io/snapshots/admin_article_detail_new.png" alt="Article 상세 페이지" class="border rounded shadow" /></p>
<blockquote>
<p>모델만들기에 약간의 시간을 허비했지만 admin 사이트를 더 많은 방법으로 커스터마이징 하는 것이 이번 튜토리얼의 목표에 대해서 불필요하므로 임시로 덮어 두기로 결정하였다.</p>
<p>– swarf00, 모델만들기에 대한 간단한 예제에서...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net게시판이라는 앱의 기능을 미리 정의하고 모델 설계하는 방법을 설명합니다. 설계를 바탕으로 장고(Django) 웹프레임워크의 ORM을 통해 모델을 정의하고 활용하는 방법을 설명합니다.템플릿 만들기2018-11-23T00:00:00+09:002018-11-23T00:00:00+09:00https://swarf00.github.io/2018/11/23/build-template<h2 id="1-템플릿-설계">1. 템플릿 설계</h2>
<p>템플릿도 설계가 필요합니다. 많이 봐오신 웹사이트들을 보면 웹사이트는 대부분의 페이지들이 항상 일정한 헤드, 메뉴, 푸터 등을 표시하는 것을 볼 수 있습니다. 현재 이 웹사이트도 헤드(상단)과 사이드바(좌측), 푸터 등이 항상 일정하게 나타나고 있습니다. 단지 현재의 페이지에 따라서 특정 텍스트들이 강조되어 있습니다. 현재 사이드바의 Template 만들기가 강조되어 있습니다. <strong>템플릿을 기능별로 구분한다면 재활용성이 높고, 개발할 때 단순함을 더할 수 있습니다.</strong></p>
<p>게시판의 모든 화면은 크게 두가지로 나눌 것 입니다. <strong>기본구조와 실제 화면내용</strong>으로 구분됩니다. <code class="language-plaintext highlighter-rouge">기본구조</code>는 html의 공통적인 head와 body에서 화면내용이 삽입될 <code class="language-plaintext highlighter-rouge">틀</code>입니다. <code class="language-plaintext highlighter-rouge">화면내용</code>은 뷰마다 제공하는 데이터를 사용자가 알아볼 수 있게 표현하는 부분입니다. 말로만 하면 설명을 이해하기 어려우니 일단 따라합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"ko"</span><span class="nt">></span>
<span class="nt"><head></span>
{% block title %} <span class="c"><!-- 페이지별 타이틀 공간 --></span>
<span class="nt"><title></span>bbs - minitutorial<span class="nt"></title></span>
{% endblock title %}
{% block meta %} <span class="c"><!-- 페이지별 메타 데이터 공간 --></span>
{% endblock meta %}
{% block scripts %} <span class="c"><!-- 페이지별 스크립트 공간 --></span>
{% endblock scripts %}
{% block css %} <span class="c"><!-- 페이지별 css --></span>
{% endblock css %}
<span class="nt"></head></span>
<span class="nt"><body></span>
{% block content %}
view: {{ view }} <span class="c"><!-- ctx['view'] --></span>
<span class="nt"><br></span>
data: {{ data }} <span class="c"><!-- ctx['data'] --></span>
{% endblock content %}
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<h3 id="템플릿-태그와-템플릿-변수">템플릿 태그와 템플릿 변수</h3>
<p>우선 기본구조를 먼저 생성합니다. 템플릿은 단순한 텍스트(MarkUp) 파일입니다. 템플릿엔진은 내부가 html인지 csv인지 뭔지 아무런 관심도 없고 <del>여러분처럼</del> 이해하지도 못합니다. 템플릿엔진이 알아볼 수 있는건 딱 두가지 템플릿태그와 템플릿변수 입니다.</p>
<p>각 block 태그들은 페이지마다 끼워넣거나 대체해넣을 수 있는 공간입니다. 해당 블록에 <strong><code class="language-plaintext highlighter-rouge">title block</code> 처럼 block의 시작과 종료(endblock)사이에 값(문자열)을 넣으면 해당하는 값의 기본값이 설정</strong>됩니다. 어떤 템플릿이든 이 템플릿(base.html)을 상속받으면 해당 데이터를 덮어쓰거나 추가할 수 있습니다.</p>
<blockquote>
<p>템플릿태그와 템플릿 변수</p>
<ul>
<li>템플릿태그는 for-in 반복문, if-elif-else 조건문 등의 템플릿 엔진이 이해하는 몇가지 기능들을 수행합니다. {% %} 으로 표현하고 for-in, if-elif-else 처럼 처리해야 할 텍스트가 2줄이상이 될 수 있는 경우 여는 태그({% %})와 닫는 태그({% %})로 이루어집니다. 여는 태그와 닫는 태그 사이의 텍스트를 제어하는 것입니다. for-in 태그는 endfor 태그로 반드시 종료가 되어야 합니다.</li>
<li>템플릿변수는 뷰로부터 전달받은 객체의 값을 인용할 때 사용합니다. {{ }}로 표현하며 표현할 변수의 값이 딕셔너리일 경우에도 getattr 연산자(.)으로 key에 접근할 수 있습니다. 리스트나 튜플일 때도 인덱스를 대괄호가 아니라 .으로 접근할 수 있습니다. 템플릿엔진은 일부 문법에 대해 파이썬 문법보다 좀더 유연함을 제공합니다.</li>
</ul>
</blockquote>
<p>템플릿은 수정을 해도 장고를 재시작할 필요가 없습니다. 이것은 매 요청 때마다 템플릿 렌더링을 한다는 것을 의미하고, 성능적으로 그리 좋지 않다는 것을 의미합니다. <del>여기선 안 가르져주니</del>나중에 캐시도 공부해보시기 바랍니다.</p>
<p>이 상태에서 뷰 테스트했을 때와 같이 접속해보면 동일한 결과를 볼 수 있습니다.</p>
<ul>
<li>http://127.0.0.1/article/</li>
<li>http://127.0.0.1/article/create/</li>
<li>http://127.0.0.1/article/10/</li>
<li>http://127.0.0.1/article/11/update/</li>
</ul>
<h2 id="2-리스트-템플릿-구현">2. 리스트 템플릿 구현</h2>
<h3 id="템플릿의-상속">템플릿의 상속</h3>
<p>정상적으로 출력이 된다면 각 화면별로 템플릿을 작성합니다. 반드시 base.html 템플릿을 상속받도록 구현하는 것이 좋습니다. 그렇게 해야 중복된 코드를 줄이고 실수로 공통 코드를 빠뜨리지 않을 수 있습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block content %}
view: {{ view }}
<span class="nt"><br></span>
data: {{ data }}
{% endblock content %}
</code></pre></div></div>
<p>article_list.html 템플릿은 extends의 인자인 'base.html' 파일을 상속받습니다. <strong>템플릿에서 상속이란 기본 뼈대를 부모 템플릿으로 두고 각 block을 오버라이드 한다는 의미</strong>입니다. python의 클래스 상속과 마찬가지로 부모템플릿의 내용을 인용하고 싶다면 <code class="language-plaintext highlighter-rouge">{{ block.super }}</code> 변수를 사용하면 됩니다.</p>
<p>ArticleListView의 템플릿이 base.html에서 article_list.html로 변경되었으니 뷰의 template_name 클래스변수를 변경합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_list.html'</span> <span class="c1"># 뷰 전용 템플릿 생성.
</span> <span class="n">queryset</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
</code></pre></div></div>
<blockquote>
<p>템플릿 파일명을 article_list.html로 지은 이유가 있습니다. 모델을 기반으로 하는 ListView나 DetailView등은 클래스 변수 model을 정의할 경우 자동으로 모델명(소문자) + '_list.html' 또는 '_detail.html'로 템플릿파일을 자동으로 생성합니다. 파일명을 이런식으로 작명한다면 나중에 더 복잡한 제네릭뷰를 사용할 때 편리하고 오류를 줄일 수 있습니다.<del>뿌듯해. 간만에 꿀팁 지렸다.^^</del></p>
</blockquote>
<h3 id="템플릿-반복문">템플릿 반복문</h3>
<p>리스트 템플릿은 0개 이상의 데이터를 표현해야 합니다. 0개일 경우를 따로 구현하진 않을 예정이지만 1개이상인 경우 테이블형태로 표현되도록 할 예정입니다. 카드형태로 <del>구현하는 것은 숙제입니다.</del>구현하실 분들은 따로 해보시기 바랍니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block content %}
<span class="nt"><table></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>게시글번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>3<span class="nt"></td><td></span>제목3<span class="nt"></td><td></span>작성자<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>2<span class="nt"></td><td></span>제목2<span class="nt"></td><td></span>작성자<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>1<span class="nt"></td><td></span>제목1<span class="nt"></td><td></span>작성자<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
{% endblock content %}
</code></pre></div></div>
<p>이런식으로 테이블로 표현할 것입니다. 아직은 실제 데이터를 넣지 않고 가짜데이터로 틀만 만들어봤습니다. 우선 <code class="language-plaintext highlighter-rouge">table</code> 태그를 간단히 살펴보겠습니다. 들여쓰기로 구분하여 보시면 좋습니다.</p>
<p><code class="language-plaintext highlighter-rouge">thead</code>와 <code class="language-plaintext highlighter-rouge">tbody</code>로 나뉩니다. <code class="language-plaintext highlighter-rouge">thead</code>는 칼럼의 제목들이 표시될 것입니다. <code class="language-plaintext highlighter-rouge">thread</code> 안에는 <code class="language-plaintext highlighter-rouge">th</code>태그들이 있는데 각 태그들은 칼럼들의 제목이 표시됩니다.
<code class="language-plaintext highlighter-rouge">tbody</code>는 여러개의 <code class="language-plaintext highlighter-rouge">tr</code>로 구성됩니다. <code class="language-plaintext highlighter-rouge">tr</code>은 데이터의 갯수만큼 출력됩니다. 위의 코드에서는 3개의 <code class="language-plaintext highlighter-rouge">tr</code>이 있는 걸 보니 데이터의 총갯수가 3개입니다. 각 <code class="language-plaintext highlighter-rouge">tr</code>태그에는 각각 3개의 <code class="language-plaintext highlighter-rouge">td</code>태그들이 있습니다. 이 태그들 안에 데이터의 적절한 속성값을 넣어주면 됩니다.</p>
<p>위 예를 보면 <code class="language-plaintext highlighter-rouge">tr</code>태그 단위로 하나의 데이터라는 것을 알 수 있습니다. 즉 <strong><code class="language-plaintext highlighter-rouge">for 루프</code>가 한 사이클이 돌 때마다 <code class="language-plaintext highlighter-rouge">tr</code>태그를 생성</strong>해 주면 됩니다. 물론 <code class="language-plaintext highlighter-rouge">tr</code> 태그 내부에 있는 <code class="language-plaintext highlighter-rouge">td</code>의 데이터도 채워서 생성해야 합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block content %}
<span class="nt"><table></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
{% for article in articles %} <span class="nt"><</span><span class="err">!</span> <span class="na">--</span> <span class="na">for</span> <span class="na">tag</span> <span class="err">시작</span> <span class="na">--</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>3<span class="nt"></td><td></span>제목3<span class="nt"></td><td></span>작성자<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %} <span class="nt"><</span><span class="err">!</span> <span class="na">--</span> <span class="na">for</span> <span class="na">tag</span> <span class="err">종료</span> <span class="na">--</span><span class="nt">></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
{% endblock content %}
</code></pre></div></div>
<p>한번 더 가짜데이터로 출력을 해봤습니다. 이번에 달라진 점은 for 태그가 사용되었다는 것입니다. python의 for-in 루프와 닮았습니다. <strong>다른 점은 {% %}로 감싸 있다는 것이고 {% endfor %}로 for-in루프의 블럭의 끝을 표시</strong>했다는 것입니다. for 태그는 반복문을 실행하되 {% for ~ in ~ %} 에서부터 {% endfor %} 사이의 텍스트를 출력해줍니다.</p>
<p>실제로 테스트하기 위해 뷰의 ctx를 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_list.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'articles'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>템플릿의 <code class="language-plaintext highlighter-rouge">title</code> 태그로 페이지 제목을 알 수 있으니 ctx의 view 값을 제거했습니다. <code class="language-plaintext highlighter-rouge">data</code>라는 이름으로 모호했던 이름을 템플릿에서 사용하는 <code class="language-plaintext highlighter-rouge">articles</code>로 변경합니다. 현재 저의 데이터베이스에는 2개의 레코드가 저장된 상태라서 <code class="language-plaintext highlighter-rouge">articles.count()</code>는 2입니다.</p>
<p><img src="https://swarf00.github.io/snapshots/result_articlelist_02.png" alt="ArticleList for 태그 적용" class="border rounded shadow" /></p>
<p>게시글이 총 2개이기 때문에 <code class="language-plaintext highlighter-rouge">tr</code> 태그가 2번 반복해서 출력이 되었습니다. 그럼 마지막으로 <code class="language-plaintext highlighter-rouge">td</code>의 값을 실제 값으로 채워넣으면 됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block content %}
<span class="nt"><table></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
{% for article in articles %}
<span class="nt"><tr></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td><td></span>{{ article.title }}<span class="nt"></td><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %}
<span class="nt"></tbody></span>
<span class="nt"></table></span>
{% endblock content %}
</code></pre></div></div>
<p>템플릿 변수를 사용하면 특정 값으로 치환을 할 수 있습니다. <code class="language-plaintext highlighter-rouge">for 루프</code>에서 선언한 변수 article의 값을 템플릿변수에서 접근하는데 각각 <code class="language-plaintext highlighter-rouge">pk</code>, <code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">author</code> 속성값으로 치환합니다. <code class="language-plaintext highlighter-rouge">pk</code>는 전에 설명한 대로 <code class="language-plaintext highlighter-rouge">primarykey</code>로 설정된 값 즉, id가 반환됩니다.</p>
<p><img src="https://swarf00.github.io/snapshots/result_articlelist_03.png" alt="ArticleList 템플릿변수 적용" class="border rounded shadow" /></p>
<p><a href="http://bootstrapk.com/css/#tables">bootstrap</a>을 이용해서 디자인을 좀 입혀보겠습니다. <a href="http://bootstrapk.com/">bootstrap</a>의 한글 메뉴얼도 있으니 참고해보시면 더 좋은 기능들을 확인할 수 있습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block css %} <span class="c"><!-- bootstrap CSS --></span>
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-hover table-responsive"</span><span class="nt">></span> <span class="c"><!-- hover, responsive --></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
{% for article in articles %}
<span class="nt"><tr></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td><td></span>{{ article.title }}<span class="nt"></td><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %}
<span class="nt"></tbody></span>
<span class="nt"></table></span>
{% endblock content %}
</code></pre></div></div>
<p>부트스트랩의 자바스크립트는 아직까지 사용할 일이 없으니 추가하지 않습니다. 부트스트랩의 table은 css파일만 가져오면 됩니다. 공개 cdn으로부터 무료로 다운로드해서 사용하실 수 있습니다. <code class="language-plaintext highlighter-rouge">table</code> 태그에 <code class="language-plaintext highlighter-rouge">.table</code> <code class="language-plaintext highlighter-rouge">.table-hover</code> <code class="language-plaintext highlighter-rouge">.table-responsive</code> 클래스를 추가합니다. .table은 부트스트랩의 테이블 디자인을 사용한다는 의미이고, <code class="language-plaintext highlighter-rouge">.table-hover</code>는 각 줄(<code class="language-plaintext highlighter-rouge">tr</code>)에 마우스를 올리면 색깔이 강조되도록 해주는 디자인입니다. <code class="language-plaintext highlighter-rouge">.table-responsive</code>는 모바일처럼 폭이 좁은 화면에서도 깨짐없이 보일 수 있게 해주는 기능입니다. 현재까지는 모바일에서 부자연스럽지 않지만 제목이 굉장히 길거나 작성자 이름이 굉장히 길 경우 또는 칼럼수가 늘어날 경우 좀 더 모바일 친화적으로 보여지게 됩니다.</p>
<h3 id="다른-페이지로-링크">다른 페이지로 링크</h3>
<p>상세페이지와 새게시글 작성 페이지로 이동하는 링크를 추가하면 일단 완료됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_list.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 목록<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
<span class="nt"><style </span><span class="na">type=</span><span class="s">"text/css"</span><span class="nt">></span>
<span class="nt">tbody</span> <span class="o">></span> <span class="nt">tr</span> <span class="p">{</span><span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;}</span>
<span class="nt"></style></span>
{% endblock css %}
{% block content %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-hover table-responsive"</span><span class="nt">></span>
<span class="nt"><thead></span>
<span class="nt"><th></span>번호<span class="nt"></th><th></span>제목<span class="nt"></th><th></span>작성자<span class="nt"></th></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
{% for article in articles %}
<span class="nt"><tr</span> <span class="na">onclick=</span><span class="s">"location.href='/article/{{ article.pk }}/'"</span><span class="nt">></span> <span class="c"><!-- 테이블 행 click 시 url 이동 --></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td><td></span>{{ article.title }}<span class="nt"></td><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %}
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="c"><!-- 버튼 click 시 url 이동 --></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/article/create/"</span><span class="nt">><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"button"</span><span class="nt">></span>새 게시글 작성<span class="nt"></button></a></span>
{% endblock content %}
</code></pre></div></div>
<p><img src="https://swarf00.github.io/snapshots/result_articlelist_04.png" alt="ArticleList 최종화면" class="border rounded shadow" /></p>
<p>테이블의 행을 클릭하면 그 행의 <code class="language-plaintext highlighter-rouge">pk</code>를 따라 이동하도록 했습니다. <code class="language-plaintext highlighter-rouge">tr</code>, <code class="language-plaintext highlighter-rouge">td</code> 태그에서는 <code class="language-plaintext highlighter-rouge">a</code> 태그가 적용되지 않아서 <code class="language-plaintext highlighter-rouge">tr</code>태그에 <code class="language-plaintext highlighter-rouge">onclick</code> 이벤트를 등록했습니다. 태그의 <code class="language-plaintext highlighter-rouge">onclick</code> 속성값을 정의하면 해당 태그를 클릭했을 때 정의된 값이 실행됩니다.
새 게시글 작성 버튼은 무조건 <code class="language-plaintext highlighter-rouge">/article/create/</code>로 이동하도록 했습니다. 게시글 목록 템플릿의 디자인은 여기서 멈춥니다.<del>더 이상 설명하면 디자이너들 밥줄 끊깁니다. 갑자기 뭐래...</del></p>
<h2 id="3-게시물-상세보기-템플릿-구현">3. 게시물 상세보기 템플릿 구현</h2>
<p>게시물 상세보기는 게시물 목록 화면보다 간단합니다. 모든 내용을 다 출력해주면 됩니다. 별 내용이 없으니 아예 수정하기 버튼까지 만들면 좋겠습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_detail.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 상세 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-striped table-bordered"</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>번호<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>제목<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.title }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>내용<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.content }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성자<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성자<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.created_at }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/article/{{ article.pk }}/update/"</span><span class="nt">><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"button"</span><span class="nt">></span>게시글 수정<span class="nt"></button></a></span>
{% endblock content %}
</code></pre></div></div>
<p>게시글 목록 화면과 같이 뷰도 수정해줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="k">class</span> <span class="nc">ArticleDetailView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_detail.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid pk'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">article</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>게시글 목록과는 다르게 오직 하나의 데이터만 템플릿에 전달하기 때문에 게시글 객체 이름을 article이라고 정의했습니다. table은 리스트와는 다르게 한 행에 한 속성씩 출력했습니다. 테이블에 마우스 클릭이 필요없으니 hover효과를 빼고 가시성을 높이는 <code class="language-plaintext highlighter-rouge">.table-striped</code>와 <code class="language-plaintext highlighter-rouge">.table-bordered</code> 클래스를 추가했습니다.</p>
<p>게시글 수정 버튼을 클릭하면 해당 게시물의 업데이트 화면으로 이동할 수 있게 추가했습니다.</p>
<p>게시글 목록에서 아무 행이나 클릭해서 상세페이지로 이동해 봅니다. 제대로 <del>복붙</del>작성했다면 테이블이 나타날 겁니다.</p>
<p><img src="https://swarf00.github.io/snapshots/result_articledetail_01.png" alt="ArticleDetail 템플릿 구현 후" class="border rounded shadow" /></p>
<h3 id="템플릿-필터">템플릿 필터</h3>
<p>정상적으로 출력된 듯 합니다. <del>구라고</del> 유심히 살펴보면 두 가지가 불편해 보입니다.</p>
<ol>
<li>내용의 데이터가 한줄로 출력되었는데, 제가 입력할 때는 줄바꿈이 있었습니다. 즉 <code class="language-plaintext highlighter-rouge">'\n'</code> 문자가 html에서는 적용이 안됩니다.</li>
<li>작성일이 일반 한국사람이 보기에 적합하지 않습니다. 제가 익숙한 <code class="language-plaintext highlighter-rouge">yyyy-mm-dd HH:MM</code> 형식으로 출력되면 좋겠습니다.</li>
</ol>
<p>이럴 줄 알았다는 듯이 장고는 필터라는 기능을 제공하고 있습니다. 이미 장고에서 제공하는 수많은 필터가 있으니 알맞게 가져다 사용만 하면 됩니다. 필터는 템플릿변수 안에서 파이프(<code class="language-plaintext highlighter-rouge">|</code>)로 연결하여 값을 변경하는 함수를 말합니다. <code class="language-plaintext highlighter-rouge">linebreaksbr</code>이라는 필터를 사용하면 필터링할 문자열에서 모든 <code class="language-plaintext highlighter-rouge">\n</code>문자를 <code class="language-plaintext highlighter-rouge"><br></code>태그로 변환해주는 기능을 합니다.</p>
<p>날짜시간의 포멧을 변경하는 필터도 역시 존재합니다. <code class="language-plaintext highlighter-rouge">date</code>이라는 필터인데 이 필터는 인자를 넘겨줄 수도 있습니다. 인자를 넘겨주지 않으면 기본포멧으로 출력되는데 원하는데로 나온다는 보장이 없습니다. PHP의 시간포멧과 비슷한데 <code class="language-plaintext highlighter-rouge">"Y-m-d H:i"</code>이라고 인자를 넘겨주면 익숙한 형태의 날짜와 시간이 출력됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_detail.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 상세 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-striped table-bordered"</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>번호<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>제목<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.title }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>내용<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.content | linebreaksbr }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성자<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.author }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성일<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.created_at | date:"Y-m-d H:i" }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/article/{{ article.pk }}/update/"</span><span class="nt">><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"button"</span><span class="nt">></span>게시글 수정<span class="nt"></button></a></span>
{% endblock content %}
</code></pre></div></div>
<blockquote>
<p>장고에 내장된 필터가 수십여가지가 있는데 많은 것들이 알아두면 좋습니다. <del>저도 다는 몰라서</del> 모두 설명할 수 없으니 <a href="https://docs.djangoproject.com/en/2.1/ref/templates/builtins/">참고 문서</a>를 꼭 한번 정독하시기 바랍니다.</p>
</blockquote>
<h2 id="4-게시물-업데이트-템플릿-구현">4. 게시물 업데이트 템플릿 구현</h2>
<p>게시물 상세화면을 조금 수정해서 템플릿을 구현할 것입니다. 게시물 업데이트 화면은 번호와 작성일은 수정할 수 없고 제목, 내용, 작성자만 변경할 수 있게 할 것입니다. 각 항목들은 서버에 저장되어 있는 값들을 기본값으로 채워넣은 상태로 보여줍니다. 그래야 사용자가 변경하기가 쉬울테니까요.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_update.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 상세 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
<span class="c"><!-- form --></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"/article/{{ article.pk }}/update/"</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">class=</span><span class="s">"form-horizontal"</span><span class="nt">></span>
{% csrf_token %} <span class="c"><!-- csrftoken 태그 --></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"action"</span> <span class="na">value=</span><span class="s">"update"</span><span class="nt">></span> <span class="c"><!-- action --></span>
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-striped table-bordered"</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>번호<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>제목<span class="nt"></th></span> <span class="c"><!-- 제목 입력 --></span>
<span class="nt"><td><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"title"</span> <span class="na">value=</span><span class="s">"{{ article.title }}"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>내용<span class="nt"></th></span> <span class="c"><!-- 내용 입력 --></span>
<span class="nt"><td><textarea</span> <span class="na">rows=</span><span class="s">"10"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"content"</span><span class="nt">></span>{{ article.content }}<span class="nt"></textarea></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성자<span class="nt"></th></span> <span class="c"><!-- 작성자 입력 --></span>
<span class="nt"><td><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"author"</span> <span class="na">value=</span><span class="s">"{{ article.author }}"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성일<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.created_at | date:"Y-m-d H:i" }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>게시글 저장<span class="nt"></button></span>
<span class="nt"></form></span>
{% endblock content %}
</code></pre></div></div>
<p>테이블과 버튼을 form 태그로 감싸서 테이블 안의 input, textarea의 데이터들을 전송할 수 있게 했습니다. form 태그부터 속성을 살펴보면, action은 update 액선의 url을 지정했고, method는 post를 지정했습니다. class를 form-horizontal로 지정했는데 html 엘리먼트가 수평으로 잘 정리되도록 하는 bootstrap 속성입니다.</p>
<p>그 아래를 보면 csrf_token 이라는 태그가 있습니다. 이 태그는 <code class="language-plaintext highlighter-rouge"><input type="hidden" name="csrfmiddlewaretoken" value="kjxqvcTIDJ......w2RT9HMHhdF"></code> 식으로 자동으로 태그를 만들어 줍니다. 장고는 이 post 요청은 csrfmiddlewaretoken이라는 값이 있어야만 정상적인 요청으로 인식합니다. 템플릿엔진은 csrf_token 태그를 만나면 자동으로 csrf verification 프레임워크에서 생성한 csrfmiddlewaretoken의 값을 이용해서 html 태그로 변환해줍니다. 그리고 post요청했을 때 장고의 미들웨어에서 이 값을 검증하고 비정상일 경우 오류를 반환합니다. 템플릿에 <code class="language-plaintext highlighter-rouge">{% csrf_token %}</code> 토큰만 넣으면 된다는 것!! 너무 간단해서 잊어버릴 수 있습니다.</p>
<p>그 아래에는 <code class="language-plaintext highlighter-rouge">hidden</code> 타입의 <code class="language-plaintext highlighter-rouge">input</code> 태그를 추가했습니다. <code class="language-plaintext highlighter-rouge">action</code>이라는 이름에 <code class="language-plaintext highlighter-rouge">update</code>라는 값을 지정했습니다. <code class="language-plaintext highlighter-rouge">hidden</code> 타입은 사용자에게는 보이지 않는 <code class="language-plaintext highlighter-rouge">input</code> 태그입니다. action이라는 값을 사용자에게 보여주지도 않고 볼 수 없으니 변경할 수도 없도록 한 것입니다. 물론 사용자가 html에 대한 지식이 있다면 action 값을 확인하거나 수정할 수도 있겠지만 딱히 문제될 내용이 아니기 때문에 hidden타입으로 두는 것으로도 문제가 없습니다.</p>
<p>입력받을 3가지 값이 있는데 1줄로 입력받아도 된다면 input, 2줄 이상으로 입력받아야 한다면 <code class="language-plaintext highlighter-rouge">textarea</code>를 사용합니다. <strong><code class="language-plaintext highlighter-rouge">textarea</code>는 엔터키를 줄바꿈으로 인식</strong>합니다. textarea는 데이터가 매우 길 수 있기 때문에 value라는 속성 대신 여는 태그와 닫는 태그 사이에 작성하도록 되어 있습니다.</p>
<p>마지막으로 버튼은 둘러싸고 있던 a 태그를 제거하고 type을 submit으로 변경했습니다. submit 타입의 버튼은 클릭시 해당 버튼을 둘러싸고 있는 가장 가까운 폼을 서버에 전송합니다. 물론 form 태그 내부에 있는 모든 데이터들을 가지고 전송이 됩니다. 각 태그들의 이름이 key가 되고 value가 값이 되어 서버에 전송이 됩니다. <code class="language-plaintext highlighter-rouge">'key1=value1&key2=value2'</code> 형태로 전송됩니다. 이렇게 전달된 문자열 데이터는 장고의 미들웨어에서 자동으로 딕셔너리로 변환 후 request.POST 객체에 저장이 됩니다.</p>
<p>뷰의 기능은 이미 구현해 둔 상태인데 이전에 추가했던 데코레이터를 삭제해기만 하면 됩니다. <strong>csrf_exempt 데코레이터는 테스트용으로만 사용</strong>하고 가급적 사용하지 않아야 합니다.</p>
<blockquote>
<p>csrf verification은 이용자가 원치 않는 post요청을 하는 것을 막기 위한 보안 프레임워크입니다. csrf 공격방법은 <a href="https://namu.wiki/w/CSRF">나무위키</a>에 잘 정리되어 있으니 참고해서 이해하는 것이 좋습니다. 장고의 보안 프레임워크는 만능이 아니고 날이 갈수록 진보된 공격방식이 개발되기 때문에 공격 매카니즘을 이해하는 것이 필요합니다.</p>
</blockquote>
<p><img src="https://swarf00.github.io/snapshots/result_articleupdate_01.png" alt="ArticleUpdate 템플릿 구현 후" class="border rounded shadow" /></p>
<p>정상적으로 출력이 됩니다. 그런데 내용을 수정하고 게시글 수정 버튼을 눌렀는데 살짝 깜박임이 보이지만 저장이 잘 되었는 지 의심이 됩니다. 여러번 반복해도 내 눈을 의심하게 될 뿐 저장되었다는 확신이 없습니다. 에러가 발생한 경우도 그냥 에러화면으로 이동이 되는 것이 마음에 걸렸는데 이번 기회에 메시지창을 만들어서 사용자요청이 어떻게 처리되었는 지 알려주는 것이 좋을 것 같습니다.</p>
<p>장고의 <code class="language-plaintext highlighter-rouge">messages</code> 프레임워크를 사용할 때가 왔습니다. <code class="language-plaintext highlighter-rouge">messages</code> 프레임워크는 아무 때나 메시지 내용을 기록하면 템플릿에서 데이터를 출력할 때까지 임시로 데이터를 저장해두는 프레임워크입니다. <strong>로그처럼 레벨이 있기 때문에</strong> 오류인지, 단순한 정보인지 구분하기가 좋습니다.</p>
<p>먼저 뷰에서 <code class="language-plaintext highlighter-rouge">messages</code> 프레임워크에 메시지를 입력하도록 수정합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">messages</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_update.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s">'게시글이 저장되었습니다.'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">pk</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid pk'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">article</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'action'</span><span class="p">)</span>
<span class="n">post_data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">)}</span>
<span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">post_data</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">post_data</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'{} 값이 존재하지 않습니다.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">key</span><span class="p">),</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span> <span class="c1"># error 레벨로 메시지 저장
</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">messages</span><span class="o">.</span><span class="n">get_messages</span><span class="p">(</span><span class="n">request</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="c1"># 메시지가 있다면 아무것도 처리하지 않음
</span> <span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'create'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="o">**</span><span class="n">post_data</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_message</span><span class="p">)</span> <span class="c1"># success 레벨로 메시지 저장
</span> <span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">post_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_message</span><span class="p">)</span> <span class="c1"># success 레벨로 메시지 저장
</span> <span class="k">else</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'알 수 없는 요청입니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span> <span class="c1"># error 레벨로 메시지 저장
</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span> <span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span> <span class="k">else</span> <span class="bp">None</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>messages 프레임워크는 뷰에서 사용하는 방법이 간단합니다. messsages 모듈의 <code class="language-plaintext highlighter-rouge">debug</code>, <code class="language-plaintext highlighter-rouge">info</code>, <code class="language-plaintext highlighter-rouge">success</code>, <code class="language-plaintext highlighter-rouge">warning</code>, <code class="language-plaintext highlighter-rouge">error</code> 5가지 함수 중 하나를 선택해서 request 객체와 저장할 메시지를 전달하면 됩니다. 성공시 success, 오류시 error 함수를 호출했습니다. 템플릿에서 level에 따라 구분되게 표시할 수 있습니다.</p>
<p><code class="language-plaintext highlighter-rouge">messages.get_messages(request)</code> 함수는 현재까지 저장된 메시지들을 반환합니다. 저장된 메시지들이 1개 이상이라면 현재의 코드에서는 반드시 오류가 발생했다는 것이기 때문에 액션로직을 실행하지 않도록 했습니다. article 변수는 액션로직 안에서 정의하기 때문에 만약 오류가 발생하면 action이 <code class="language-plaintext highlighter-rouge">'update'</code>인 경우 article을 검색해오고 <code class="language-plaintext highlighter-rouge">'create'</code>인 경우는 None을 저장하도록 했습니다. <strong>if ~ else 문법을 이용해서 3항 연산자처럼 표현한 식</strong>을 익혀두시면 유용하게 사용하실 수 있습니다.</p>
<blockquote>
<p>log레벨과 사용법이 비슷합니다. 몇가지 레벨이 없지만 웹프레임워크로서 충분합니다. 강제적인 규칙은 아니지만 로그레벨은 그 의미대로 사용하는 것이 좋습니다. 결국 사람이 보기 위한 것이기 때문에 반드시 레벨로 메시지의 속성을 표현해셔야 합니다.</p>
<p>그 외 <code class="language-plaintext highlighter-rouge">커스텀 레벨</code>과 <code class="language-plaintext highlighter-rouge">extra_tags</code>를 이용하는 방법이 있는데 <a href="https://docs.djangoproject.com/ko/2.1/ref/contrib/messages/#creating-custom-message-levels">장고문서</a>가 어렵지 않으니 간단히 살펴보면 쉽게 따라하실 수 있습니다.</p>
</blockquote>
<p>장고에서 템플릿으로 <code class="language-plaintext highlighter-rouge">messages</code>라는 객체로 저장된 메시지들이 전달됩니다. 뷰에서 context로 전달하지 않아도 됩니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_update.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 상세 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
{% if messages %} <span class="c"><!-- message 프레임워크 --></span>
{% for message in messages %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"alert alert-{{ message.tags }} alert-dismissible"</span> <span class="na">role=</span><span class="s">"alert"</span><span class="nt">></span>
{{ message }}
<span class="nt"></div></span>
{% endfor %}
{% endif %}
<span class="c"><!-- 생략 --></span>
{% endblock content %}
</code></pre></div></div>
<p>if 템플릿 태그로 <code class="language-plaintext highlighter-rouge">messages</code> 객체가 있는지 확인합니다. <code class="language-plaintext highlighter-rouge">if</code> 태그는 반드시 <code class="language-plaintext highlighter-rouge">endif</code> 태그로 종료되어야 한다는 것 주의하셔야 합니다. <code class="language-plaintext highlighter-rouge">messages</code> 객체는 <code class="language-plaintext highlighter-rouge">iterable</code> 객체이기 때문에 <code class="language-plaintext highlighter-rouge">for-in</code> 루프로 반복출력해야 합니다. <code class="language-plaintext highlighter-rouge">for-in</code> 루프처럼 <code class="language-plaintext highlighter-rouge">iterate</code>를 진행해야 메시지가 사용된 것으로 변경됩니다. message 그 자체를 출력해도 되고 message.tags 또는 message.level를 이용하셔도 됩니다. message.level은 message를 저장할 때 사용하는 그 레벨이 출력이 되고, <strong><code class="language-plaintext highlighter-rouge">tags</code>는 <code class="language-plaintext highlighter-rouge">extra_tags</code>와 message.level의 조합</strong>입니다. 이 예제에서는 message.tags를 이용했는데 extra_tags를 전달하지 않았기 때문에 level값만 출력이 됩니다. bootstrap의 alert class 를 사용하면 쉽게 강조표시를 할 수 있습니다. <code class="language-plaintext highlighter-rouge">alert-success</code>, <code class="language-plaintext highlighter-rouge">alert-info</code>, <code class="language-plaintext highlighter-rouge">alert-warning</code>, <code class="language-plaintext highlighter-rouge">alert-danger</code> 등에 따라 색상이 달라지기 때문에 message의 레벨을 적절히 조합하면 손쉽게 일관성있는 강조 표시를 할 수 있습니다. messages 에는 <code class="language-plaintext highlighter-rouge">danger</code>라는 레벨이 없기 때문에 <code class="language-plaintext highlighter-rouge">error</code>라는 레벨의 함수에는 <code class="language-plaintext highlighter-rouge">extra_tags</code>를 이용해서 <code class="language-plaintext highlighter-rouge">error</code>를 추가해줬습니다. 그러면 message.tags 는 <code class="language-plaintext highlighter-rouge">'danger error'</code>를 출력합니다.</p>
<p><img src="https://swarf00.github.io/snapshots/result_articleupdate_02.png" alt="ArticleUpdate 메시지 추가" class="border rounded shadow" /></p>
<h3 id="부트스트랩-네비게이션바">부트스트랩 네비게이션바</h3>
<p>업데이트까지 정상적으로 되는 것이 확인되었습니다. 이제 마지막으로 게시글 생성 기능을 구현하면 되는데 목록보기로 바로 갈 수 있는 버튼이 없어 불편합니다. 브라우저의 뒤로가기 기능을 사용해도 되지만 여러번 수정했을 경우 여러번 뒤로 가야 하는 문제가 있습니다. 상단의 네비게이션바가 없어서 허전했었는데 네비게이션바에 홈버튼을 추가하면 좀 편리할 것 같습니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"ko"</span><span class="nt">></span>
<span class="nt"><head></span>
{% block title %}
<span class="nt"><title></span>bbs - minitutorial<span class="nt"></title></span>
{% endblock title %}
{% block meta %}
{% endblock meta %}
{% block scripts %}
{% endblock scripts %}
{% block css %}
{% endblock css %}
<span class="nt"></head></span>
<span class="nt"><body></span>
{% block header %}
<span class="nt"><nav</span> <span class="na">class=</span><span class="s">"navbar navbar-default"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"navbar-header"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"navbar-brand"</span> <span class="na">href=</span><span class="s">"/article/"</span><span class="nt">></span>게시글 목록<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></nav></span>
{% endblock header %}
{% block content %}
{% endblock content %}
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>공통적으로 표시되어야 할 부분이기 때문에 base.html에 <code class="language-plaintext highlighter-rouge">header 블럭</code>을 추가하고 그 안에 코드를 넣었습니다. 각 페이지에서 네비게이션바를 변형시키고 싶다면 <code class="language-plaintext highlighter-rouge">block 태그</code>를 이용해서 변경하면 됩니다. base.html은 항상 특정 페이지에서 변경이 있을 수 있다는 점을 염두해두고 <strong>각 부분마다 block으로 정의</strong>해두면 좋습니다. header 블럭도 여러 태그로 구성되어 있는데 각 태그마다 블럭을 정의해도 괜찮습니다. 너무 과하게 block을 지정하면 보기에 불편하니 필요한 만큼만 정의하시기 바랍니다.</p>
<h2 id="4-게시물-작성-템플릿-구현">4. 게시물 작성 템플릿 구현</h2>
<p>게시물 작성 페이지는 이전에 <code class="language-plaintext highlighter-rouge">update</code>와 동일한 뷰와 템플릿을 사용하기로 했습니다. 아무것도 하지 않아도 게시글 목록 화면에서 게시글 작성 버튼을 누르면 게시글 작성 페이지로 잘 이동합니다. 하지만 제목이 게시글 수정으로 되어 있고, 게시글 저장 버튼을 클릭했을 때도 오류가 발생합니다. 왜냐하면 액션이 <code class="language-plaintext highlighter-rouge">update</code>로 되어 있기 때문입니다. 템플릿이 게시물 작성 페이지에서는 액션부분을 <code class="language-plaintext highlighter-rouge">create</code>와 <code class="language-plaintext highlighter-rouge">update</code>로 잘 구분해줘야 합니다.</p>
<h3 id="create-update-분리">create, update 분리</h3>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/article_update.html --></span>
{% extends 'base.html' %}
{% block title %}<span class="nt"><title></span>게시글 수정 - {{ article.pk }}. {{ article.title }}<span class="nt"></title></span>{% endblock title %}
{% block css %}
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"</span><span class="nt">></span>
{% endblock css %}
{% block content %}
{% if messages %}
{% for message in messages %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"alert alert-{{ message.tags }} alert-dismissible"</span> <span class="na">role=</span><span class="s">"alert"</span><span class="nt">></span>
{{ message }}
<span class="nt"></div></span>
{% endfor %}
{% endif %}
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"."</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">class=</span><span class="s">"form-horizontal"</span><span class="nt">></span> # action 변경
{% csrf_token %}
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"action"</span> <span class="na">value=</span><span class="s">"{% if article %}update{% else %}create{% endif %}"</span><span class="nt">></span>
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"table table-striped table-bordered"</span><span class="nt">></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>번호<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.pk }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>제목<span class="nt"></th></span>
<span class="nt"><td><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"title"</span> <span class="na">value=</span><span class="s">"{{ article.title }}"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>내용<span class="nt"></th></span>
<span class="nt"><td><textarea</span> <span class="na">rows=</span><span class="s">"10"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"content"</span><span class="nt">></span>{{ article.content }}<span class="nt"></textarea></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성자<span class="nt"></th></span>
<span class="nt"><td><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">name=</span><span class="s">"author"</span> <span class="na">value=</span><span class="s">"{{ article.author }}"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>작성일<span class="nt"></th></span>
<span class="nt"><td></span>{{ article.created_at | date:"Y-m-d H:i" }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"btn btn-primary"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>게시글 저장<span class="nt"></button></span>
<span class="nt"></form></span>
{% endblock content %}
</code></pre></div></div>
<p>upate는 url에 article.pk 값이 포함되기 때문에 아직 객체가 생성되지 않은 create 액션은 사용할 수 없는 url입니다. 그래서 현재 url을 의미하는 .을 이용했습니다. 어차피 post나 get이나 모두 같은 뷰에서 처리하니 url이 같아도 상관없습니다.</p>
<p>그리고 <code class="language-plaintext highlighter-rouge">action</code>의 값은 뷰에서 article 객체가 전달되었으면 <code class="language-plaintext highlighter-rouge">'update'</code> 그렇지 않으면 <code class="language-plaintext highlighter-rouge">'create'</code>가 되도록 수정했습니다. 게시글 생성화면에서 article 객체가 전달되지 않지만 <code class="language-plaintext highlighter-rouge">article.pk</code>, <code class="language-plaintext highlighter-rouge">article.title</code> 등의 변수는 python과는 달리 오류를 발생하지 않습니다. <strong>None 객체의 속성값에 접근하면 None이 출력됩니다</strong>.</p>
<p>이대로 테스트를 해보면 게시글 저장이 정상적으로 작동되지만, 저장된 내용으로 채워진 게시글 수정화면으로 이동합니다. 저장 전과 후의 화면이 혼동될 수 있으니 아예 새 게시글이 정상적으로 저장이 되면 게시글 목록 화면으로 이동시킵니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'article_update.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">pk</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid pk'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">article</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="n">article</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'action'</span><span class="p">)</span>
<span class="n">post_data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">)}</span>
<span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">post_data</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">post_data</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'{} 값이 존재하지 않습니다.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">key</span><span class="p">),</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">messages</span><span class="o">.</span><span class="n">get_messages</span><span class="p">(</span><span class="n">request</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'create'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="o">**</span><span class="n">post_data</span><span class="p">)</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'게시글이 저장되었습니다.'</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">post_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'게시글이 저장되었습니다.'</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">messages</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s">'알 수 없는 요청입니다.'</span><span class="p">,</span> <span class="n">extra_tags</span><span class="o">=</span><span class="s">'danger'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="s">'/article/'</span><span class="p">)</span> <span class="c1"># 정상적인 저장이 완료되면 '/articles/'로 이동됨
</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'article'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span> <span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span> <span class="k">else</span> <span class="bp">None</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="템플릿-내-message-중복">템플릿 내 message 중복</h3>
<p>각 화면 템플릿마다 message 출력을 위한 코드가 동일한 모습으로 추가되어 있습니다. 모든 화면이 base.html 템플릿을 확장하고 있기 때문에 base.html 템플릿에서 처리하면 base.html 템플릿을 확장하는 곳에서는 따로 처리해 줄 필요가 없어집니다.</p>
<p>아래와 같이 base.html 을 수정하고, article_list.html, article_update.html 에서 message 객체를 출력하는 코드를 삭제합니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"ko"</span><span class="nt">></span>
<span class="nt"><head></span>
{% block title %}
<span class="nt"><title></span>bbs - minitutorial<span class="nt"></title></span>
{% endblock title %}
{% block meta %}
{% endblock meta %}
{% block scripts %}
{% endblock scripts %}
{% block css %}
{% endblock css %}
<span class="nt"></head></span>
<span class="nt"><body></span>
{% block header %}
<span class="nt"><nav</span> <span class="na">class=</span><span class="s">"navbar navbar-default"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container-fluid"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"navbar-header"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">class=</span><span class="s">"navbar-brand"</span> <span class="na">href=</span><span class="s">"/article/"</span><span class="nt">></span>게시글 목록<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></nav></span>
{% if messages %} <span class="c"><!-- 추가된 부분 시작 --></span>
{% for message in messages %}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"alert alert-{{ message.tags }} alert-dismissible"</span> <span class="na">role=</span><span class="s">"alert"</span><span class="nt">></span>
{{ message }}
<span class="nt"></div></span>
{% endfor %}
{% endif %} <span class="c"><!-- 추가된 부분 끝 --></span>
{% endblock header %}
{% block content %}
{% endblock content %}
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>이제 기본적인 기능들은 완성되었습니다. 제네릭뷰도 다양하게 구현했다가 너무 많은 걸 설명하다보니 다 빼고 새로 <code class="language-plaintext highlighter-rouge">TemplateView</code>로만 구현하는 것을 변경했습니다. 모델과 템플릿에서도 좀 더 빙글빙글 꼬아서 여러 탬플릿태그들을 설명하고 싶었는데 <del>여러분의 수준을 고려하여</del> 미니튜토리얼이라 일단 여기에서 마무리하고 <strong>사용자인증</strong>, <strong>페이지네이션</strong> 등의 기능은 추후에 추가하기로 약속! <del>찡끗</del></p>
<blockquote>
<p>우리가 어느날 마주칠 재난은 우리가 소홀히 테스트한 어느 코드에 대한 보복이다.</p>
<p>– swarf00, 곧 마주칠 재난을 앞두고...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크의 템플릿태그와 템플릿변수를 활용한 템플릿을 작성하는 방법을 설명합니다.뷰 만들기2018-11-23T00:00:00+09:002018-11-23T00:00:00+09:00https://swarf00.github.io/2018/11/23/build-view<h2 id="1-뷰-설계">1. 뷰 설계</h2>
<p>모델을 설계할 때 가정한 사용자들의 행동들을 화면 단위로 상상해봅니다.</p>
<blockquote>
<ol>
<li>게시글 목록 - 게시판(게시글 목록)에는 게시글들의 목록이 나열됩니다.</li>
<li>게시글 목록 - 게시글들은 제목과 작성자 표시됩니다.</li>
<li>게시글 상세 화면 - 게시글을 (클릭해서) 들어가면 게시글 상세화면으로 이동하고 제목, 내용, 작성일이 출력합니다.</li>
<li>게시글 수정 화면 - 게시글 상세화면에서 수정하기 버튼을 누르면 수정하는 화면으로 이동합니다.</li>
<li>게시글 수정 화면 - 게시글 수정화면에서 저장하기 버튼을 누르면 수정된 내용이 저장되고 게시판으로 이동합니다.</li>
<li>게시글 수정 화면 - 게시글 수정화면에서 삭제하기 버튼을 누르면 게시글이 삭제되고 게시판으로 이동합니다.</li>
<li>게시글 추가 화면 - 게시판에서 새글쓰기 버튼을 누르면 새로운 게시글을 입력할 수 있는 화면이 출력됩니다.</li>
<li>게시글 추가 화면 - 게시글을 작성하고 저장하기 버튼을 누르면 수정된 내용이 저장되고 게시판으로 이동합니다.</li>
</ol>
</blockquote>
<p>화면이 크게 4가지(목록, 상세, 수정, 추가)이고 수정과 추가화면은 동일한 화면을 사용해도 될 것 같습니다. 추가화면에는 각 입력값이 빈 상태로 나타나고, 수정화면은 추가화면에 데이터베이스에 저장된 값으로 초기화해서 보여주면 될 것 같습니다.</p>
<blockquote>
<p>DRY (Don't Repeat Yourself) principle</p>
<ul>
<li>똑같은 일을 두번 하지 않는다.</li>
<li>중복되는 함수나 코드는 하나의 공통의 콤포넌트(또는 함수)에 넣고 사용한다.</li>
<li>큰 시스템을 여러 조각으로 나누고 서로 참조한다.</li>
</ul>
</blockquote>
<h2 id="2-뷰-생성">2. 뷰 생성</h2>
<h3 id="화면-핸들러-정의하기">화면 핸들러 정의하기</h3>
<p>우선 각 화면들을 표시할 핸들러들을 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">to</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'Hello {}.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">to</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">list_article</span><span class="p">(</span><span class="n">request</span><span class="p">):</span> <span class="c1"># 목록보기
</span> <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'list'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">detail_article</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">article_id</span><span class="p">):</span> <span class="c1"># 상세보기, 상세보기할 article의 id 필요
</span> <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'detail {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">article_id</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">create_or_update_article</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">article_id</span><span class="p">):</span> <span class="c1"># 생성 및 수정하기, 수정할 때는 article의 id 필요
</span> <span class="k">if</span> <span class="n">article_id</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'update {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">article_id</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'create'</span><span class="p">)</span>
</code></pre></div></div>
<p>url에 핸들러를 연결시켜 줍니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">bbs.views</span> <span class="kn">import</span> <span class="n">hello</span><span class="p">,</span> <span class="n">list_article</span><span class="p">,</span> <span class="n">detail_article</span><span class="p">,</span> <span class="n">create_or_update_article</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">list_article</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">create_or_update_article</span><span class="p">,</span> <span class="p">{</span><span class="s">'article_id'</span><span class="p">:</span> <span class="bp">None</span><span class="p">}),</span> <span class="c1"># {'article_id':None} 필수
</span> <span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">detail_article</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">create_or_update_article</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">create_or_update_article</code> 핸들러에서 article_id 함수의 기본값이 없기 때문에 반드시 핸들러에 추가 파라미터 {'article_id':None}을 넣어줘야 합니다.</p>
<p>장고를 실행하고 아래와 같이 접속해보고 화면에 정상적으로 출력이 되는 지 확인을 합니다.</p>
<ul>
<li>http://127.0.0.1/article/ # list 출력</li>
<li>http://127.0.0.1/article/create/ # create 출력</li>
<li>http://127.0.0.1/article/10/ # detail 10 출력</li>
<li>http://127.0.0.1/article/11/update/ # update 10 출력</li>
</ul>
<h3 id="액션-핸들러-정의하기">액션 핸들러 정의하기</h3>
<p>화면은 일단 더미 핸들러를 만들어 출력했으니 이제 화면 내에서의 사용자 입력(수정하기, 생성하기 등)을 처리하는 핸들러를 정의합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="c1"># 생략
</span><span class="k">def</span> <span class="nf">do_create_article</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">do_update_article</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">)</span>
</code></pre></div></div>
<p>RESTful API를 제공하면 좋겠지만 본 미니튜토리얼에서는 전통적인 POST 방식으로 액션을 처리할 예정입니다. 걸음마는 첫걸음부터, 프로그래밍은 헬로월드부터 사용자액션은 POST방식부터...순서대로 합니다.</p>
<p>액션 핸들러도 url은 따로 연결하지 않고 create_or_update 핸들러에서 메소드가 GET 일 경우 화면을 보여주고, POST 일 경우 액션핸들러를 호출합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span><span class="p">,</span> <span class="n">HttpResponseNotAllowed</span>
<span class="c1"># 생략
</span><span class="k">def</span> <span class="nf">create_or_update_article</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">article_id</span><span class="p">):</span>
<span class="k">if</span> <span class="n">article_id</span><span class="p">:</span> <span class="c1"># 수정하기
</span> <span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'update {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">article_id</span><span class="p">))</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'POST'</span><span class="p">:</span>
<span class="k">return</span> <span class="n">do_create_article</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponseNotAllowed</span><span class="p">([</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'POST'</span><span class="p">])</span>
<span class="k">else</span><span class="p">:</span> <span class="c1"># 생성하기
</span> <span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'create'</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'POST'</span><span class="p">:</span>
<span class="k">return</span> <span class="n">do_update_article</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponseNotAllowed</span><span class="p">([</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'POST'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">do_create_article</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">do_update_article</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">)</span>
</code></pre></div></div>
<p>HttpResponseNotAllowed 클래스는 HttpResponse와 다르게 status_code가 405이고 허용되지 않는 메소드로 요청했다는 의미를 가지고 있습니다. POST 방식이므로 GET과 POST만 허용합니다.</p>
<p>POST에 대해서는 아직 테스트를 하지 않고 개발을 진행하면서 테스트합니다.</p>
<h3 id="cbv로-변환">CBV로 변환</h3>
<p>create_or_update_article 핸들러는 작성하면서도 느꼈지만 <strong>DRY</strong>원칙이 떠오릅니다. 장고에서는 FBV(Function Based View)와 CBV(Class Based View) 두가지의 뷰를 개발할 수 있는 방법을 제공합니다. 현재까지는 FBV로만 개발했지만 CBV를 이용한다면 중복된 코드를 최소화할 수 있습니다. <del>원래는 다 구현하고 리팩토링 하려했으나</del> 더 늦기 전에 CBV로 변환합니다. ^^;</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">TemplateView</span>
<span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span> <span class="c1"># 게시글 목록
</span> <span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{}</span> <span class="c1"># 템플릿에 전달할 데이터
</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleDetailView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span> <span class="c1"># 게시글 상세
</span> <span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span> <span class="c1"># 게시글 추가, 수정
</span> <span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="c1"># 화면 요청
</span> <span class="n">ctx</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="c1"># 액션
</span> <span class="n">ctx</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">to</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s">'Hello {}.'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">to</span><span class="p">))</span>
</code></pre></div></div>
<p>장고에서는 CBV를 지원하기 위해 <code class="language-plaintext highlighter-rouge">제네릭 뷰</code>라고 부르는 다양한 클래스들을 제공있습니다. 대표적으로 <code class="language-plaintext highlighter-rouge">TemplateView</code>, <code class="language-plaintext highlighter-rouge">ListView</code>, <code class="language-plaintext highlighter-rouge">DetailView</code>, <code class="language-plaintext highlighter-rouge">CreateView</code>, <code class="language-plaintext highlighter-rouge">UpdateView</code> 등이 있습니다. 그 중 가장 간단한 <code class="language-plaintext highlighter-rouge">TemplateView</code>를 이용해 변환했습니다.</p>
<p>모든 뷰에 클래스변수로 template_name 속성을 추가해서 모두 'base.html'이라고 정의했습니다. base.html은 템플릿 파일의 이름입니다. template_name을 정의하면 장고에서는 자동으로 앱 디렉토리의 templates 디렉토리에서 참조해 파일명이 template_name인 파일을 템플릿으로 사용합니다.</p>
<p>제네렉 뷰에서는 http메소드에 따라 해당 이름의 클래스 매소드를 호출합니다. 화면요청의 경우 항상 http get으로 요청할 것이고 액션의 경우 항상 http post로 요청할 것입니다. http get은 데이터를 url에 query 파라미터로만 보낼 수 있어서 제한적이지만, http post는 데이터를 body에 보낼 수 있어서 사이즈에 제한 없이 다양한 종류의 데이터를 전송할 수 있기 때문입니다.</p>
<p>모든 뷰에 공통적으로 get 핸들러가 정의되어 있지만 post 핸들러는 액션이 필요한 ArticleCreateUpdateView에서만 정의했습니다. get 핸들러와 post 핸들러를 언제 사용해야 하는 지 헷갈린다면 간단하게 <strong>화면을 보여줘 = get, 서버에서 처리해줘 = post</strong> 라고 생각하시면 됩니다. <strong>게시글을 작성할 화면을 보여줘 = get, 게시글을 서버에 저장해줘 = post</strong> 이런 식입니다. 여러가지의 뷰들을 처리하다보면 익숙해질 수 있습니다.</p>
<p>모든 핸들러에 공통적으로 self.render_to_response로 반환하도록 되어 있습니다. render_to_response는 제네릭 뷰에서 제공하는 함수로서, 템플릿을 자동적으로 기본 템플릿 엔진을 이용해서 html로 변환해주는 함수입니다. 이 때 템플릿 내부에 변수를 사용해야 한다면 인자로 ctx(CONTEXT) 객체를 전달해 줄 수 있습니다. 공통된 템플릿을 사용하더라도 ctx 값은 각 뷰마다 적절하게 사용하면 됩니다. 단 반드시 dict 형태로 정의해야 합니다.</p>
<p>핸들러가 함수에서 클래스로 변경되었으니 url 연결도 변경해줘야 합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># minitutorial/urls.py
</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span>
<span class="kn">from</span> <span class="nn">bbs.views</span> <span class="kn">import</span> <span class="n">hello</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s">'hello/<to>'</span><span class="p">,</span> <span class="n">hello</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/'</span><span class="p">,</span> <span class="n">ArticleListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/create/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/'</span><span class="p">,</span> <span class="n">ArticleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'article/<article_id>/update/'</span><span class="p">,</span> <span class="n">ArticleCreateUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()),</span>
<span class="n">path</span><span class="p">(</span><span class="s">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div></div>
<p>path 함수의 두번째 인자로 핸들러 함수가 전달되던 것이 뷰 클래스의 as_view()메소드 실행 결과값으로 변경되었습니다. as_view메소드는 간단하게 설명하면 뷰클래스의 초기화와 핸들러를 반환하는 기능을 제공합니다.</p>
<p>마지막으로 뷰에서 호출할 template을 작성을 합니다. template은 우선 dummy로 만들고 모든 화면이 정상적으로 동작하는 것을 확인하면 하나씩 변경할 것입니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"ko"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><title></span>base<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
base....
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>base.html은 아무런 템플릿태그나 템플릿변수도 사용하지 않는 dummy template 입니다. 이럴때 장고 템플릿은 아무런 변경을 하지 않습니다.</p>
<p>FBV로 테스트했을 때처럼 장고를 실행하고 url에 접속해서 정상적으로 화면이 출력되는 지 확인합니다.</p>
<ul>
<li>http://127.0.0.1/article/ # base... 출력</li>
<li>http://127.0.0.1/article/create/ # base... 출력</li>
<li>http://127.0.0.1/article/10/ # base... 출력</li>
<li>http://127.0.0.1/article/11/update/ # base... 출력</li>
</ul>
<h3 id="데이터-검색">데이터 검색</h3>
<p>이제 실제로 데이터를 불러와서 화면에 출력해야 하는 뷰에서 데이터를 검색 후 어떻게 템플릿에 데이터를 전달하는 지 확인할 차례입니다.
TemplateView는 데이터 검색에 대한 메소드를 제공해주지 않기 때문에 상속받은 클래스에 직접 구현해야 합니다.</p>
<p>먼저 ArticleListView부터 하나씩 수정해 나갑니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="c1"># 생략
</span><span class="kn">from</span> <span class="nn">bbs.models</span> <span class="kn">import</span> <span class="n">Article</span>
<span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span> <span class="c1"># 모든 게시글
</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span> <span class="c1"># 클래스의 이름
</span> <span class="s">'data'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span> <span class="c1"># 검색 결과
</span> <span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="c1"># 생략
</span></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">queryset</code> 이라는 클래스 변수를 정의를 했는데 <code class="language-plaintext highlighter-rouge">queryset</code>은 <code class="language-plaintext highlighter-rouge">TemplateView</code>를 제외한 다른 제네릭뷰들에서 공통적으로 정의된 클래스변수입니다. 제네릭뷰에서 데이터 검색관련 클래스 변수가 필요하다면 <del>꼭 이렇게 하지 않아도 되지만</del> <code class="language-plaintext highlighter-rouge">queryset</code>으로 정의하는 것을 추천합니다. <code class="language-plaintext highlighter-rouge">queryset</code> 뿐만 아니라 다른 클래스 변수들도 장고에서 사용하는 일반적인 변수명을 <del>아는만큼</del> 사용하는 것을 추천합니다.</p>
<p>ctx 값은 <code class="language-plaintext highlighter-rouge">view</code>와 <code class="language-plaintext highlighter-rouge">data</code>로 키를 구성했습니다. <code class="language-plaintext highlighter-rouge">view</code>의 값 <code class="language-plaintext highlighter-rouge">self.__class__.__name__</code>는 해당 제네릭뷰 인스턴스의 클래스 이름으로서, 현재 보여지는 화면을 처리하는 뷰의 이름을 전달했습니다. <code class="language-plaintext highlighter-rouge">data</code>는 검색된 데이터 그대로 전달하는데 템플릿에서는 장고ORM의 <code class="language-plaintext highlighter-rouge">QuerySet</code>을 잘 이해하기 때문에 그대로 사용가능 합니다.</p>
<blockquote>
<p>다른 제네릭뷰에서는 <code class="language-plaintext highlighter-rouge">SingleObjectMixin</code>, <code class="language-plaintext highlighter-rouge">MultipleObjectMixin</code> 등을 상속받아 정의가 되어 있습니다. 두 믹스인의
클래스변수를 미리 알아두면 좋을 것 같습니다. 메소드는 소스코드를 참고해서 공부해보세요.</p>
<ul>
<li><strong>SingleObjectMixin</strong>
<ul>
<li>model = None # 뷰에서 사용할 모델</li>
<li>queryset = None # 검색 객체</li>
<li>slug_field = 'slug' # 모델에 정의된 슬러그 필드 이름</li>
<li>context_object_name = None # 템플릿에 전달될 검색 데이터 이름</li>
<li>slug_url_kwarg = 'slug' # path 함수로부터 전달받을 슬러그의 키워드 이름</li>
<li>pk_url_kwarg = 'pk' # path 함수로부터 전달받을 pk의 키워드 이름</li>
<li>query_pk_and_slug = False # 슬러그와 pk를 데이터 검색에서 사용할 지 여부</li>
</ul>
</li>
<li><strong>MultipleObjectMixin</strong>
<ul>
<li>allow_empty = True # 검색결과가 없어도 되는 지 여부</li>
<li>queryset = None # 검색 객체</li>
<li>model = None # 뷰에서 사용할 모델</li>
<li>paginate_by = None # 검색데이터가 많을 때 한 페이지당 보여줄 데이터 수량</li>
<li>paginate_orphans = 0 # 마지막 페이지의 최소 데이터 수량</li>
<li>context_object_name = None # 템플릿에 전달된 검색 데이터 이름</li>
<li>paginator_class = django.core.paginator.Paginator # 페이지화를 작동시킬 구현체</li>
<li>page_kwarg = 'page' # 검색할 페이지 번호에 대한 키워드 이름</li>
<li>ordering = None # 검색시 사용할 정렬방식. ORM의 <code class="language-plaintext highlighter-rouge">order_by</code></li>
</ul>
</li>
</ul>
</blockquote>
<p>그리고 템플릿을 수정해서 ctx의 값들을 간단하게 출력해봅니다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- bbs/templates/base.html --></span>
<span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"ko"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><title></span>base<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
view: {{ view }} <span class="c"><!-- ctx['view'] --></span>
<span class="nt"><br></span>
data: {{ data }} <span class="c"><!-- ctx['data'] --></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>템플릿에서 ctx의 값을 사용하는 방법은 간단합니다. ctx 객체에 저장된 데이터의 key 이름을 <code class="language-plaintext highlighter-rouge">{{ }}</code> 안에 넣어주기만 합니다. 템플릿 엔진은 <code class="language-plaintext highlighter-rouge">{{ }}</code>로 표시를 안에 있는 key에 하당하는 값으로 치환해줍니다.</p>
<p><img src="https://swarf00.github.io/snapshots/result_articlelist_01.png" alt="ArticleListView 결과" class="border rounded shadow" /></p>
<p>출력결과가 위와 같이 나온다면 성공입니다. <code class="language-plaintext highlighter-rouge"><QuerySet [<Article: [1] How to create a article>]></code>는 검색된 <code class="language-plaintext highlighter-rouge">QuerySet</code>오브젝트이고 리스트 안에 보이는 객체들이 검색결과입니다.</p>
<p>이제 나머지 ArticleDetailView 와 ArticleCreateUpdateView의 화면요청에 대한 데이터 검색을 구현합니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="k">class</span> <span class="nc">ArticleDetailView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span> <span class="c1"># 검색데이터의 primary key를 전달받을 이름
</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span> <span class="c1"># queryset 파라미터 초기화
</span> <span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span> <span class="c1"># pk는 모델에서 정의된 pk값, 즉 모델의 id
</span> <span class="k">return</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span> <span class="c1"># pk로 검색된 데이터가 있다면 그 중 첫번째 데이터 없다면 None 반환
</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid article_id'</span><span class="p">)</span> <span class="c1"># 검색된 데이터가 없다면 에러 발생
</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid article_id'</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">pk_url_kwargs</code> 클래스변수를 정의할 필요없이 urls.py에서 url의 <code class="language-plaintext highlighter-rouge"><article_id></code>를 <code class="language-plaintext highlighter-rouge"><pk></code>로 변환해줘도 상관없습니다. 보통은 <code class="language-plaintext highlighter-rouge"><pk></code>을 사용하지만 다른 방법도 있다는 것을 알려드리고 싶어서 굳이 이렇게 <del>뻘짓</del>full git을 하고 있습니다.</p>
<p>화면출력을 위한 데이터 검색은 이정도면 될 것 같습니다. 추가 작업은 더 필요할 때 구현하도록 합니다.</p>
<p>이제 액션을 구현하도록 합니다. 액션은 <code class="language-plaintext highlighter-rouge">create</code>, <code class="language-plaintext highlighter-rouge">update</code> 두 가지가 있는데 클라이언트(브라우저)로부터 데이터를 전달받아야 합니다. 전달된 데이터는 request.POST라는 <code class="language-plaintext highlighter-rouge">QueryDict</code>객체에 저장됩니다. 전달받을 데이터는 title(제목), content(내용), author(작성자) 이 세가지 입니다. 모두 필수로 입력받고 데이터가 없거나 문자열인 경우 오류를 발생시킵니다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.utils.decorators</span> <span class="kn">import</span> <span class="n">method_decorator</span>
<span class="kn">from</span> <span class="nn">django.views.decorators.csrf</span> <span class="kn">import</span> <span class="n">csrf_exempt</span>
<span class="c1"># 생략
</span>
<span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid article_id'</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'action'</span><span class="p">)</span> <span class="c1"># request.POST 객체에서 데이터 얻기
</span> <span class="n">post_data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">)}</span>
<span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">post_data</span><span class="p">:</span> <span class="c1"># 세가지 데이터 모두 있어야 통과
</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">post_data</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'no data for {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">key</span><span class="p">))</span>
<span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'create'</span><span class="p">:</span> <span class="c1"># action이 create일 경우
</span> <span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="n">title</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="n">content</span><span class="p">,</span> <span class="n">author</span><span class="o">=</span><span class="n">author</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span><span class="p">:</span> <span class="c1"># action이 update일 경우
</span> <span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid article_id'</span><span class="p">)</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">post_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span> <span class="c1"># action이 없거나 create, update 중 하나가 아닐 경우
</span> <span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid action'</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span> <span class="c1"># 액션 작업 후 화면을 보냄
</span></code></pre></div></div>
<p>http post의 경우 request.body 객체에 데이터 내용이 문자열 형태로 전달됩니다. 이 데이터가 딕셔너리로 변환이 가능할 경우 장고의 미들웨어가 자동으로 request.POST 객체에 변환된 값을 저장합니다. 변환된 값은 딕셔너리와 동일하게 읽을 수 있습니다. 하지만 immutable 객체이기 때문에 수정이 불가합니다.</p>
<p>실제 동작이 작동하는 지 curl또는 사용하시는 rest client로 테스트를 해봅니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>test-venv-36<span class="o">)</span> <span class="nv">$ </span>curl <span class="nt">-X</span> POST http://127.0.0.1:8000/article/create/ <span class="nt">-d</span> <span class="s2">"title='test title'&content='test content'"</span>
</code></pre></div></div>
<p><img src="https://swarf00.github.io/snapshots/result_articlecreate_01.png" alt="ArticleCreateUpdate 뷰의 create 결과" class="border rounded shadow" /></p>
<h3 id="csrf-verification">CSRF verification</h3>
<p><code class="language-plaintext highlighter-rouge">CSRF verification failed. Request.aborted.</code> 라는 오류메시지가 반환됐습니다. 장고의 여러 보안관련 기능 중 <code class="language-plaintext highlighter-rouge">CSRF verification</code> 이라는 것이 있는데, 모든 http post 요청은 장고에서 자동생성한 <code class="language-plaintext highlighter-rouge">csrftoken</code>을 body 데이터에 포함하고 있어야 합니다. <code class="language-plaintext highlighter-rouge">csrfmiddlewaretoken</code>이라는 값이 없을 경우 CSRF 공격으로 인식하고 미들웨어에서 에러를 발생시킵니다. 정상적인 템플릿으로 테스트하면 <code class="language-plaintext highlighter-rouge">csrfmiddlewaretoken</code>을 보낼 수 있지만 이번은 간단하게 뷰에서 <strong>CSRF verification 기능을 예외처리</strong>하고 테스트하겠습니다. 뷰 테스트가 종료되고 본격적으로 템플릿을 개발하기 전까지 예외처리해두겠습니다.</p>
<blockquote>
<p>curl 설치법</p>
<ul>
<li>실행환경이 윈도우라면 https://curl.haxx.se/download.html 에서 다운로드받으세요.</li>
<li>centos - yum install curl</li>
<li>ubuntu - sudo apt install curl</li>
<li>macos - 설치되어 있음.</li>
</ul>
<p>postman 설치법</p>
<ol>
<li>크롬 실행</li>
<li>메뉴 > 창 > 확장 프로그램</li>
<li>postman 검색 및 설치</li>
</ol>
</blockquote>
<p>CSRF verification을 예외처리할 때 계속 거슬리던 반복 코드를 정리하겠습니다. <code class="language-plaintext highlighter-rouge">get_objct</code> 메소드 호출 뒤에 항상 데이터가 None인지 아닌지를 확인하고 None일 경우 동일한 작업을 합니다. 차라리 <code class="language-plaintext highlighter-rouge">get_object</code>에서 <code class="language-plaintext highlighter-rouge">pk</code>가 <code class="language-plaintext highlighter-rouge">None</code>이 아닌데 결과값이 <code class="language-plaintext highlighter-rouge">None</code>인지 체크를 하도록 수정하겠습니다. <code class="language-plaintext highlighter-rouge">kwargs</code>에 <code class="language-plaintext highlighter-rouge">pk</code>가 있다는 것은 <code class="language-plaintext highlighter-rouge">update</code>를 의미하고, <code class="language-plaintext highlighter-rouge">pk</code>가 없다는 건 <code class="language-plaintext highlighter-rouge">create</code>를 의미합니다.</p>
<blockquote>
<p>반복되는 코드들을 볼 때마다 가슴 한쪽에 고구마가 틀어막고 있음을 느낀다면 대단히 정상적입니다.</p>
</blockquote>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># bbs/views.py
</span>
<span class="kn">from</span> <span class="nn">django.utils.decorators</span> <span class="kn">import</span> <span class="n">method_decorator</span>
<span class="kn">from</span> <span class="nn">django.views.decorators.csrf</span> <span class="kn">import</span> <span class="n">csrf_exempt</span>
<span class="c1">#생략
</span>
<span class="o">@</span><span class="n">method_decorator</span><span class="p">(</span><span class="n">csrf_exempt</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">'dispatch'</span><span class="p">)</span> <span class="c1"># 모든 핸들러 예외 처리
</span><span class="k">class</span> <span class="nc">ArticleCreateUpdateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s">'base.html'</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="nb">all</span><span class="p">()</span>
<span class="n">pk_url_kwargs</span> <span class="o">=</span> <span class="s">'article_id'</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">queryset</span>
<span class="n">pk</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">pk_url_kwargs</span><span class="p">)</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">queryset</span><span class="o">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">pk</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">article</span><span class="p">:</span> <span class="c1"># 검색결과가 없으면 곧바로 에러 발생
</span> <span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid pk'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">article</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'action'</span><span class="p">)</span>
<span class="n">post_data</span> <span class="o">=</span> <span class="p">{</span><span class="n">key</span><span class="p">:</span> <span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">)}</span>
<span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">post_data</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">post_data</span><span class="p">[</span><span class="n">key</span><span class="p">]:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'no data for {}'</span><span class="o">.</span><span class="nb">format</span><span class="p">(</span><span class="n">key</span><span class="p">))</span>
<span class="k">if</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'create'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="n">title</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="n">content</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="n">author</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">action</span> <span class="o">==</span> <span class="s">'update'</span><span class="p">:</span>
<span class="n">article</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">post_data</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="n">article</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="n">article</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Http404</span><span class="p">(</span><span class="s">'invalid action'</span><span class="p">)</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">'view'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">__class__</span><span class="o">.</span><span class="n">__name__</span><span class="p">,</span>
<span class="s">'data'</span><span class="p">:</span> <span class="n">article</span>
<span class="p">}</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>모든 제네릭 뷰에는 dispatch라는 메소드가 내장되어 있습니다. 이 메소드에서 get, post 핸들러로 분기시켜주는 역할을 합니다. 이 dispatch 함수에 csrf_exempt 데코레이터로 예외처리를 해주면 되는데, 그러면 dispatch함수를 한번 더 오버라이딩해줘야 하는 수고가 있습니다. 수고로움을 견디지 못하는 탓에 method_decorator 데코레이터를 사용했습니다. 클래스에서 오버라이딩하지 않은 메소드에 데코레이팅 할 때 편리하게 사용할 수 있습니다.</p>
<p>여기까지 이해하셨다면 이제 템플릿만 남았습니다. 모델과 뷰를 개발하면서 모델은 이런 걸 하고, 뷰는 저런 걸 하는구나 하는 감이 와야 합니다. <del>첨부터 한번 더 본다고 돈 들지 않습니다.</del> 그렇지 않다면 저의 탓입니다. 못난 독자를 둔 저의 탓으로 넘기고 가벼운 마음으로 다음 페이지로 넘어가십시오.</p>
<blockquote>
<p>모델과 뷰와 책임감을 수반하는 고통은 또한 템플릿을 주기도 한다.</p>
<p>– swarf00, <del>못난</del> 불쌍한 독자들을 생각하며...</p>
</blockquote>Sehun Kimpaul-kim00@hanmail.net장고(Django) 웹프레임워크의 FBV와 CBV를 통해 뷰를 구현하는 방법을 설명합니다. CBV는 제네릭 뷰 중 가장 단순한 구현체인 TemplateView를 통해 개발하는 방법을 설명합니다.Get Started2018-11-23T00:00:00+09:002018-11-23T00:00:00+09:00https://swarf00.github.io/2018/11/23/get-started<h2 id="장고를-사용해야-할-이유가-뭔가요">장고를 사용해야 할 이유가 뭔가요?</h2>
<p>장고를 사용한다면 단지 몇 시간만에도 작은 웹 어플리케이션을 개발할 수 있습니다. 웹 개발의 일반적인 기능들은 장고가 처리를 합니다. 여러분은 단지 비지니스 로직에만 집중하면 됩니다.
또한 장고는 오픈소스이고 무료입니다. 수많은 오픈소스 개발자들이 장고의 기능향상을 위해 힘쓰고 있고, 부가적인 라이브러리들을 개발하고 있습니다. 여러분이 원하는 대부분의 기능은 이미 구현되어 있습니다. 단지 검색하시고 설치하세요. 약간의 설정이 필요할 테지만 스스로 개발하는 것보다 훨씬 빠르게 완료할 수 있습니다.</p>
<h3 id="높은-생산성">높은 생산성</h3>
<p>장고는 설계단계부터 빠른 개발을 목표로 하고 있습니다.</p>
<h3 id="다양한-기능">다양한 기능</h3>
<p>사용자 인증, 데이터 관리 사이트, 사이트맵, RSS feed 등 흔히 웹 개발할 때 구현해야 할 많은 것들이 모두 내장되어 있습니다.</p>
<h3 id="안정적인-보안">안정적인 보안</h3>
<p>보안문제를 심도있게 다루고 있습니다. 개발자들이 흔히 일어나는 SQL injection, XSS, CSRF, Clickjacking 등의 보안문제를 방지하도록 도와줍니다. 또한 사용자 인증 시스템은 계정과 비밀번호를 안전하게 관리할 수 있는 기능을 제공합니다.</p>
<h3 id="고가용성">고가용성</h3>
<p>장고는 빠르고 유연하게 가용성을 높이는 방법을 제공되고 있습니다.
데이터베이스나 캐시 서버를 분리해서 개발한다면 쉽게 가용성을 높일 수 있습니다.</p>
<h3 id="높은-범용성">높은 범용성</h3>
<p>기업, 기관, 정부 등 다양한 분야에서 장고를 이용하여 웹 서비스를 제공하고 있습니다.
CMS 부터 SNS 그리고 과학계산 플랫폼으로도 사용가능합니다.</p>
<h2 id="설치">설치</h2>
<p><a href="/install.html">설치 따라하기</a></p>
<h2 id="장고-앱-맛보기">장고 앱 맛보기</h2>
<p><a href="/setup-project.html">프로젝트 만들기</a></p>
<p><a href="/build-model.html">모델 만들기</a></p>
<p><a href="/build-view.html">뷰 만들기</a></p>
<p><a href="/build-template.html">템플릿 만들기</a></p>
<h2 id="실습">실습</h2>
<hr />Sehun Kimpaul-kim00@hanmail.net장고 웹프레임워크의 장점과 어떻게 개발을 시작할 수 있는 지 설명합니다.