SlideShare a Scribd company logo
1 of 64
Download to read offline
Django를 Django답게
Django로 뉴스 사이트 만들기
자기 소개
NRISE Backend Chapter Leader (현재)
ODK Media Backend Leader (3년)
프리랜서 (1년), 혜움세무회계 CTO 4개월 포함
SmartStudy Software engineer (3년 반)
아이티동아 Developer (4년)
정경업 (파이)
발표 주제를 정하기까지
지금까지 Django 사용법에서 시작하여 자아성찰까지 여러 주제 발표 했었음
흔한 고민 이번엔 뭘하지…?
처음으로 돌아가 사용법에 대해 가볍게 이야기 해볼까?
Django는 뉴스 CMS로 시작된 프레임워크
마침 외주로 아이티동아, 게임동아 뉴스 사이트를 재개발
발표 해보자!
요구사항과 구현 목표
기본적인 뉴스 사이트 기능
● 홈, 사이드 위젯 구성과 기사 읽기, 목록, 작성, 포털 배포
기존 데이터 이전
한가지 앱으로 두 사이트 운영
유지 보수 최소화(외주)
기자들이 직접 홈과 위젯을 편집 가능
시작
프로젝트 레이아웃
Docker-Compose
IDE(Pycharm) 연동
개발 환경
프로젝트 레이아웃
api : API 구현 코드 분리
app : 프로젝트 메인에 관련 설정 파일
article : 기사 구현
home : 홈 화면 및 사이드 위젯 등 공용 코드
migrator : 데이터 이전 작업
publish : 기사 배포 관련
search : 검색
docker-compose.yml
배포 관련 스크립트
Docker-compose
# 많이 생략된 대략적인 docker-compose.yml
services:
postgres:
image: postgres
django:
build:
dockerfile: ./docker/django.dockerfile
command: python3 manage.py runserver 0.0.0.0:8000
volumes:
- ./app:/usr/src/web/app
ports:
- "80:8000"
depends_on:
- postgres
pure-ftpd: # FTP 테스트 용
image: stilliard/pure-ftpd
Docker-compose
Docker-compose를 개발환경에서 쓰면
● DB 등 필요한 서비스를 손쉽게 관리할 수 있음
● 파이썬 라이브러리 완전히 독립적
● 다 까먹어도 'up'명령 한줄이면 개발 환경 구축
# 대충 적은 사용 법
docker-compose build # 이미지 생성
docker-compose up # 서비스 실행
docker-compose down # 서비스 내리기
docker-compose run django bash # shell(bash) 접근
IDE(Pycharm) 연동
Pycharm Settings
● Python Interpreter - Docker-compose
● Language & Frameworks - Django
● Run/Debug Configrations - Django Server, Django tests
IDE에서 바로 테스트 실행 가능
1장
Sites로 멀티 도메인 구현
Article 모델 확장과 Test코드
CBV로 기사 목록, 읽기 구현
돌아가는 기반
Sites로 멀티 도메인 구현
한 앱으로 두 사이트 만드는 방법
# models.py
from django.contrib.sites.models import Site
class Article(TimeStampedModel):
site = models.ForeignKey(Site, on_delete=models.PROTECT, db_index=True)
title = models.CharField(max_length=250, db_index=True)
# views.py
from django.contrib.sites.shortcuts import get_current_site
def list_view(request):
articles = Article.objects.filter(site=get_current_site(request))
한 앱으로 두 사이트 만드는 방법
내장된 Sites 모델과 get_current_site 함수로 간단하게 구현
● 실제 적용된 내용은 Category - Article 모델 관계
Sites 내용은 Fixture로 만들어놓고 테스트 코드 및 배포시 사용 가능
- model: sites.site
pk: 1
fields:
domain: it.donga.com
name: IT동아
- model: sites.site
pk: 2
fields:
domain: game.donga.com
name: 게임동아
Article 모델 확장과 Test코드
Manager로 배포 상태의 기사만 다루기
def live_q():
q = Q(published=_lte=timezone.now()) | Q(published=_isnull=True)
return q & Q(active=True)
class LiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(live_q())
class Article(TimeStampedModel):
# managers
live_objects = LiveManager()
objects = models.Manager()
live_objects를 쓰면 배포 상태의 기사만 가져오기 쉽다
objects는 Admin에서 쓰이므로 유지
Manager로 배포 상태의 기사만 다루기
# tests.py
class TestArticleModels(TestCase):
def test_live(self):
# 모델 인스턴스 목 생성
baker.make(Article, active=True) # 유효
baker.make(Article, active=False) # 무효
# 전체 기사는 2개지만 배포 기사는 1개인 것을 Test 로 검증
self.assertEqual(Article.objects.count(), 2)
self.assertEqual(Article.live_objects.count(), 1)
간단한 카운트 함수와 테스트 코드
기사를 읽었을 때 카운트가 올라가는 기능을 Model에 구현
# models.py
class Article(TimeStampedModel):
def hit(self):
Article.objects.filter(id=self.pk).update(hit_count=F("hit_count") + 1)
간단한 카운트 함수와 테스트 코드
작동하는지 확인하는 간단한 Test 작성
# tests.py
class TestArticleModels(TestCase):
def test_hit(self):
article = baker.make(Article)
self.assertEqual(article.hit_count, 0)
article.hit() # +1
article.refresh_from_db() # DB에서 값을 다시 읽기
self.assertEqual(article.hit_count, 1)
article.hit()
article.hit() # +2
article.refresh_from_db()
self.assertEqual(article.hit_count, 3)
Signal로 기사 번호 만들기
@receiver(post_save, sender=Article)
def article_auto_number(sender, instance, created, **kwargs):
if not created:
return
if instance.number > 0:
return
queryset = sender.objects.filter(site=instance.site)
max_number = queryset.aggregate(Max("number"))["number=_max"]
queryset.filter(id=instance.id).update(number=max_number + 1)
두 사이트의 기사 숫자가 차이나고
기존 데이터를 가져올 때 옮겨오기 쉽게 하려고
ID를 그냥 쓰지 못하고 number를 만듬
Signal로 기사 번호 만들기
class TestArticleModels(TestCase):
def test_auto_number(self):
a_1 = baker.make(Article, site=self.site_a) # a, number 1
a_7 = baker.make(Article, site=self.site_a, number=7) # a, number 7
a_1.refresh_from_db()
a_7.refresh_from_db()
self.assertEqual(a_1.number, 1)
self.assertEqual(a_7.number, 7)
baker.make(Article, site=self.site_b) # b, number 1
baker.make(Article, site=self.site_b) # b, number 2
b_3 = baker.make(Article, site=self.site_b) # b, number 3
b_3.refresh_from_db()
self.assertEqual(b_3.number, 3)
값이 생성될 때마다 post_save signal이 실행되는지 확인
CBV로 기사 목록, 읽기 구현
Function Based View VS Class Based View
Function Based View
● 절차적으로 작성하여 코드가 섞이기 쉬우며
● 기능을 찾아내기 어려운 코드가 되어
● 재활용도 덜 신경쓰게 되는걸 자주 봅니다
Class Based View
● 역할이 명시적으로 나뉘어 코드가 덜 섞이며
● 있는 기능을 잘 쓰는 법을 찾아야 하지만
● 재활용 하기 쉬워 적은 양의 코드로 많은 구현을 할 수 있습니다
기사 목록
class ArticleListView(BaseSideMixin, PageNumbersMixin, ListView):
paginate_by = 10
template_name = 'article/list.html'
category = None # get_queryset에서 할당 후 get_context_data에서 사용
def get_queryset(self):
self.category = get_object_or_404(
Category, site=get_current_site(self.request), code=self.kwargs['category_code'])
return Article.live_objects.filter(category=self.category).only(*ARTICLE_LIST_FIELDS)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data.update({'title': self.category.name, 'category': self.category})
return data
기사 목록
상속 받은 클래스
● ListView : Django에서 제공하는 기본 클래스
● BaseSideMixin : 사이트의 사이드바를 구현 (후술)
● PageNumbersMixin : 페이지 번호 방식을 변경하기 위한 구현
속성, 함수
● paginate_by : 페이지당 개수
● template_name : View에서 사용할 템플릿 이이름
● get_queryset : Django ORM에서 가져올 목록용 쿼리, 분류를 함께 구현
● get_context_data : 템플릿에 보여질 context 생성
기사 읽기
class ArticleDetailView(BaseSideMixin, DetailView):
template_name = 'article/detail.html'
slug_url_kwarg = 'number'
slug_field = 'number'
def get_queryset(self):
site = get_current_site(self.request)
return Article.live_objects.filter(site=site).select_related('site', 'category')
def get_context_data(self, **kwargs):
self.object.hit() # 기사 조회수 증가
data = super().get_context_data(**kwargs)
data['tag_names'] = self.object.tags.values_list('name', flat=True)
return data
2장
캐시 적용 및 관리
모양을 바꿀 수 있는 홈, 사이드 위젯
Admin 쓸만하게
운영에 필요한 것들
캐시 적용 및 관리
캐시 적용 및 관리
웹 서비스 대부분의 부하는 DB이며
DB 쿼리를 최대한 덜하도록 캐시를 많이 씁니다
캐시를 적용하는 것은 Django에서 손쉽게 가능하나
관리는 알아서 해야 합니다
약간의 코드로 관리할 수 있게 구현 해보았습니다
일원화된 캐시 키 관리
settings에서 어떤 캐시가 있는지 확인할 수 있게 정리
# settings.py
CACHE_KEYS = {
'article': [
'article_links',
'article_footer',
'article_content',
],
'home': [
'widget_side',
'widget_home',
],
'text_page': [
'text_detail'
]
}
템플릿에 캐시 적용하기
기사 상단
{% load cache %}
{% cache 7200 article_links object.id %}
{% if object_prev %}
<link rel="prev" title="{{ object_prev.title }}"
href="{{ object_prev.get_absolute_url }}">
{% endif %}
{% if object_next %}
<link rel="next" title="{{ object_next.title }}"
href="{{ object_next.get_absolute_url }}">
{% endif %}
<meta property="og:url" content="{{ request.build_absolute_uri }}"=>
<meta property="og:type" content="website"=>
<meta property="og:title" content="{{ object.title }}"=>
<meta property="og:description" content="{{ object.intro }}"=>
<meta property="og:image" content="{{ object.thumbnail|convert_image_url:site }}"=>
{% endcache %}
템플릿에 캐시 적용하기
기사 본문
{% cache 7200 article_content object.id %}
<header>
<h1>{{ object.title }}=/h1>
{% include 'article/_tags.html' %}
<strong>{{ object.reporter_name }}=/strong>
<a href="{% url 'search-list' %}?reporter_name={{ object.reporter_name }}"><i
class="fas fa-search">=/i>=/a>
<a href="mailto:{{ object.reporter_email }}">{{ object.reporter_email }}=/a>
{% include '_time.html' with time=object.published %}
=/header>
{{ object.contents|convert_html_image_url:site|safe }}
{% include 'article/_tags.html' %}
{% endcache %}
템플릿에 캐시 적용하기
기사 하단
{% cache 7200 article_footer object.id %}
<div class="widget related-articles">
<h5>관련 기사=/h5>
<ul>{% for i in object.related_articles %}
<li><a href="{% url 'article-detail' i.number %}">{{ i.title }}=/a>=/li>
{% empty %}<li class="list-group-item">아직 관련 기사가 없습니다.=/li>{% endfor %}
=/ul>
=/div>
<div>
{% if prev_number %}<a class="btn" href="{% url 'article-detail' prev_number %}">이전=/a>{%
endif %}
<a class="btn" href="">위로=/a>
<a class="btn" href="{% url 'article-list' object.category.code %}">목록=/a>
{% if next_number %}<a class="btn" href="{% url 'article-detail' next_number %}">다음=/i>=/a>{%
endif %}
=/div>
{% endcache %}
기사 작성 및 수정시마다 캐시 업데이트
Signal 사용
● settings에서 어떤 캐시가 있는지 확인 가능하고
● 데이터 업데이트 시점에 Signal로 캐시를 최신으로 유지
● 뉴스 기사 생성 수가 그리 크지 않으므로 가볍게 선택 가능한 전략
@receiver(post_save, sender=Article)
def article_cache_clear(sender, instance, created, **kwargs):
# settings에서 키 값을 가져옴
for name in settings.CACHE_KEYS['article']:
key = make_template_fragment_key(name, [instance.id])
cache.delete(key)
# 기사 내용이 다른 캐시에서도 쓰이는 경우 찾아서 삭제
cache.delete_many([f'{name}_{instance.site.id}' for name in settings.CACHE_KEYS['home']])
cache.delete(f'{instance.site.domain}_last_published')
모양을 바꿀 수 있는 홈, 사이드 위젯
홈 위젯 편집 기능을 만든 이유
행사 등을 이유로 특집 영역을 구성하거나
새로운 기사 묶음 목록을 실험하고 싶을 때
매번 개발자가 고치려면 시간과 비용이 듭니다
최신 기사 같은 목록을 위젯으로 구성하고
직접 편집 가능하도록 만들어보았으며
하는 김에 광고 배너도 추가했습니다
지원하는 기능
WIDGET_CHOICES = {
'kind': [('article-list', '목록'),
('banner', '배너')],
'location': [('home', '홈'),
('side', '사이드')],
'order_by': [('latest', '최신순'),
('popular-1-week', '인기순(1주간)'),
('popular-2-week', '인기순(2주간)')],
'skin': [
('home-large-tile', '홈 큰 타일'),
('large-image-2-item', '큰 이미지(2)'),
('large-image-3-item', '큰 이미지(3)'),
('large-image-4-item', '큰 이미지(4)'),
('small-image-4-item', '작은 이미지(4)'),
('small-image-first-only', '첫 기사만 작은 이미지'),
('title-only', '제목만'),
]
}
모델 코드 일부
class Widget(SortableMixin, TimeStampedModel):
# 분류
site = models.ForeignKey(Site, verbose_name="사이트")
location = models.CharField('위치', choices=WIDGET_CHOICES['location'])
kind = models.CharField('구분', choices=WIDGET_CHOICES['kind'])
# 위젯 속성
title = models.CharField('제목')
title_url = models.URLField('제목 URL')
skin = models.CharField('스킨', choices=WIDGET_CHOICES['skin'])
size = models.IntegerField('최대 기사 숫자', default=5)
# 데이터 직접 입력
html = models.TextField('HTML 직접 입력')
data = JSONField('기사 직접 입력')
articles = models.ManyToManyField(Article, verbose_name='기사 연결')
# 필터
order_by = models.CharField('기사 정렬', choices=WIDGET_CHOICES['order_by'])
categories = models.ManyToManyField(Category, verbose_name='기사 분류 필터')
tags = TaggableManager(verbose_name='기사 태그 필터')
sort = models.PositiveIntegerField('위젯 순서')
모델에서 구현된 함수
def get_data(self, size=None):
# 길고 복잡하여 코드 생략
# 조건에 맞춰서 현재 표시해야할 데이터 생성
# 배너의 경우 html만 읽기
# 직접 적은 기사 내용
# 선택한 기사
# 조건에 맞는 기사 기사
def template_name(self):
if self.kind == 'banner':
return 'widgets/banner.html'
if self.kind == 'article-list' and self.skin:
return f'widgets/{self.skin}.html'
return 'widgets/title-only.html'
손쉽게 사용하기 위한 Manager
class WidgetManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('site') 
.prefetch_related('articles', 'tags', 'categories')
def all_by_location(self, site, location):
return [{'obj': i, 'data': i.get_data()} for i in
self.get_queryset().filter(site=site, location=location)]
@cache_widget('widget_side') # 연산이 많이 일어날 수 있어서 캐시 구현
def sides(self, site):
return self.all_by_location(site, 'side')
@cache_widget('widget_home')
def homes(self, site):
return self.all_by_location(site, 'home')
Widget.objects.homes() 간단하게 사용 할 수 있게 함
기자들이 사용하기 위한 Admin
@admin.register(Widget)
class WidgetAdmin(SortableAdmin):
formfield_overrides = {JSONField: {'widget': JSONEditor}, }
raw_id_fields = ['articles']
list_display = ['id', site_name, 'location', 'kind', 'ad_test',
'title', 'skin', 'size', 'order_by', 'sort']
list_filter = ['site', 'location', 'kind', 'ad_test']
list_editable = ['title', 'skin', 'size', 'order_by', ]
ordering = ['site', 'location', 'sort']
radio_fields = {
'site': admin.VERTICAL,
'kind': admin.HORIZONTAL, 'location': admin.HORIZONTAL,
}
filter_horizontal = ['categories', ]
Admin 쓸만하게
Admin 쓸만하게
기자들이 사용할 Admin을
Django 기본 기능으로만 구현
어떤 것들이 가능한지 간단한 소개
Admin 코드 일부
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
formfield_overrides = {models.TextField: {'widget': AdminToastEditorWidget}, }
list_select_related = ('site', 'category', 'category=_site')
list_display = [site_name, 'number', 'category_name', 'title_with_tags', 'reporter',
'hit_count', 'active', 'dates']
list_display_links = ['number']
list_per_page = 50
list_filter = ['site', 'category', 'active']
search_fields = ['title', 'reporter_name', 'reporter_email']
date_hierarchy = 'published'
ordering = ['-published']
readonly_fields = ['migrated', 'number']
change_form_template = 'admin/article/change_form.html'
actions = [export_as_csv, cache_clear]
Toast에디터를 기사 편집 툴로 사용
class AdminToastEditorWidget(widgets.AdminTextareaWidget):
template_name = 'forms/admin-toast-editor.html'
class Media:
css = {"all": (
"https:=/uicdn.toast.com/editor/2.5.2/toastui-editor.min.css",
"https:=/cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css",
)}
js = ("js/admin-toast-editor.js",)
Admin 커스텀 위젯 기능, toast 에디터를 읽는 js를 추가
기사 제목 필드에 여러 기능 한번에 넣기
html을 일부 하드코딩하여 기사 태그를 제목에 함께 표현
기사를 포털에 배포하는 기능과 배포된 기사를 검색하는 링크 추가
def title_with_tags(self, obj):
return safe_render("""
<strong><a href="{% url 'admin:article_article_change' obj.id %}">
{{ obj.title }}=/a>=/strong><br=>
{% for i in obj.tags.all %}<small>#{{ i.name }}=/small> {% endfor %}
<br=>
<a href="{% url 'admin:publish' obj.id %}">[배포 하기]=/a>
<a href="{% url 'admin:publish_publishedarticle_changelist' %}?article={{ obj.id }}">[배포 기록]=/a>
/ 검색:
<a href="https:=/search.naver.com/search.naver?query={{ obj.title|urlencode }}">[네이버]=/a>
<a href="https:=/search.daum.net/search?q={{ obj.title|urlencode }}">[다음]=/a>
<a href="http:=/search.zum.com/search.zum?query={{ obj.title|urlencode }}">[줌]=/a>
<a href="https:=/search.daum.net/nate?q={{ obj.title|urlencode }}">[네이트]=/a>
""", {"obj": obj})
title_with_tags.short_description = '제목 / 태그 / 배포'
title_with_tags.admin_order_field = 'title'
추가적인 액션 함수
# actions = [export_as_csv, cache_clear] 부분에서 사용
def export_as_csv(modeladmin, request, queryset):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="articles.csv"'
writer = csv.writer(response)
writer.writerow(['Number', 'URL', 'Title', 'Reporter', 'Published', 'Category'])
articles = queryset.select_related('site', 'category').only(
'site', 'number', 'title', 'reporter_name', 'published', 'category')
for article in articles:
writer.writerow((
article.number,
f'https:=/{article.site.domain}{reverse("article-detail", args=[article.number])}',
article.title, article.reporter_name,
date(article.published, 'Y-m-d H:i'), article.category.name))
return response
def cache_clear(modeladmin, request, queryset):
cache.clear()
3장
기사를 포털에 배포하기
간단한 검색 구현
데이터 이전 스크립트
더 필요한 것들
기사를 포털에 배포하기
기사를 포털에 배포하기
한국의 뉴스 사이트는 네이버, 다음 같은 포털에 기사를 배포함
대부분 XML 파일을 생성 후 각 포털 서버에 FTP로 전송
HTTP POST 방식으로 제공하는 곳 생겨나는 중
매체별로 기사 포멧을 변환 후 전송 방식에 맞춰 보내줘야함
현재 각 뉴스 사이트마다 4개의 포탈에 전송 중
Media 모델로 매체별 차이 대응
class Media(TimeStampedModel):
active = models.BooleanField('활성화')
# 식별
site = models.ForeignKey(Site, verbose_name='사이트')
code = models.CharField(verbose_name='매체 구분 코드')
title = models.CharField('매체명')
# 파일명
file_ext = models.CharField('파일명 확장자', choices=[('', '없음'), ('txt', 'txt'), ('xml', 'xml')])
file_name_prefix = models.CharField('파일명 프리픽스', max_length=20, default='', choices=[
('', '없음'), ('out', 'out'), ('news', 'news'),
('itdonga_', 'itdonga_'), ('gamedonga_', 'gamedonga_')])
file_name_type = models.CharField('파일명 타입', choices=[
('number', '{기사번호}'), ('yyyymmdd-8-number', '{년}{월}{일}{기사번호8자}')])
# 내용
encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[
('euc-kr', 'EUC-KR'), ('utf-8', 'UTF-8')])
publish_type = models.CharField('배포 유형', max_length=50, choices=PublishType.choices)
extra = JSONField('추가 데이터', help_text='각 설정마다 필요한 추가 값을 JSON 형태로 기록합니다.')
template = models.TextField('템플릿', help_text='기사 생성시 사용하는 템플릿입니다.')
PublishedArticle 모델로 변환된 기사 내용과 이력을 남김
class PublishedArticle(TimeStampedModel):
media = models.ForeignKey(Media, verbose_name='매체', on_delete=models.PROTECT, db_index=True)
article = models.ForeignKey(Article, verbose_name='기사', on_delete=models.SET_NULL,
blank=True, null=True, db_index=True)
state = models.CharField('상태', max_length=6, default='new', blank=True, choices=[
('new', '신규'),
('update', '수정'),
('delete', '삭제'),
])
file_name = models.CharField('파일명', default='', max_length=50, blank=True)
content = models.TextField('변환된 기사 내용', default='', blank=True)
encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[
('euc-kr', 'EUC-KR'),
('utf-8', 'UTF-8'),
], blank=True)
배포 함수
# 복잡한 세부 함수들은 생략
def publish(self, article, state='new'):
pa = PublishedArticle.objects.create(
**{'media': self, 'article': article, 'state': state, 'encoding': self.encoding})
tags = [tag.name for tag in article.tags.all()]
pa.file_name = pa.make_file_name()
pa.content = Template(self.template).render(Context({
'obj': pa,
'article': article,
'tags': tags,
'state': pa.converted_state(state),
'site': article.site,
**self.get_codes(article, tags, pa.media.extra.get('tag', {})),
}))
pa.save(update_fields=['content', 'file_name'])
# 기사 변환 후 FTP 업로드 등의 절차를 매체에 따라 수행
self.after_publish(pa)
return pa
간단한 검색 구현
간단한 검색 구현
풀 텍스트 서치를 구현하는 것은 많은 비용이 듬
간단한 검색 쿼리에도 DB가 많이 느려질 수 있음
Django에 Postgresql을 쓸 경우 SearchVetor를 사용 가능
단어를 미리 잘라서 저장해놓고 검색
한글은 잘 안되긴 하지만 적당히 쓸만함
간단한 검색 구현
# Article 모델에 필드 추가
search_vector = SearchVectorField(null=True, blank=True)
# Signal 추가
@receiver(post_save, sender=Article)
def article_update_search_vector(sender, instance, created, **kwargs):
update_fields = kwargs.get('update_fields')
# update_fields에 직접 지정된 경우를 피해서 루프도는 것을 방지
if not update_fields or 'search_vector' not in update_fields:
obj = Article.objects.annotate(document=search_vector).get(id=instance.id)
obj.search_vector = obj.document
obj.save(update_fields=['search_vector'])
간단한 검색 구현
# 검색 쿼리
search_query = reduce(
operator.and_,
(SearchQuery(f'{x}:*', search_type='raw') for x in query.split(' '))
)
# 검색할 쿼리를 분할해서 가중치로 넣고 정렬
rank = SearchRank(search_vector, search_query, weights=[0.4, 0.6, 0.8, 1.0])
# 현재 사이트에서는 점수보다 배포 시간을 중시
return queryset.filter(search_vector=search_query) 
.annotate(rank=rank).filter(rank=_gte=0.3) 
.order_by('-published', '-rank').distinct()
데이터 이전 스크립트
데이터 이전 스크립트
하나의 회사에서 제공하는 두개의 뉴스 사이트
기능은 거의 같았으나 각각 앱과 데이터베이스가 나뉘어 있었음
하나의 앱으로 합쳐서 관리 부담을 덜고자 함
데이터 이전 시 놓치거나 잘못 들어간 필드는 치명적
데이터 제어, 가공, 매치 부분을 분할하여
코드를 쉽게 읽을 수 있는 데이터 이전 스크립트를 작성
실행은 Django Management Commands 사용
class Command(BaseCommand):
help = '예전 데이터베이스를 새 데이터베이스로 마이그레이션'
def add_arguments(self, parser):
parser.add_argument('=-count', dest='count', action='store', type=int)
def handle(self, *args, **options):
count = options['count'] if options['count'] else None
# Mover 클래스를 만들어서 제어
mover = Mover('IT동아')
mover.migrate(count)
mover = Mover('게임동아')
mover.migrate(count)
기존 데이터 DB는 Django에서 읽을 수 있게 manage inspectdb 명령으로 가져옴
Mover 클래스
class Mover:
def =_init=_(self, site_name):
self.site = Site.objects.get(name=site_name)
self.categories = [{'code': i.code, 'category': i} for i in Category.objects.filter(site=self.site)]
def get_xxx():
# 달라진 분류, 필드 내용 등을 가공하는 함수들
def migrate(self, count=None):
model, db_name = (GameArticle, 'legacy-game') if self.site.name == '게임동아' else (ItArticle, 'legacy-it')
queryset = model.objects.using(db_name).all().order_by("updated", "id")
if count:
queryset = queryset[:count]
else:
last_migrated = Article.objects.filter(site=self.site, migrated=_isnull=False).latest('migrated')
queryset = queryset.filter(updated=_gte=last_migrated.migrated)
pbar = tqdm(total=queryset.count(), desc=self.site.name, mininterval=2, miniters=1, ncols=0)
for old in queryset.iterator():
self.make_new_from_old(old)
pbar.update(1)
pbar.close()
필드 매칭은 명시적, 가공은 별도의 함수에서
def make_new_from_old(self, old):
params = {
'reporter_name': old.user_name if old.user_name else '',
'reporter_email': old.user_email if old.user_email else '',
'title': old.title,
'contents_md': old_html_to_new_md(old.contents) if old.contents else '',
'contents_html': old.contents,
'intro': old.intro,
'thumbnail': old.thumbnail,
'category': category,
'hit_count': old.count_view,
'published': created,
'created': created,
'modified': created if self.site.name == '게임동아' and old.id < 60000 else updated,
'migrated': updated,
}
new, _ = Article.objects.update_or_create(site=self.site, number=old.id, defaults=params)
new.tags.add(*tags)
마무리
요약하면
- Docker-compose로 개발 환경 설정
- CBV로 코드 재활용
- Test 코드로 검증
- Sites, Cache, SearchVector, Admin 등
Django 기본 기능 사용
- 복잡한 일은 단계를 나눠서 작성
이것보다 많은 코드가 있지만
시간 관계상 이 정도만 소개합니다
회사 홍보
엔라이즈는 사람과 사람, 사람과 콘텐츠를
연결하여 변화를 만듭니다.
위피: 동네 기반 소셜 데이팅 앱
콰트: 하루 10분 운동 습관 만드는 홈트레이닝
두 가지 서비스를 더 잘하기 위해 뭔가 해낼 것
같은 사람들이 점점 더 모이고 있습니다.
입사한지 반년도 되지 않았지만, 각자가 자신을
드러내며 더 잘하고자 하는 재미있는 회사입니다.
https://nrise.net/
NRISE에서 동료를 구합니다
감사합니다.
질문: perhapsspy@gmail.com

