StaticFile : 개발 리소스로서의 정적인 파일 (js, css, image etc)
Static Files Finders
App Directories Finder : “앱/static/” 경로를 검색
File System Finder : settings.STATICFILES_DIR = []의 경로를 추가
코드의 재사용을 위해서 script 코드를 별도의 파일로 분리할 필요성이 있었다.
그래서 공부한 대로 템플릿 내 script 코드를 external javascript로 분리했더니, (static 파일로 분리)
기존의 context variables를 사용하지 못하는 문제가 발생했다.(해당 script 코드는 view 에서 넘겨준 context variables를 활용)
문제를 해결하기 위해서 script 코드를 포함하는 html 템플릿 파일을 만들고, 이를 원하는 템플릿 내에 include 하는 방식으로 해결했다. (더 좋은 방법이 있을 것도 같은데..) 관련코드
요즘 연습중인 인스타그램st 프로젝트 를 진행하며 좋아요 버튼을 눌렀을 때 새로고침 없이 좋아요 숫자가 증감하도록 구현하고 싶었다.
내가 원하는 것 (좋아요 숫자가 바로 변경된다)
찾아보니 jQuery와 ajax를 활용하면 페이지 새로고침 없이 서버와 데이터를 주고 받을 수 있다고 하여 적용해 보았다. ajax 통신은 페이스북, 인스타에서 사용하는 무제한 스크롤, 구글의 라이브 검색 등 널리 사용되고 있다. 이번 연습 과정을 기록하여, ajax 개념을 다시 정리하고 유사한 기능 구현시 활용하려고 한다.
좋아요를 구현한 2가지 방법의 전체적인 처리 프로세스를 그려보면 아래와 같다.
각 구현방법에 대해서는 아래에서 하나씩 다루어 보려고 한다.
새로고침이 필요한 방식 / Ajax를 활용한 방식
Ajax
위키에 따르면 AJAX는 Asynchronous Javascript and XML의 약자로, 말그대로 Javascript와 XML을 이용한 비동기적 정보 교환 기법이다. 이름에 XML이라고 명시되어있긴 하지만, JSON이나 일반 텍스트 파일과 같은 다른 데이터 오브젝트들도 사용 가능하다.
간단하게, Ajax는 전체 페이지를 새로 고치지 않고도 페이지의 일부만을 위한 데이터를 로드하는 기법 이라고 할 수 있다
jQuery는 Ajax 요청을 생성하고 서버로부터 전달 받은 데이터 처리를 쉽게 만들어 준다.
동기처리모델 (synchronouse processing model) 의 경우,
브라우저는 스크립트가 서버로부터 데이터를 수집하고 이를 처리한 후 페이지의 나머지 부분이 모두 로드될 때 까지 대기한다.
반면에 비동기 처리 모델 (asynchronouse processing model) 의 경우,
브라우저가 서버에 데이터를 요청하면, 작업이 완료되는 것을 기다리지 않고 나머지 페이지를 계속해서 로드하고 사용자와의 상호작용을 처리한다.
@login_requireddefpost_like(request,pk):post=get_object_or_404(Post,pk=pk)# 중간자 모델 Like 를 사용하여, 현재 post와 request.user에 해당하는 Like 인스턴스를 가져온다.post_like,post_like_created=post.like_set.get_or_create(user=request.user)ifnotpost_like_created:post_like.delete()returnredirect('post:post_list')
@login_required@require_POST# 해당 뷰는 POST method 만 받는다.defpost_like(request):pk=request.POST.get('pk',None)# ajax 통신을 통해서 template에서 POST방식으로 전달post=get_object_or_404(Post,pk=pk)post_like,post_like_created=post.like_set.get_or_create(user=request.user)ifnotpost_like_created:post_like.delete()message="좋아요 취소"else:message="좋아요"context={'like_count':post.like_count,'message':message,'nickname':request.user.profile.nickname}returnHttpResponse(json.dumps(context),content_type="application/json")# context를 json 타입으로
Template
jQuery 를 사용하여 Ajax 통신을 수행한다.
<!-- 생략 --><li><inputtype="button"class="like"name="{{ post.id }}"value="Like"><pid="count-{{ post.id }}">{{ post.like_count }}개</p><pid="like-user-{{post.id}}">
{% for like_user in post.like_user_set.all %}
{{ like_user.profile.nickname }}
{% endfor %}
</p></li><script src="https://code.jquery.com/jquery-3.2.1.min.js"></script><script type="text/javascript">$(".like").click(function(){varpk=$(this).attr('name')$.ajax({// .like 버튼을 클릭하면 <새로고침>없이ajax로서버와통신하겠다.type:"POST",// 데이터를 전송하는 방법을 지정url:"{% url 'post:post_like' %}",// 통신할 url을 지정data:{'pk':pk,'csrfmiddlewaretoken':'{{ csrf_token }}'},// 서버로 데이터 전송시 옵션dataType:"json",// 서버측에서 전송한 데이터를 어떤 형식의 데이터로서 해석할 것인가를 지정, 없으면 알아서 판단// 서버측에서 전송한 Response 데이터 형식 (json)// {'likes_count': post.like_count, 'message': message }success:function(response){// 통신 성공시 - 동적으로 좋아요 갯수 변경, 유저 목록 변경alert(response.message);$("#count-"+pk).html(response.like_count+"개");varusers=$("#like-user-"+pk).text();if(users.indexOf(response.nickname)!=-1){$("#like-user-"+pk).text(users.replace(response.nickname,""));}else{$("#like-user-"+pk).text(response.nickname+users);}},error:function(request,status,error){// 통신 실패시 - 로그인 페이지 리다이렉트alert("로그인이 필요합니다.")window.location.replace("/accounts/login/")// alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);},});})</script>
느낀점
생각보다 많은 곳에서 Ajax가 활용되고 있다는 것을 알았다. (좋아요, 무한스크롤, 라이브검색, 계정 정보 표시 등)
이걸 간단하게 만들어주는 jQuery가 새삼 더 고맙게 느껴진다.
앞으로 Ajax를 잘 활용할 수 있도록 연습해야겠다.
(추가) 이 글을 작성한 이후에 Ajax를 활용하여 무한스크롤 기능, 댓글 추가, 팔로우 기능을 구현해보았다. 알고나니 계속 쓰게 된다. (이왕이면 새로고침 없는게 좋으니..!)코드상세
요즘 연습중인 인스타그램st 프로젝트 를 진행하며
실제 인스타그램 처럼 본문의 해시태그 문자열을 링크 처리하고,
해당 링크를 클릭하면 태그 내용이 포함된 모든 post list를 검색 결과로 보여주는 기능을 구현하고 싶었다.
내가 원하는 것
여러가지 방법을 고민하고 시도해보았는데, 생각도 못했던 오류들이 다양하게 발생했다.
(편집 화면에서 html tag가 그대로 노출되는 등)
그냥 다른 기능구현으로 넘어갈까 싶었지만 꼭 필요한 기능이라고 생각해서 열심히 찾아보았다.
결과적으로 custom template filter를 활용하여 DB 내용에 직접적인 영향을 미치지 않고, 깔끔하게 해결할 수 있었다!
Model
Post와 Tag 모델은 ManyToMany 관계를 갖고 있다.
content에 입력된 문자열 중에서 해시태그 형태 (#태그명)를 가진 문자열을 따로 추출하여 Tag 모델에 저장하도록 구현하였다.
classPost(models.Model):#...생략...content=models.CharField(max_length=140,help_text="최대 140자 입력 가능")tag_set=models.ManyToManyField('Tag',blank=True)classTag(models.Model):name=models.CharField(max_length=140,unique=True)
view
queryset을 활용하여 Post 모델의 모든 인스턴스를 가져온다. (post_list)
그리고 add_link 라는 이름의 함수를 정의한다. (이것이 바로 template에서 사용할 사용자 정의 필터의 이름이다.)
해당 필터 함수는 post|add_link 와 같이 활용될 수 있다. 이때 post 객체는 add_link 함수의 파라미터로 전달된다.
# post_extras.py@register.filterdefadd_link(value):content=value.content# 전달된 value 객체의 content 멤버변수를 가져온다.tags=value.tag_set.all()# 전달된 value 객체의 tag_set 전체를 가져오는 queryset을 리턴한다.# tags의 각각의 인스턴를(tag)를 순회하며, content 내에서 해당 문자열을 => 링크를 포함한 문자열로 replace 한다.fortagintags:content=re.sub(r'\#'+tag.name+r'\b','<a href="/post/explore/tags/'+tag.name+'">#'+tag.name+'</a>',content)returncontent# 원하는 문자열로 치환이 완료된 content를 리턴한다.
개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)
쿼리셋 수정을 통한 웹서비스 성능 개선
들어가기
웹서비스에 있어서 데이터페이스는 성능에 많은 영향을 미친다.
절대적으로 SQL 갯수를 줄이고, 각 SQL의 성능 및 처리속도 최적화가 필요하다.
리스트 조회 페이지를 만들때 Post.objects.all() 과 같은 queryset을 자주 활용했었다.
이번에 인스타그램st 프로젝트를 진행하며 데이터 조회시 몇개의 SQL 쿼리가 발생할까? 중복은 없을까? 궁금해졌다. django-debug-toolbar 를 활용해서 페이지 로딩시 발생하는 쿼리를 확인해보았는데, 결과가 충격적이었다. 고작 글 9개를 조회하고 화면에 출력하는데, 28개의 쿼리문이 발생하고 그 중에 26개는 중복이었다. (부들부들..)
django-debug-toolbar 를 활용해서 페이지 로딩시 발생하는 쿼리를 확인하니, Post 모델에서 User 모델/Tag 모델로 접근하면서 중복되는 DB 작업이 발생하는 것을 확인할 수 있었다.
{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 --><ul><li>{{ post.author.profile.nickname }}</li><!-- 1. post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 --><!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 --><!-- 총 20번의 중복 발생 --><li>{{ post.content }}</li><li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li><!-- 3. post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 --><!-- 추가적으로 10번의 중복 발생 --></ul>
{% endfor %}
# views.pydefpost_list(request):post_list=Post.objects.select_related('author').all()# 첫 DB 쿼리시 author record까지 로딩returnrender(request,'post/post_list.html',{'post_list':post_list,})
{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 --><ul><li>{{ post.author.profile.nickname }}</li><!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 --><!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 --><!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 --><!-- 총 10번의 중복 발생 --><li>{{ post.content }}</li><li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li><!-- 3. post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 --><!-- 추가적으로 10번의 중복 발생 --></ul>
{% endfor %}
첫 DB 쿼리시 .select_related('author')를 통해 author record를 함께 로딩한다.
# views.pydefpost_list(request):post_list=Post.objects.prefetch_related('tag_set').select_related('author').all()# 첫 DB 쿼리시 tag_set, author record 까지 로딩returnrender(request,'post/post_list.html',{'post_list':post_list,})
{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 --><ul><li>{{ post.author.profile.nickname }}</li><!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 --><!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 --><!-- 2. author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 --><!-- 총 10번의 중복 발생 --><li>{{ post.content }}</li><li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li><!-- 3. (개선 전) post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 --><!-- 3. (개선 후) 첫 DB 쿼리시에 tag_set record까지 로딩했기 때문에 추가 DB 없음 --></ul>
{% endfor %}
초기 DB 쿼리시 tag_set, author record를 함께 로딩한다.
수정 3
Post.objects.prefetch_related(‘tag_set’).select_related(‘author__profile’).all() 사용
28->20->13->5개로 쿼리문으로 줄어들었다 (중복없음)
(참고) post 인스턴스 갯수 : 9개
초기 DB 쿼리시 tag_set, author, author과 1:1 관계인 profile record를 함께 로딩한다.
defpost_list(request):post_list=Post.objects.prefetch_related('tag_set').select_related('author__profile').all()# 첫 DB 쿼리시 tag_set, author, author과 1:1 관계인 profile record까지 로딩returnrender(request,'post/post_list.html',{'post_list':post_list,})
{% for post in post_list %}
<!-- post_list가 10개라고 가정하면 --><ul><li>{{ post.author.profile.nickname }}</li><!-- 1. (개선 전) post 인스턴스별로 post와 N:1 관계인 author 모델에 접근 --><!-- 1. (개선 후) 첫 DB 쿼리시에 author record까지 로딩했기 때문에 추가 DB 없음 --><!-- 2. (개선 전) author 모델과 1:1 관계인 profile 모델에 접근하여 중복 작업이 발생한다 --><!-- 2. (개선 후) 첫 DB 쿼리시에 author 과 1:1 관계인 profile record 로딩했기 때문에 추가 DB 없음--><li>{{ post.content }}</li><li>{% for tag in post.tag_set.all %} {{ tag.name }} {% endfor %}</li><!-- 3. (개선 전) post 인스턴스별로 post와 N:M 관계인 tag 모델에 접근 --><!-- 3. (개선 후) 첫 DB 쿼리시에 tag_set record까지 로딩했기 때문에 추가 DB 없음 --></ul>
{% endfor %}
결론
그동안 너무 당연하게 모델.objects.all()를 사용하여 전체 DB를 조회하고 활용했었다.
이번 기회를 통해서 상황에 맞는 queryset을 사용하지 않으면 엄청난 중복이 발생하고, 이는 속도저하로 연결된다는 것을 알게되었다.
사실 AskDjango에서 관련된 강의를 들었을 때는 ‘아 이런게 있구나’ 하고 넘어갔던 내용이다.
역시 필요성이 생기니 정보에 대해 접근하는 태도가 달라지는 것 같다.
그리고 덤으로 django-debug-toolbar 활용 방법을 찾은 것 같다. 이렇게 유용할 줄이야!
messages.framework 를 활용하여 post 등록/수정/삭제시 유저 안내문구를 alert로 구현하였다.
이전에 messages.framework 를 몰랐을 때는 안내 메시지를 javascript로 하나하나 구현했었다.
강의에서 messages.framework를 접했을 때는 ‘아 이런게 있구나’ 정도로 끝났다.
필요성이 발생해서 사용하는 지금 messages.framework의 편리함이 드디어 실감으로 다가온다.
필요한 부분에 코드중복 없이 일괄 적용할 수 있다니..너무나 편하다.
select_related, prefetch_related를 활용해서 post list를 출력하는 페이지의 SQL 쿼리갯수를 28개에서 5개로 줄였다! 덕분에 소요 시간이 3.81ms에서 1.51ms로 줄어들어 성능을 개선할 수 있었다. 정말 유용한 것 같아서 블로그에 문제 해결과정을 정리해두었다.
6월 19일 (월)
오늘 한 일
생각도 못했던 시기에, 생각도 못했던 곳에서 면접 기회가 주어졌다.
Java와 Spring을 사용하는 곳이지만, 배울 수 있는 사람들이 많고 동기부여가 되는 업무환경이라고 생각했다.
1번의 필기시험과 2번의 기술면접을 진행했다. 처음 경험하는 개발자 채용 과정은 하나하나가 놀랍고 재미있었다. (맙소사 필기시험을 통과하다니!)
그렇게 최근 3주정도 집중해서 준비했던 일이 마무리 되었다.
바라던 결과는 얻지 못했지만 Java를 학습하며 새롭게 알게 된 점도 많았고, 자료구조 알고리즘도 다시 한번 정리할 수 있었다. 동기부여가 컸기에 그만큼 효율도 높았던 것 같다.
대신 3주만에 만난 django는 낯설어서 조금 충격이었다. 그렇게 열심히 했는데 이렇게 금방 낯설어 지다니!
잠시 한눈을 팔았으니 이제 애정 2배를 갖고 열심히 해야겠다 :)
Django를 활용하여 인스타그램 기능을 가진 웹어플리케이션 구현을 연습했다.
이번 연습의 목표는 아래와 같다.
1) 능동적으로 고민한다. (그동안 수동적으로 강의를 듣고, 같은 것을 반복 연습하는 부분에 집중했었다.)
프로그램을 만드는데 필요한 최소한의 도구를 익힌다. (문자, 숫자, 변수, 비교, 조건문, 반복문, 함수)
최소한의 도구로 다양한 문제를 해결해 본다.
가지고 있는 도구로 문제를 해결하는 것이 점점 어려운 일이 되었을 때 선배 개발자들의 성취를 찾아본다. 이 또한 최소한으로.
2번과 3번 반복
의식적으로 불편해지려는 시도를 해야겠다는 생각이 든다. 아는게 적은데 그 안에서 해결하려고 고민하는건 힘든 일이다. 그러다 다른 사람의 간단한 해결책을 보면 신기하기도 하면서 자괴감이 들기도 하고.. 그래서 요즘에는 문제를 만나면 ‘이걸 분명히 쉽게 푸는 방법이 있을텐데’ 하면서 다른 사람의 쉬운 해결책을 바로 찾아보려는 생각이 먼저 든다. 이게 좋은건지 나쁜건지 잘 모르겠는데, 고민을 적게 하는건 분명 좋지 않을 것 같다.
적절한 양만큼 고민하고 찾아보는 것, 적절한 양만큼 공부하고 만들어 보는 것 그걸 잘 할 수 있었으면 좋겠다.