More Related Content

What's hot

2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축
2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축
2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축Youngil Cho
 
도메인 주도 설계의 본질
도메인 주도 설계의 본질도메인 주도 설계의 본질
도메인 주도 설계의 본질Young-Ho Cho
 
Django의 배신(주니어 개발자의 Django 삽질기)
Django의 배신(주니어 개발자의 Django 삽질기)Django의 배신(주니어 개발자의 Django 삽질기)
Django의 배신(주니어 개발자의 Django 삽질기)Eunhyang Kim
 
Introduction to Node js
Introduction to Node jsIntroduction to Node js
Introduction to Node jsAkshay Mathur
 
Line Entry의 Atomic Design 적용기
Line Entry의 Atomic Design 적용기Line Entry의 Atomic Design 적용기
Line Entry의 Atomic Design 적용기NAVER Engineering
 
Open source APM Scouter로 모니터링 잘 하기
Open source APM Scouter로 모니터링 잘 하기Open source APM Scouter로 모니터링 잘 하기
Open source APM Scouter로 모니터링 잘 하기GunHee Lee
 
Web development with django - Basics Presentation
Web development with django - Basics PresentationWeb development with django - Basics Presentation
Web development with django - Basics PresentationShrinath Shenoy
 
Introduction to Node.js
Introduction to Node.jsIntroduction to Node.js
Introduction to Node.jsRob O'Doherty
 
Build RESTful API Using Express JS
Build RESTful API Using Express JSBuild RESTful API Using Express JS
Build RESTful API Using Express JSCakra Danu Sedayu
 
웹 프로그래밍 팀프로젝트 최종발표
웹 프로그래밍 팀프로젝트 최종발표웹 프로그래밍 팀프로젝트 최종발표
웹 프로그래밍 팀프로젝트 최종발표Seong Heum Park
 
Angular 10 course_content
Angular 10 course_contentAngular 10 course_content
Angular 10 course_contentNAVEENSAGGAM1
 
Introduction to Django
Introduction to DjangoIntroduction to Django
Introduction to DjangoJames Casey
 
Python 테스트 시작하기
Python 테스트 시작하기Python 테스트 시작하기
Python 테스트 시작하기Hosung Lee
 
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...Edureka!
 
Intro to Node.js (v1)
Intro to Node.js (v1)Intro to Node.js (v1)
Intro to Node.js (v1)Chris Cowan
 
mongodb와 mysql의 CRUD 연산의 성능 비교
mongodb와 mysql의 CRUD 연산의 성능 비교mongodb와 mysql의 CRUD 연산의 성능 비교
mongodb와 mysql의 CRUD 연산의 성능 비교Woo Yeong Choi
 

What's hot (20)

2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축
2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축
2017 Pycon KR - Django/AWS 를 이용한 쇼핑몰 서비스 구축
 
도메인 주도 설계의 본질
도메인 주도 설계의 본질도메인 주도 설계의 본질
도메인 주도 설계의 본질
 
Django의 배신(주니어 개발자의 Django 삽질기)
Django의 배신(주니어 개발자의 Django 삽질기)Django의 배신(주니어 개발자의 Django 삽질기)
Django의 배신(주니어 개발자의 Django 삽질기)
 
Node js introduction
Node js introductionNode js introduction
Node js introduction
 
Introduction to Node js
Introduction to Node jsIntroduction to Node js
Introduction to Node js
 
Line Entry의 Atomic Design 적용기
Line Entry의 Atomic Design 적용기Line Entry의 Atomic Design 적용기
Line Entry의 Atomic Design 적용기
 
Open source APM Scouter로 모니터링 잘 하기
Open source APM Scouter로 모니터링 잘 하기Open source APM Scouter로 모니터링 잘 하기
Open source APM Scouter로 모니터링 잘 하기
 
Web development with django - Basics Presentation
Web development with django - Basics PresentationWeb development with django - Basics Presentation
Web development with django - Basics Presentation
 
Introduction to Node.js
Introduction to Node.jsIntroduction to Node.js
Introduction to Node.js
 
Build RESTful API Using Express JS
Build RESTful API Using Express JSBuild RESTful API Using Express JS
Build RESTful API Using Express JS
 
Why Vue.js?
Why Vue.js?Why Vue.js?
Why Vue.js?
 
웹 프로그래밍 팀프로젝트 최종발표
웹 프로그래밍 팀프로젝트 최종발표웹 프로그래밍 팀프로젝트 최종발표
웹 프로그래밍 팀프로젝트 최종발표
 
Angular 10 course_content
Angular 10 course_contentAngular 10 course_content
Angular 10 course_content
 
Angular Directives
Angular DirectivesAngular Directives
Angular Directives
 
Introduction to Django
Introduction to DjangoIntroduction to Django
Introduction to Django
 
Python 테스트 시작하기
Python 테스트 시작하기Python 테스트 시작하기
Python 테스트 시작하기
 
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...
Node.js Tutorial for Beginners | Node.js Web Application Tutorial | Node.js T...
 
What’s New in Angular 14?
What’s New in Angular 14?What’s New in Angular 14?
What’s New in Angular 14?
 
Intro to Node.js (v1)
Intro to Node.js (v1)Intro to Node.js (v1)
Intro to Node.js (v1)
 
mongodb와 mysql의 CRUD 연산의 성능 비교
mongodb와 mysql의 CRUD 연산의 성능 비교mongodb와 mysql의 CRUD 연산의 성능 비교
mongodb와 mysql의 CRUD 연산의 성능 비교
 

Similar to Django를 Django답게, Django로 뉴스 사이트 만들기

Java script 강의자료_ed13
Java script 강의자료_ed13Java script 강의자료_ed13
Java script 강의자료_ed13hungrok
 
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지Html5 앱과 웹사이트를 보다 빠르게 하는 50가지
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지yongwoo Jeon
 
파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 Yong Joon Moon
 
다시보는 Angular js
다시보는 Angular js다시보는 Angular js
다시보는 Angular jsJeado Ko
 
XE Open seminar 테마만들기
XE Open seminar 테마만들기XE Open seminar 테마만들기
XE Open seminar 테마만들기Sungbum Hong
 
ReactJS | 서버와 클라이어트에서 동시에 사용하는
ReactJS | 서버와 클라이어트에서 동시에 사용하는ReactJS | 서버와 클라이어트에서 동시에 사용하는
ReactJS | 서버와 클라이어트에서 동시에 사용하는Taegon Kim
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기우영 주
 
알아봅시다, Polymer: Web Components & Web Animations
알아봅시다, Polymer: Web Components & Web Animations알아봅시다, Polymer: Web Components & Web Animations
알아봅시다, Polymer: Web Components & Web AnimationsChang W. Doh
 
[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱NAVER D2
 
자바스크립트 프레임워크 살펴보기
자바스크립트 프레임워크 살펴보기자바스크립트 프레임워크 살펴보기
자바스크립트 프레임워크 살펴보기Jeado Ko
 
Facebook은 React를 왜 만들었을까?
Facebook은 React를 왜 만들었을까? Facebook은 React를 왜 만들었을까?
Facebook은 React를 왜 만들었을까? Kim Hunmin
 
Spring Boot + React + Gradle in VSCode
Spring Boot + React + Gradle in VSCodeSpring Boot + React + Gradle in VSCode
Spring Boot + React + Gradle in VSCodedpTablo
 
Isomorphicspring Isomorphic - spring web seminar 2015
Isomorphicspring Isomorphic - spring web seminar 2015Isomorphicspring Isomorphic - spring web seminar 2015
Isomorphicspring Isomorphic - spring web seminar 2015sung yong jung
 
Python codelab2
Python codelab2Python codelab2
Python codelab2건희 김
 

Similar to Django를 Django답게, Django로 뉴스 사이트 만들기 (20)

Django View Part 1
Django View Part 1Django View Part 1
Django View Part 1
 
Java script 강의자료_ed13
Java script 강의자료_ed13Java script 강의자료_ed13
Java script 강의자료_ed13
 
[Codelab 2017] ReactJS 기초
[Codelab 2017] ReactJS 기초[Codelab 2017] ReactJS 기초
[Codelab 2017] ReactJS 기초
 
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지Html5 앱과 웹사이트를 보다 빠르게 하는 50가지
Html5 앱과 웹사이트를 보다 빠르게 하는 50가지
 
파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 파이썬 플라스크 이해하기
파이썬 플라스크 이해하기
 
다시보는 Angular js
다시보는 Angular js다시보는 Angular js
다시보는 Angular js
 
XE Open seminar 테마만들기
XE Open seminar 테마만들기XE Open seminar 테마만들기
XE Open seminar 테마만들기
 
ReactJS | 서버와 클라이어트에서 동시에 사용하는
ReactJS | 서버와 클라이어트에서 동시에 사용하는ReactJS | 서버와 클라이어트에서 동시에 사용하는
ReactJS | 서버와 클라이어트에서 동시에 사용하는
 
Hacosa jquery 1th
Hacosa jquery 1thHacosa jquery 1th
Hacosa jquery 1th
 
컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기컴포넌트 관점에서 개발하기
컴포넌트 관점에서 개발하기
 
알아봅시다, Polymer: Web Components & Web Animations
알아봅시다, Polymer: Web Components & Web Animations알아봅시다, Polymer: Web Components & Web Animations
알아봅시다, Polymer: Web Components & Web Animations
 
[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱[114]angularvs react 김훈민손찬욱
[114]angularvs react 김훈민손찬욱
 
자바스크립트 프레임워크 살펴보기
자바스크립트 프레임워크 살펴보기자바스크립트 프레임워크 살펴보기
자바스크립트 프레임워크 살펴보기
 
Facebook은 React를 왜 만들었을까?
Facebook은 React를 왜 만들었을까? Facebook은 React를 왜 만들었을까?
Facebook은 React를 왜 만들었을까?
 
react-ko.pdf
react-ko.pdfreact-ko.pdf
react-ko.pdf
 
Spring Boot + React + Gradle in VSCode
Spring Boot + React + Gradle in VSCodeSpring Boot + React + Gradle in VSCode
Spring Boot + React + Gradle in VSCode
 
Isomorphicspring Isomorphic - spring web seminar 2015
Isomorphicspring Isomorphic - spring web seminar 2015Isomorphicspring Isomorphic - spring web seminar 2015
Isomorphicspring Isomorphic - spring web seminar 2015
 
4-3. jquery
4-3. jquery4-3. jquery
4-3. jquery
 
Python codelab2
Python codelab2Python codelab2
Python codelab2
 
3-2. selector api
3-2. selector api3-2. selector api
3-2. selector api
 

More from Kyoung Up Jung

Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdf
Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdfDjango 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdf
Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdfKyoung Up Jung
 
OK, 계획대로 되고 있어?
OK, 계획대로 되고 있어?OK, 계획대로 되고 있어?
OK, 계획대로 되고 있어?Kyoung Up Jung
 
테스트가 뭐예요?
테스트가 뭐예요?테스트가 뭐예요?
테스트가 뭐예요?Kyoung Up Jung
 
Django를 배우다, Django로 배우다.
Django를 배우다, Django로 배우다.Django를 배우다, Django로 배우다.
Django를 배우다, Django로 배우다.Kyoung Up Jung
 
어른스럽게 일하기
어른스럽게 일하기어른스럽게 일하기
어른스럽게 일하기Kyoung Up Jung
 
신입에서 CTO까지, 야근하지 않는 웹개발
신입에서 CTO까지, 야근하지 않는 웹개발신입에서 CTO까지, 야근하지 않는 웹개발
신입에서 CTO까지, 야근하지 않는 웹개발Kyoung Up Jung
 
웹 개발, 왜 어려운가?
웹 개발, 왜 어려운가?웹 개발, 왜 어려운가?
웹 개발, 왜 어려운가?Kyoung Up Jung
 
Django ORM 왜 어렵게 느껴질까?
Django ORM 왜 어렵게 느껴질까?Django ORM 왜 어렵게 느껴질까?
Django ORM 왜 어렵게 느껴질까?Kyoung Up Jung
 
뭔지 모르지만 발표
뭔지 모르지만 발표뭔지 모르지만 발표
뭔지 모르지만 발표Kyoung Up Jung
 
Django개발은 PyCharm에서
Django개발은 PyCharm에서Django개발은 PyCharm에서
Django개발은 PyCharm에서Kyoung Up Jung
 

More from Kyoung Up Jung (11)

Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdf
Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdfDjango 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdf
Django 봄은 다시 온다 - Django와 함께 좋은 웹서비스 코드 만들기.pdf
 
NRISE에서 3개월
NRISE에서 3개월NRISE에서 3개월
NRISE에서 3개월
 
OK, 계획대로 되고 있어?
OK, 계획대로 되고 있어?OK, 계획대로 되고 있어?
OK, 계획대로 되고 있어?
 
테스트가 뭐예요?
테스트가 뭐예요?테스트가 뭐예요?
테스트가 뭐예요?
 
Django를 배우다, Django로 배우다.
Django를 배우다, Django로 배우다.Django를 배우다, Django로 배우다.
Django를 배우다, Django로 배우다.
 
어른스럽게 일하기
어른스럽게 일하기어른스럽게 일하기
어른스럽게 일하기
 
신입에서 CTO까지, 야근하지 않는 웹개발
신입에서 CTO까지, 야근하지 않는 웹개발신입에서 CTO까지, 야근하지 않는 웹개발
신입에서 CTO까지, 야근하지 않는 웹개발
 
웹 개발, 왜 어려운가?
웹 개발, 왜 어려운가?웹 개발, 왜 어려운가?
웹 개발, 왜 어려운가?
 
Django ORM 왜 어렵게 느껴질까?
Django ORM 왜 어렵게 느껴질까?Django ORM 왜 어렵게 느껴질까?
Django ORM 왜 어렵게 느껴질까?
 
뭔지 모르지만 발표
뭔지 모르지만 발표뭔지 모르지만 발표
뭔지 모르지만 발표
 
Django개발은 PyCharm에서
Django개발은 PyCharm에서Django개발은 PyCharm에서
Django개발은 PyCharm에서
 

Django를 Django답게, Django로 뉴스 사이트 만들기

  • 2. 자기 소개 NRISE Backend Chapter Leader (현재) ODK Media Backend Leader (3년) 프리랜서 (1년), 혜움세무회계 CTO 4개월 포함 SmartStudy Software engineer (3년 반) 아이티동아 Developer (4년) 정경업 (파이)
  • 3. 발표 주제를 정하기까지 지금까지 Django 사용법에서 시작하여 자아성찰까지 여러 주제 발표 했었음 흔한 고민 이번엔 뭘하지…? 처음으로 돌아가 사용법에 대해 가볍게 이야기 해볼까? Django는 뉴스 CMS로 시작된 프레임워크 마침 외주로 아이티동아, 게임동아 뉴스 사이트를 재개발 발표 해보자!
  • 4. 요구사항과 구현 목표 기본적인 뉴스 사이트 기능 ● 홈, 사이드 위젯 구성과 기사 읽기, 목록, 작성, 포털 배포 기존 데이터 이전 한가지 앱으로 두 사이트 운영 유지 보수 최소화(외주) 기자들이 직접 홈과 위젯을 편집 가능
  • 6. 프로젝트 레이아웃 api : API 구현 코드 분리 app : 프로젝트 메인에 관련 설정 파일 article : 기사 구현 home : 홈 화면 및 사이드 위젯 등 공용 코드 migrator : 데이터 이전 작업 publish : 기사 배포 관련 search : 검색 docker-compose.yml 배포 관련 스크립트
  • 7. Docker-compose # 많이 생략된 대략적인 docker-compose.yml services: postgres: image: postgres django: build: dockerfile: ./docker/django.dockerfile command: python3 manage.py runserver 0.0.0.0:8000 volumes: - ./app:/usr/src/web/app ports: - "80:8000" depends_on: - postgres pure-ftpd: # FTP 테스트 용 image: stilliard/pure-ftpd
  • 8. Docker-compose Docker-compose를 개발환경에서 쓰면 ● DB 등 필요한 서비스를 손쉽게 관리할 수 있음 ● 파이썬 라이브러리 완전히 독립적 ● 다 까먹어도 'up'명령 한줄이면 개발 환경 구축 # 대충 적은 사용 법 docker-compose build # 이미지 생성 docker-compose up # 서비스 실행 docker-compose down # 서비스 내리기 docker-compose run django bash # shell(bash) 접근
  • 9. IDE(Pycharm) 연동 Pycharm Settings ● Python Interpreter - Docker-compose ● Language & Frameworks - Django ● Run/Debug Configrations - Django Server, Django tests IDE에서 바로 테스트 실행 가능
  • 10. 1장 Sites로 멀티 도메인 구현 Article 모델 확장과 Test코드 CBV로 기사 목록, 읽기 구현 돌아가는 기반
  • 12. 한 앱으로 두 사이트 만드는 방법 # models.py from django.contrib.sites.models import Site class Article(TimeStampedModel): site = models.ForeignKey(Site, on_delete=models.PROTECT, db_index=True) title = models.CharField(max_length=250, db_index=True) # views.py from django.contrib.sites.shortcuts import get_current_site def list_view(request): articles = Article.objects.filter(site=get_current_site(request))
  • 13. 한 앱으로 두 사이트 만드는 방법 내장된 Sites 모델과 get_current_site 함수로 간단하게 구현 ● 실제 적용된 내용은 Category - Article 모델 관계 Sites 내용은 Fixture로 만들어놓고 테스트 코드 및 배포시 사용 가능 - model: sites.site pk: 1 fields: domain: it.donga.com name: IT동아 - model: sites.site pk: 2 fields: domain: game.donga.com name: 게임동아
  • 15. Manager로 배포 상태의 기사만 다루기 def live_q(): q = Q(published=_lte=timezone.now()) | Q(published=_isnull=True) return q & Q(active=True) class LiveManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(live_q()) class Article(TimeStampedModel): # managers live_objects = LiveManager() objects = models.Manager() live_objects를 쓰면 배포 상태의 기사만 가져오기 쉽다 objects는 Admin에서 쓰이므로 유지
  • 16. Manager로 배포 상태의 기사만 다루기 # tests.py class TestArticleModels(TestCase): def test_live(self): # 모델 인스턴스 목 생성 baker.make(Article, active=True) # 유효 baker.make(Article, active=False) # 무효 # 전체 기사는 2개지만 배포 기사는 1개인 것을 Test 로 검증 self.assertEqual(Article.objects.count(), 2) self.assertEqual(Article.live_objects.count(), 1)
  • 17. 간단한 카운트 함수와 테스트 코드 기사를 읽었을 때 카운트가 올라가는 기능을 Model에 구현 # models.py class Article(TimeStampedModel): def hit(self): Article.objects.filter(id=self.pk).update(hit_count=F("hit_count") + 1)
  • 18. 간단한 카운트 함수와 테스트 코드 작동하는지 확인하는 간단한 Test 작성 # tests.py class TestArticleModels(TestCase): def test_hit(self): article = baker.make(Article) self.assertEqual(article.hit_count, 0) article.hit() # +1 article.refresh_from_db() # DB에서 값을 다시 읽기 self.assertEqual(article.hit_count, 1) article.hit() article.hit() # +2 article.refresh_from_db() self.assertEqual(article.hit_count, 3)
  • 19. Signal로 기사 번호 만들기 @receiver(post_save, sender=Article) def article_auto_number(sender, instance, created, **kwargs): if not created: return if instance.number > 0: return queryset = sender.objects.filter(site=instance.site) max_number = queryset.aggregate(Max("number"))["number=_max"] queryset.filter(id=instance.id).update(number=max_number + 1) 두 사이트의 기사 숫자가 차이나고 기존 데이터를 가져올 때 옮겨오기 쉽게 하려고 ID를 그냥 쓰지 못하고 number를 만듬
  • 20. Signal로 기사 번호 만들기 class TestArticleModels(TestCase): def test_auto_number(self): a_1 = baker.make(Article, site=self.site_a) # a, number 1 a_7 = baker.make(Article, site=self.site_a, number=7) # a, number 7 a_1.refresh_from_db() a_7.refresh_from_db() self.assertEqual(a_1.number, 1) self.assertEqual(a_7.number, 7) baker.make(Article, site=self.site_b) # b, number 1 baker.make(Article, site=self.site_b) # b, number 2 b_3 = baker.make(Article, site=self.site_b) # b, number 3 b_3.refresh_from_db() self.assertEqual(b_3.number, 3) 값이 생성될 때마다 post_save signal이 실행되는지 확인
  • 21. CBV로 기사 목록, 읽기 구현
  • 22. Function Based View VS Class Based View Function Based View ● 절차적으로 작성하여 코드가 섞이기 쉬우며 ● 기능을 찾아내기 어려운 코드가 되어 ● 재활용도 덜 신경쓰게 되는걸 자주 봅니다 Class Based View ● 역할이 명시적으로 나뉘어 코드가 덜 섞이며 ● 있는 기능을 잘 쓰는 법을 찾아야 하지만 ● 재활용 하기 쉬워 적은 양의 코드로 많은 구현을 할 수 있습니다
  • 23. 기사 목록 class ArticleListView(BaseSideMixin, PageNumbersMixin, ListView): paginate_by = 10 template_name = 'article/list.html' category = None # get_queryset에서 할당 후 get_context_data에서 사용 def get_queryset(self): self.category = get_object_or_404( Category, site=get_current_site(self.request), code=self.kwargs['category_code']) return Article.live_objects.filter(category=self.category).only(*ARTICLE_LIST_FIELDS) def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) data.update({'title': self.category.name, 'category': self.category}) return data
  • 24. 기사 목록 상속 받은 클래스 ● ListView : Django에서 제공하는 기본 클래스 ● BaseSideMixin : 사이트의 사이드바를 구현 (후술) ● PageNumbersMixin : 페이지 번호 방식을 변경하기 위한 구현 속성, 함수 ● paginate_by : 페이지당 개수 ● template_name : View에서 사용할 템플릿 이이름 ● get_queryset : Django ORM에서 가져올 목록용 쿼리, 분류를 함께 구현 ● get_context_data : 템플릿에 보여질 context 생성
  • 25. 기사 읽기 class ArticleDetailView(BaseSideMixin, DetailView): template_name = 'article/detail.html' slug_url_kwarg = 'number' slug_field = 'number' def get_queryset(self): site = get_current_site(self.request) return Article.live_objects.filter(site=site).select_related('site', 'category') def get_context_data(self, **kwargs): self.object.hit() # 기사 조회수 증가 data = super().get_context_data(**kwargs) data['tag_names'] = self.object.tags.values_list('name', flat=True) return data
  • 26. 2장 캐시 적용 및 관리 모양을 바꿀 수 있는 홈, 사이드 위젯 Admin 쓸만하게 운영에 필요한 것들
  • 28. 캐시 적용 및 관리 웹 서비스 대부분의 부하는 DB이며 DB 쿼리를 최대한 덜하도록 캐시를 많이 씁니다 캐시를 적용하는 것은 Django에서 손쉽게 가능하나 관리는 알아서 해야 합니다 약간의 코드로 관리할 수 있게 구현 해보았습니다
  • 29. 일원화된 캐시 키 관리 settings에서 어떤 캐시가 있는지 확인할 수 있게 정리 # settings.py CACHE_KEYS = { 'article': [ 'article_links', 'article_footer', 'article_content', ], 'home': [ 'widget_side', 'widget_home', ], 'text_page': [ 'text_detail' ] }
  • 30. 템플릿에 캐시 적용하기 기사 상단 {% load cache %} {% cache 7200 article_links object.id %} {% if object_prev %} <link rel="prev" title="{{ object_prev.title }}" href="{{ object_prev.get_absolute_url }}"> {% endif %} {% if object_next %} <link rel="next" title="{{ object_next.title }}" href="{{ object_next.get_absolute_url }}"> {% endif %} <meta property="og:url" content="{{ request.build_absolute_uri }}"=> <meta property="og:type" content="website"=> <meta property="og:title" content="{{ object.title }}"=> <meta property="og:description" content="{{ object.intro }}"=> <meta property="og:image" content="{{ object.thumbnail|convert_image_url:site }}"=> {% endcache %}
  • 31. 템플릿에 캐시 적용하기 기사 본문 {% cache 7200 article_content object.id %} <header> <h1>{{ object.title }}=/h1> {% include 'article/_tags.html' %} <strong>{{ object.reporter_name }}=/strong> <a href="{% url 'search-list' %}?reporter_name={{ object.reporter_name }}"><i class="fas fa-search">=/i>=/a> <a href="mailto:{{ object.reporter_email }}">{{ object.reporter_email }}=/a> {% include '_time.html' with time=object.published %} =/header> {{ object.contents|convert_html_image_url:site|safe }} {% include 'article/_tags.html' %} {% endcache %}
  • 32. 템플릿에 캐시 적용하기 기사 하단 {% cache 7200 article_footer object.id %} <div class="widget related-articles"> <h5>관련 기사=/h5> <ul>{% for i in object.related_articles %} <li><a href="{% url 'article-detail' i.number %}">{{ i.title }}=/a>=/li> {% empty %}<li class="list-group-item">아직 관련 기사가 없습니다.=/li>{% endfor %} =/ul> =/div> <div> {% if prev_number %}<a class="btn" href="{% url 'article-detail' prev_number %}">이전=/a>{% endif %} <a class="btn" href="">위로=/a> <a class="btn" href="{% url 'article-list' object.category.code %}">목록=/a> {% if next_number %}<a class="btn" href="{% url 'article-detail' next_number %}">다음=/i>=/a>{% endif %} =/div> {% endcache %}
  • 33. 기사 작성 및 수정시마다 캐시 업데이트 Signal 사용 ● settings에서 어떤 캐시가 있는지 확인 가능하고 ● 데이터 업데이트 시점에 Signal로 캐시를 최신으로 유지 ● 뉴스 기사 생성 수가 그리 크지 않으므로 가볍게 선택 가능한 전략 @receiver(post_save, sender=Article) def article_cache_clear(sender, instance, created, **kwargs): # settings에서 키 값을 가져옴 for name in settings.CACHE_KEYS['article']: key = make_template_fragment_key(name, [instance.id]) cache.delete(key) # 기사 내용이 다른 캐시에서도 쓰이는 경우 찾아서 삭제 cache.delete_many([f'{name}_{instance.site.id}' for name in settings.CACHE_KEYS['home']]) cache.delete(f'{instance.site.domain}_last_published')
  • 34. 모양을 바꿀 수 있는 홈, 사이드 위젯
  • 35. 홈 위젯 편집 기능을 만든 이유 행사 등을 이유로 특집 영역을 구성하거나 새로운 기사 묶음 목록을 실험하고 싶을 때 매번 개발자가 고치려면 시간과 비용이 듭니다 최신 기사 같은 목록을 위젯으로 구성하고 직접 편집 가능하도록 만들어보았으며 하는 김에 광고 배너도 추가했습니다
  • 36. 지원하는 기능 WIDGET_CHOICES = { 'kind': [('article-list', '목록'), ('banner', '배너')], 'location': [('home', '홈'), ('side', '사이드')], 'order_by': [('latest', '최신순'), ('popular-1-week', '인기순(1주간)'), ('popular-2-week', '인기순(2주간)')], 'skin': [ ('home-large-tile', '홈 큰 타일'), ('large-image-2-item', '큰 이미지(2)'), ('large-image-3-item', '큰 이미지(3)'), ('large-image-4-item', '큰 이미지(4)'), ('small-image-4-item', '작은 이미지(4)'), ('small-image-first-only', '첫 기사만 작은 이미지'), ('title-only', '제목만'), ] }
  • 37. 모델 코드 일부 class Widget(SortableMixin, TimeStampedModel): # 분류 site = models.ForeignKey(Site, verbose_name="사이트") location = models.CharField('위치', choices=WIDGET_CHOICES['location']) kind = models.CharField('구분', choices=WIDGET_CHOICES['kind']) # 위젯 속성 title = models.CharField('제목') title_url = models.URLField('제목 URL') skin = models.CharField('스킨', choices=WIDGET_CHOICES['skin']) size = models.IntegerField('최대 기사 숫자', default=5) # 데이터 직접 입력 html = models.TextField('HTML 직접 입력') data = JSONField('기사 직접 입력') articles = models.ManyToManyField(Article, verbose_name='기사 연결') # 필터 order_by = models.CharField('기사 정렬', choices=WIDGET_CHOICES['order_by']) categories = models.ManyToManyField(Category, verbose_name='기사 분류 필터') tags = TaggableManager(verbose_name='기사 태그 필터') sort = models.PositiveIntegerField('위젯 순서')
  • 38. 모델에서 구현된 함수 def get_data(self, size=None): # 길고 복잡하여 코드 생략 # 조건에 맞춰서 현재 표시해야할 데이터 생성 # 배너의 경우 html만 읽기 # 직접 적은 기사 내용 # 선택한 기사 # 조건에 맞는 기사 기사 def template_name(self): if self.kind == 'banner': return 'widgets/banner.html' if self.kind == 'article-list' and self.skin: return f'widgets/{self.skin}.html' return 'widgets/title-only.html'
  • 39. 손쉽게 사용하기 위한 Manager class WidgetManager(models.Manager): def get_queryset(self): return super().get_queryset().select_related('site') .prefetch_related('articles', 'tags', 'categories') def all_by_location(self, site, location): return [{'obj': i, 'data': i.get_data()} for i in self.get_queryset().filter(site=site, location=location)] @cache_widget('widget_side') # 연산이 많이 일어날 수 있어서 캐시 구현 def sides(self, site): return self.all_by_location(site, 'side') @cache_widget('widget_home') def homes(self, site): return self.all_by_location(site, 'home') Widget.objects.homes() 간단하게 사용 할 수 있게 함
  • 40. 기자들이 사용하기 위한 Admin @admin.register(Widget) class WidgetAdmin(SortableAdmin): formfield_overrides = {JSONField: {'widget': JSONEditor}, } raw_id_fields = ['articles'] list_display = ['id', site_name, 'location', 'kind', 'ad_test', 'title', 'skin', 'size', 'order_by', 'sort'] list_filter = ['site', 'location', 'kind', 'ad_test'] list_editable = ['title', 'skin', 'size', 'order_by', ] ordering = ['site', 'location', 'sort'] radio_fields = { 'site': admin.VERTICAL, 'kind': admin.HORIZONTAL, 'location': admin.HORIZONTAL, } filter_horizontal = ['categories', ]
  • 42. Admin 쓸만하게 기자들이 사용할 Admin을 Django 기본 기능으로만 구현 어떤 것들이 가능한지 간단한 소개
  • 43. Admin 코드 일부 @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): formfield_overrides = {models.TextField: {'widget': AdminToastEditorWidget}, } list_select_related = ('site', 'category', 'category=_site') list_display = [site_name, 'number', 'category_name', 'title_with_tags', 'reporter', 'hit_count', 'active', 'dates'] list_display_links = ['number'] list_per_page = 50 list_filter = ['site', 'category', 'active'] search_fields = ['title', 'reporter_name', 'reporter_email'] date_hierarchy = 'published' ordering = ['-published'] readonly_fields = ['migrated', 'number'] change_form_template = 'admin/article/change_form.html' actions = [export_as_csv, cache_clear]
  • 44. Toast에디터를 기사 편집 툴로 사용 class AdminToastEditorWidget(widgets.AdminTextareaWidget): template_name = 'forms/admin-toast-editor.html' class Media: css = {"all": ( "https:=/uicdn.toast.com/editor/2.5.2/toastui-editor.min.css", "https:=/cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css", )} js = ("js/admin-toast-editor.js",) Admin 커스텀 위젯 기능, toast 에디터를 읽는 js를 추가
  • 45. 기사 제목 필드에 여러 기능 한번에 넣기 html을 일부 하드코딩하여 기사 태그를 제목에 함께 표현 기사를 포털에 배포하는 기능과 배포된 기사를 검색하는 링크 추가 def title_with_tags(self, obj): return safe_render(""" <strong><a href="{% url 'admin:article_article_change' obj.id %}"> {{ obj.title }}=/a>=/strong><br=> {% for i in obj.tags.all %}<small>#{{ i.name }}=/small> {% endfor %} <br=> <a href="{% url 'admin:publish' obj.id %}">[배포 하기]=/a> <a href="{% url 'admin:publish_publishedarticle_changelist' %}?article={{ obj.id }}">[배포 기록]=/a> / 검색: <a href="https:=/search.naver.com/search.naver?query={{ obj.title|urlencode }}">[네이버]=/a> <a href="https:=/search.daum.net/search?q={{ obj.title|urlencode }}">[다음]=/a> <a href="http:=/search.zum.com/search.zum?query={{ obj.title|urlencode }}">[줌]=/a> <a href="https:=/search.daum.net/nate?q={{ obj.title|urlencode }}">[네이트]=/a> """, {"obj": obj}) title_with_tags.short_description = '제목 / 태그 / 배포' title_with_tags.admin_order_field = 'title'
  • 46. 추가적인 액션 함수 # actions = [export_as_csv, cache_clear] 부분에서 사용 def export_as_csv(modeladmin, request, queryset): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = f'attachment; filename="articles.csv"' writer = csv.writer(response) writer.writerow(['Number', 'URL', 'Title', 'Reporter', 'Published', 'Category']) articles = queryset.select_related('site', 'category').only( 'site', 'number', 'title', 'reporter_name', 'published', 'category') for article in articles: writer.writerow(( article.number, f'https:=/{article.site.domain}{reverse("article-detail", args=[article.number])}', article.title, article.reporter_name, date(article.published, 'Y-m-d H:i'), article.category.name)) return response def cache_clear(modeladmin, request, queryset): cache.clear()
  • 47. 3장 기사를 포털에 배포하기 간단한 검색 구현 데이터 이전 스크립트 더 필요한 것들
  • 49. 기사를 포털에 배포하기 한국의 뉴스 사이트는 네이버, 다음 같은 포털에 기사를 배포함 대부분 XML 파일을 생성 후 각 포털 서버에 FTP로 전송 HTTP POST 방식으로 제공하는 곳 생겨나는 중 매체별로 기사 포멧을 변환 후 전송 방식에 맞춰 보내줘야함 현재 각 뉴스 사이트마다 4개의 포탈에 전송 중
  • 50. Media 모델로 매체별 차이 대응 class Media(TimeStampedModel): active = models.BooleanField('활성화') # 식별 site = models.ForeignKey(Site, verbose_name='사이트') code = models.CharField(verbose_name='매체 구분 코드') title = models.CharField('매체명') # 파일명 file_ext = models.CharField('파일명 확장자', choices=[('', '없음'), ('txt', 'txt'), ('xml', 'xml')]) file_name_prefix = models.CharField('파일명 프리픽스', max_length=20, default='', choices=[ ('', '없음'), ('out', 'out'), ('news', 'news'), ('itdonga_', 'itdonga_'), ('gamedonga_', 'gamedonga_')]) file_name_type = models.CharField('파일명 타입', choices=[ ('number', '{기사번호}'), ('yyyymmdd-8-number', '{년}{월}{일}{기사번호8자}')]) # 내용 encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[ ('euc-kr', 'EUC-KR'), ('utf-8', 'UTF-8')]) publish_type = models.CharField('배포 유형', max_length=50, choices=PublishType.choices) extra = JSONField('추가 데이터', help_text='각 설정마다 필요한 추가 값을 JSON 형태로 기록합니다.') template = models.TextField('템플릿', help_text='기사 생성시 사용하는 템플릿입니다.')
  • 51. PublishedArticle 모델로 변환된 기사 내용과 이력을 남김 class PublishedArticle(TimeStampedModel): media = models.ForeignKey(Media, verbose_name='매체', on_delete=models.PROTECT, db_index=True) article = models.ForeignKey(Article, verbose_name='기사', on_delete=models.SET_NULL, blank=True, null=True, db_index=True) state = models.CharField('상태', max_length=6, default='new', blank=True, choices=[ ('new', '신규'), ('update', '수정'), ('delete', '삭제'), ]) file_name = models.CharField('파일명', default='', max_length=50, blank=True) content = models.TextField('변환된 기사 내용', default='', blank=True) encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[ ('euc-kr', 'EUC-KR'), ('utf-8', 'UTF-8'), ], blank=True)
  • 52. 배포 함수 # 복잡한 세부 함수들은 생략 def publish(self, article, state='new'): pa = PublishedArticle.objects.create( **{'media': self, 'article': article, 'state': state, 'encoding': self.encoding}) tags = [tag.name for tag in article.tags.all()] pa.file_name = pa.make_file_name() pa.content = Template(self.template).render(Context({ 'obj': pa, 'article': article, 'tags': tags, 'state': pa.converted_state(state), 'site': article.site, **self.get_codes(article, tags, pa.media.extra.get('tag', {})), })) pa.save(update_fields=['content', 'file_name']) # 기사 변환 후 FTP 업로드 등의 절차를 매체에 따라 수행 self.after_publish(pa) return pa
  • 54. 간단한 검색 구현 풀 텍스트 서치를 구현하는 것은 많은 비용이 듬 간단한 검색 쿼리에도 DB가 많이 느려질 수 있음 Django에 Postgresql을 쓸 경우 SearchVetor를 사용 가능 단어를 미리 잘라서 저장해놓고 검색 한글은 잘 안되긴 하지만 적당히 쓸만함
  • 55. 간단한 검색 구현 # Article 모델에 필드 추가 search_vector = SearchVectorField(null=True, blank=True) # Signal 추가 @receiver(post_save, sender=Article) def article_update_search_vector(sender, instance, created, **kwargs): update_fields = kwargs.get('update_fields') # update_fields에 직접 지정된 경우를 피해서 루프도는 것을 방지 if not update_fields or 'search_vector' not in update_fields: obj = Article.objects.annotate(document=search_vector).get(id=instance.id) obj.search_vector = obj.document obj.save(update_fields=['search_vector'])
  • 56. 간단한 검색 구현 # 검색 쿼리 search_query = reduce( operator.and_, (SearchQuery(f'{x}:*', search_type='raw') for x in query.split(' ')) ) # 검색할 쿼리를 분할해서 가중치로 넣고 정렬 rank = SearchRank(search_vector, search_query, weights=[0.4, 0.6, 0.8, 1.0]) # 현재 사이트에서는 점수보다 배포 시간을 중시 return queryset.filter(search_vector=search_query) .annotate(rank=rank).filter(rank=_gte=0.3) .order_by('-published', '-rank').distinct()
  • 58. 데이터 이전 스크립트 하나의 회사에서 제공하는 두개의 뉴스 사이트 기능은 거의 같았으나 각각 앱과 데이터베이스가 나뉘어 있었음 하나의 앱으로 합쳐서 관리 부담을 덜고자 함 데이터 이전 시 놓치거나 잘못 들어간 필드는 치명적 데이터 제어, 가공, 매치 부분을 분할하여 코드를 쉽게 읽을 수 있는 데이터 이전 스크립트를 작성
  • 59. 실행은 Django Management Commands 사용 class Command(BaseCommand): help = '예전 데이터베이스를 새 데이터베이스로 마이그레이션' def add_arguments(self, parser): parser.add_argument('=-count', dest='count', action='store', type=int) def handle(self, *args, **options): count = options['count'] if options['count'] else None # Mover 클래스를 만들어서 제어 mover = Mover('IT동아') mover.migrate(count) mover = Mover('게임동아') mover.migrate(count) 기존 데이터 DB는 Django에서 읽을 수 있게 manage inspectdb 명령으로 가져옴
  • 60. Mover 클래스 class Mover: def =_init=_(self, site_name): self.site = Site.objects.get(name=site_name) self.categories = [{'code': i.code, 'category': i} for i in Category.objects.filter(site=self.site)] def get_xxx(): # 달라진 분류, 필드 내용 등을 가공하는 함수들 def migrate(self, count=None): model, db_name = (GameArticle, 'legacy-game') if self.site.name == '게임동아' else (ItArticle, 'legacy-it') queryset = model.objects.using(db_name).all().order_by("updated", "id") if count: queryset = queryset[:count] else: last_migrated = Article.objects.filter(site=self.site, migrated=_isnull=False).latest('migrated') queryset = queryset.filter(updated=_gte=last_migrated.migrated) pbar = tqdm(total=queryset.count(), desc=self.site.name, mininterval=2, miniters=1, ncols=0) for old in queryset.iterator(): self.make_new_from_old(old) pbar.update(1) pbar.close()
  • 61. 필드 매칭은 명시적, 가공은 별도의 함수에서 def make_new_from_old(self, old): params = { 'reporter_name': old.user_name if old.user_name else '', 'reporter_email': old.user_email if old.user_email else '', 'title': old.title, 'contents_md': old_html_to_new_md(old.contents) if old.contents else '', 'contents_html': old.contents, 'intro': old.intro, 'thumbnail': old.thumbnail, 'category': category, 'hit_count': old.count_view, 'published': created, 'created': created, 'modified': created if self.site.name == '게임동아' and old.id < 60000 else updated, 'migrated': updated, } new, _ = Article.objects.update_or_create(site=self.site, number=old.id, defaults=params) new.tags.add(*tags)
  • 62. 마무리 요약하면 - Docker-compose로 개발 환경 설정 - CBV로 코드 재활용 - Test 코드로 검증 - Sites, Cache, SearchVector, Admin 등 Django 기본 기능 사용 - 복잡한 일은 단계를 나눠서 작성 이것보다 많은 코드가 있지만 시간 관계상 이 정도만 소개합니다
  • 63. 회사 홍보 엔라이즈는 사람과 사람, 사람과 콘텐츠를 연결하여 변화를 만듭니다. 위피: 동네 기반 소셜 데이팅 앱 콰트: 하루 10분 운동 습관 만드는 홈트레이닝 두 가지 서비스를 더 잘하기 위해 뭔가 해낼 것 같은 사람들이 점점 더 모이고 있습니다. 입사한지 반년도 되지 않았지만, 각자가 자신을 드러내며 더 잘하고자 하는 재미있는 회사입니다. https://nrise.net/ NRISE에서 동료를 구합니다