본문 바로가기
Always Awake/피로그래밍 12기(19.12.31~20.02.22)

로그쉐어 프로젝트 및 피로그래밍 12기 7주차 활동 정리(20.02.10~20.02.16)

by 욕심많은알파카 2020. 2. 19.

토요일 (02.15)

DOM과 JavaScript by 박건태 선배님

 

-DOM이란?

HTML의 프로그래밍 인터페이스

—> 인터페이스는 데이터를 외부에서 조작할 수 있도록 해주는 방식.

 

구조화된 Node들, property와 method를 갖고 있는 object

—> 마치 Class와 유사하다.

—> html 클래스 내의 header, body.. 이런식으로 타고 들어가서 원하는 정보를 다 가져올 수 있다.

—> 브라우저는 html을 트리로 만든 후 이의 각 요소들을 중첩된 객체의 형태로 표현한다.

—> 따라서 DOM은 HTML의 데이터로 만든 커다란 오브젝트이다.

 

-DOM을 조작하는 법

스크립트 언어(자바스크립트)를 통해 DOM에 접근할 수 있다.

DOM은 html뿐만 아니라 xml 등 여러 형식에서도 만들어질 수 있다. 그러나 html의 DOM을 조작할 수 있는 스크립트언어는 자바스크립트뿐이다.

 

- 이후 자바스크립트 실습, DOM을 조작하여 클릭으로 on/off할 수 있는 시계만들기.

 

 

장고걸스 aws로 배포해보기 by 길재은선배님

 

uWSGI

클라이언트와 서버를 연결해주는 NGINX에게 django의 경로, 설정 등을 알려준다.

 

NGINX

우리가 일반적으로 url이라고 부르는 도메인 명은 사람들이 찾아들어오기 쉽도록 호스팅업체에서 서버와 연결시켜주는 이름일 뿐이다.

실제로 서버는 IP주소와 연결되어있으며, 도메인을 산 뒤 NGINX설정에서 IP값을 도메인명으로 바꾸어놓으면 해당 도메인과 서버가 연결된다.

 

사실 서버 배포는 기다리고 있던 강의였는데, 실전에서는 connected error를 만나 수업의 대부분을 놓쳤기 때문에 제대로 따라가지 못했다.  처음 해보는 배포에, aws 인터페이스에, vi에 당황해서 notion에 올라온 커맨드를 따라치기만 했던 것 같다. 관련 자료에 대해서는 피로그래밍이 끝난 후 (또는 가능하다면 로그쉐어 프로젝트의 배포시에) 정리하도록 하겠다.

 

 

팀프로젝트 로그쉐어 2주차

팀프로젝트도 2주차에 접어들고 팀원들과 많이 친해졌다. 우리 팀은 다른 팀에 비해 만나는 횟수가 많은 편이었는데, 1주차에는 월화수목금을 모두 만났고, 2주차에는 하루를 제외하고 또 모든 날을 만났었다. 1주차에 자바스크립트를 사용해야 하는 기능을 제외하고 대부분의 뷰를 짜놓았기 때문에, 2주차에는 프론트엔드를 어떤 식으로 만들어 갈 것인지를 정하고, 자바스크립트와 jquery, Ajax를 사용한 기능 구현에 집중했다. 

 

저번주 토요일 수업을 바탕으로 notion과 adobeXD의 사용을 시도해보았다. 1주차에 실수한 내용을 바탕으로, 2주차에는 기본에 충실하려 노력했다. 따라서 기본적인 선결 과제들을 먼저 처리하고, 디자인적 재능이 있는 팀원과의 토의하에 템플릿을 만들었다.

 

구현해야하는 페이지를 정리하였다

 

로그쉐어 notion todo list (이 사진은 3주차에 캡쳐했습니다)

 

 

2주차의 가장 큰 변화는 notion의 사용이었다. 우리 팀은 피로그래밍 12기 6개 조 중에서도 정말 노션을 열심히 써보려 노력했다고 말 할수 있다(사실 다른 팀 노션을 보지는 않았지만). 개발 및 스타트업 운영에 굉장히 자주 사용된다고 해서 써보고 싶은 마음도 있었고, 실시간으로

서로 무엇을 하고 있는지 공유하고 어떤 버그가 생겼는지, 어떤 기능이 필요한지 바로 볼 수 있었다. 마치 굉장히 기능이 많은 실시간 화이트보드를 쓰는 느낌이라고할까.

 

사실 1주차에는 개인별로 앱 하나씩을 잡고 맡아나갔기 때문에, 코드공유와 리뷰를 하는것을 제외하고는 굳이 누가 무엇을 하고있는지에 대해서 알 필요가 없었다. 그러나 2주차에 접어들면서 서로의 기능이 각각 얽혀들었다. 예를 들어 로그쉐어의 핵심 기능중 하나인 그룹내의 정보검색 기능은 group_management앱의 모델과 로직, search앱의 검색필터링이 얽혀있었다. 따라서 두 앱을 맡았던 팀원들간의 정보공유가 필요했다.

 

게다가, 나는 2주차에 접어들면서 포지션을 바꾸었기 때문에 더 그랬다. 레퍼런스를 찾아 전달하고, 프로토타입을 만들고, 디버깅을 하는 과정들이 앱 하나에 국한 된 것이 아니라 프로젝트 전반적으로 이루어졌기 때문에 노션을 사용해서 정말 다행이라고 느꼈다.

 

 

todo list의 세부항목

 

위와 같이 todo list에 올라온 특정 주제에 대해서 어떤 코드를 수정하거나 추가하였는지 코드 블럭이나 이미지로 추가할 수 있다는 점도 노션의 굉장히 큰 장점이다.

 

 

아쉽게도 Adobe XD는 사용하지 못했다. 사용 경험이 있는 팀원이 한명밖에 없기도 했고, 툴의 사용법을 익혀서 원하는 디자인을 만들어내기까지 시간이 너무 오래 걸린다고 판단했다. 해당 디자인을 코드로 옮겨야하기도 하니, 짧은 기간의 프로젝트동안은 무리라고 생각했다. 우리 포지션은 당장은 개발자이지, 디자이너가 아니니까.

 

 

2주차에 내가 겪었던 기술적 문제들은 이렇다.

 

1. Ajax사용하여 그룹 가입 신청 구현 (json, 쿼리셋형식)

 

2주차에 태어나서 처음으로 자바스크립트에 도전하면서, 정말 맨땅에 헤딩하는 느낌이었다. 사실 자바스크립트의 문법이나 이런 것들이 어려운 것은 아니었는데, 어떤 방식으로 동작하는지나 원하는 로그를 찍어보는 방법 등에 익숙하지 않았던 탓이다.

 

일반적으로 django-python만으로 하나의 페이지를 만든다고 가정했을때, 해당 페이지 내에서 클라이언트의 입력을 받을 수 있는 방식은 GET과 POST 방식이다. 유저가 정보를 입력하여 submit했을때, 해당 정보가 전송되어 view를 다시 불러오게 되고, 결과적으로 템플릿을 한번 더 렌더링하여 보여준다. 따라서, 클라이언트는 많은 정보를 전송하든 적은 정보를 전송하든 한번 이상의 페이지 리로딩을 거쳐야한다. 이 방식은 불필요한 렌더링을 늘려서 서버에도 비효율적일 뿐만 아니라, 클라이언트에게도 잠깐의 단절경험을 제공하기 때문에 좋지않다.

 

이 과정에서 사용되는 것이 Ajax이다. Javascript중에서도 Ajax는 페이지의 비동기적 구현을 위해 사용하는 라이브러리이다. Ajax는 해당 페이지 내에서 수정이 필요한, 새로운 정보들만을 json형식으로 받아 전달할 수 있게 해준다. 

 

구현해야 하는 것은 그룹에 가입신청을 하는것. 로그 쉐어에서 유저는 특정 그룹에 가입 신청을 보낼 수 있고, 신청에 대해서 그룹 매니저는 수락 또는 거절을 클릭하여 이를 승인/거절 할 수 있다. 그러나 한 신청에 대해 수락/거절을 클릭할 때마다 새로 페이지가 리로딩되는것은 여러 가입신청을 처리해야하는 매니저의 입장에서 너무 불편한 경험이기 때문에, 그 부분을 Ajax로 처리하기로 했다.

 

#group_management.views.py

@login_required
@require_POST
def allow_request(request):
    pk = request.POST.get('pk', None)
    message = get_object_or_404(GroupRequest, id=pk)
    sender = message.sender
    group = message.group
    group.members.add(sender)
    GroupRequest.objects.filter(id=pk).delete()
    context = {
        'messages': GroupRequest.objects.filter(group=group)
    }
    return HttpResponse(context)

@login_required
@require_POST
def disallow_request(request):
    pk = request.POST.get('pk', None)
    message = get_object_or_404(GroupRequest, id=pk)
    group = message.group
    GroupRequest.objects.filter(id=pk).delete()
    context = {
        'messages': GroupRequest.objects.filter(group=group)
    }
    return HttpResponse(context)

 

프로토타입이라 뷰 자체는 간단하다. 만들어진 GroupRequest (가입신청) 객체에 대해

 

1. 수락하여 가입신청을 delete하고 그룹에 add하든지

2. 거절하여 가입신청을 delete하고 그룹에는 add하지 않든지

 

두 경우 중 하나이다.

 

GroupRequest객체 자체는 모델에서 unique로 잡혀있어서, 한 사람이 특정 그룹에 동시에 여러개의 가입신청을 보낼 수 없다. 따라서 거절을 당해서 가입신청이 삭제되었을 경우에만 다시 새로 가입신청을 할 수 있다.

 

이 뷰에 대한 Ajax는 다음과 같다.

 

<!--group_management.request_to.html-->

{% for message in messages %}
    <div>
        <span>{{ message.sender }}</span> -->
        <span>{{ message.group }}</span> :
        <span id="is_allowed-{{ message.id }}">{{ message.status }}</span>
    </div>
    <form action="" id="group_request_form-{{ message.id }}" class="group_request_form" method="post">
    {% csrf_token %}
        <button type="button" class="allow_button" data-id="{{ message.id }}">수락</button>
        <button type="button" class="disallow_button" data-id="{{ message.id }}">거절</button>
    </form>
{#    {% include 'group_management/deal_request.html' %}#}


    <br>
{% endfor %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script async type="text/javascript">
        $(".allow_button").click(function(){
            var pk = $(this).attr('data-id');
            $.ajax({
                type: "POST",
                url: "{% url 'group_management:allow_request' %}",
                data: {
                    'pk': pk,
                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                },
                dataType: "html",
                success: function(response){  //통신 성공시 메시지 출력
                    alert("수락하였습니다.");
                    $("#is_allowed-"+pk).html('멤버 등록 완료');
                    $("#group_request_form-"+pk).html('');

                },
                error : function(request, status, error) { //통신 실패시 요청관리페이지로 리다이렉트
                    alert("잘못된 요청입니다. error:"+error)
                    window.location.replace("{% url "group_management:request_to" %}");
                },
            });
        })
        $(".disallow_button").click(function(){
            var pk = $(this).attr('data-id');
            $.ajax({
                type: "POST",
                url: "{% url 'group_management:disallow_request' %}",
                data: {
                    'pk': pk,
                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                },
                dataType: "html",
                success: function(response){  //통신 성공시 메시지 출력
                    alert("거절하였습니다.");
                    $("#is_allowed-"+pk).html('멤버 등록 거절');
                    $("#group_request_form-"+pk).html('');

                },
                error : function(request, status, error) { //통신 실패시 요청관리페이지로 리다이렉트
                    alert("잘못된 요청입니다. error:"+error);
                    window.location.replace("{% url "group_management:request_to" %}");
                },
            });
        })
    </script>

각 버튼은 하나의 GroupRequest 객체의 id를 담고있는 고유한 버튼들이고, 이 id를 script부분에서 넘겨받아 click function으로 작동한다.

각 GroupRequest에 대하여 allow 또는 disallow url로 연결시켜주며, 수락 거절을 한 이후에는 따로 가져올 데이터 없이 수락완료 혹은 거절완료를 표시해주기만 하면 되기 때문에 datatype이 html이다.(json으로 지정하면 넘겨줄 정보를 담은 javascript객체를 반환한다)

 

datatype html과 json의 차이때문에 몇시간을 고생했다. 많은 예제들이 datatype을 json으로 지정하고 있었는데, 왜 그렇게 하는지 이해하지도 않고 무턱대고 json형식으로 넘기려 하니 not iterable오류라던가, 정말 갖가지 오류들이 다 튀어나왔다.

 

 

2. Ajax 사용하여 그룹회원 추방 기능 구현

 

위의 Ajax 경험을 살려 회원 추방 기능도 구현했다.

 

로그쉐어에서 그룹에 가입한다는 것은 곧 그룹 멤버들 전체의 활동 기록을 공유받을 수 있다는 것을 의미한다. 로그 쉐어는 그룹 멤버들 간에서의 제한적인 정보 공유를 목적으로 하는 사이트이기때문에, 그룹 내에는 반드시 정보를 공유받을 자격이 있는 일원들만 존재해야한다. 따라서 관리자가 불필요한 멤버를 '제거'해줄 필요가 있다.

 

    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script async type="text/javascript">
        $(".delete_member_button").click(function () {
            var member_id = $(this).attr('member_id');
            var group_id = $(this).attr('group_id');
            $.ajax({
                type: "POST",
                url: "{% url 'group_management:delete_member' %}",
                data: {
                    'member_id': member_id,
                    'group_id': group_id,
                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                },
                dataType: "html",
                success: function (response) {  //통신 성공시 메시지 출력
                    $("#delete_member_button-" + member_id).html('삭제된 멤버입니다.')

                },
                error: function (request, status, error) { //통신 실패시 요청관리페이지로 리다이렉트
                    alert("잘못된 요청입니다. error:" + error)
                    window.location.replace("{% url "group_management:detail_group" group.id %}");
                },
            });
        })

    </script>

 

Ajax 사용자체는 어렵지 않았다. 살펴보면 위의 코드와 거의 구성이 똑같다고 느낄 것이다.

수락 거절을 하는것처럼 그룹의 멤버를 delete하는 view로 연결하면 된다.

그런데 여기서 예상하지 못한 문제를 만났다.

 

def delete_member(request):
    member_id = request.POST.get('member_id', None)
    group_id = request.POST.get('group_id', None)
    now_group = get_object_or_404(CustomGroup, id=group_id)
    # 해당 그룹과 멤버 조인 테이블에서 특정 레코드를 삭제한다.(멤버관계를 삭제한다)
    now_group.membership.filter(Q(customgroup_id=group_id) & Q(user_id=member_id)).delete()
    q = request.GET.get('q', '')  # GET request의 인자중에 q 값이 있으면 가져오고, 없으면 빈 문자열 넣기
    qs = now_group.members.all()
    if q:
        qs = now_group.members.filter(Q(username__icontains=q) | Q(user_profile__name__icontains=q))

    context = {
        'group':now_group,
        'members':qs,
    }

    return HttpResponse(context, content_type="application/json")

view의 로직을 보면, 해당 그룹 내에서 membership 테이블을 참조해 특정 그룹과 특정멤버의 관계를 삭제하는 코드를 확인할 수 있다.

 

첫 모델을 짤 때, 우리는 CustomGroup(Group)과 User(Member)의 관계를 ManyToMany로 설정했다. 한 유저가 여러개의 그룹에 가입할 수도 있고, 한 그룹이 여러명의 멤버를 가질수도 있기 때문이었다. Django ORM에서, ManyToMany로 설정된 테이블간의 관계에서는 자동적으로 새로운 테이블을 하나 만들게 된다. 두 객체들간의 id를 한번에 foreignkey로 잡고 있는 중간테이블이다. 이 테이블의 이름은 두 테이블의 이름을 합쳐서 자동적으로 생성되며, 우리가 작성한 models.py에서 확인할 수 없기 때문에 이게 있긴 한지, 어떻게 접근하는지도 모르고 있었다.

 

따라서 처음 view를 짰을 때 순진한 생각으로, now_group의 member로 들어가 해당 멤버를 삭제하면 그룹의 입장에서 멤버가 삭제된 것이니 멤버관계가 사라지겠지? 라고 코드를 짰다. 그리고 실제로 잘 작동하는 것 처럼 보였다. 그룹 회원 추방을 누르면 실제로 리스트에서 멤버가 사라졌으니까..

 

나중에 admin페이지에서 확인해보고서야 깨달았다. now_group의 member는 그룹과 엮여 있는 User테이블이다. 따라서 해당 멤버를 삭제해버리면 말그대로 'DB에서 해당 유저를 삭제'해버린다. 정말 바보같은 일을 저질렀다.

 

나중에 알아보니 내가 구현하려는 기능은 group과 member간의 '관계'를 삭제하려는 것으로, 숨겨져있는 중간 테이블의 해당 relationship 레코드를 삭제하려면 된다고 한다.

 

그러면 도대체 숨겨져있는 중간테이블은 어떻게 접근해야할까?

 

3. dbinspect 사용 원래 있던 조인 테이블 가져와서 커스텀하기

 

찾아보니 db inspect라는 명령어로 이미 존재하는 테이블들의 models.py 코드를 가져올 수 있다고 한다.

 

(django) inspectdb, 기존 테이블을 models.py 로 가져오기 - 이상한모임

많은 장고(Django) 예제에서 models.py 를 먼저 만들고 syncdb 를 통해서 실제 데이터베이스에 구조를 잡게 되는데 사실은 실제 데이터베이스에 이미 데이터나 구조가 있는 경우가 더 많은것같다. 그럴경우 일일히 models.py에서 데이터 모델들을 잡아주는것이 번거로운데(테이블이 많으니까) 장고에서는 inspectdb 라는 기능을 통해서 settings.py에 연결되어 있는 데이터베이스에 대한 models.py의 내용을 가져올 수 있도록 해준

blog.weirdx.io

 

따라서 db inspect를 통하여 중간에 join되어 있는 테이블을 가져왔다. 그리고 해당 코드를 참고하여서 내 models.py에 해당 테이블의 참조명을 재설정하고 오버라이드했다.

 

members = models.ManyToManyField(User, verbose_name='그룹멤버', related_name='user_groups', db_table='group_membership')
class GroupMembership(models.Model):
    customgroup = models.ForeignKey(CustomGroup, models.DO_NOTHING, related_name='membership')
    user = models.ForeignKey(User, models.DO_NOTHING)

    class Meta:
        managed = False
        db_table = 'group_membership'
        unique_together = (('customgroup', 'user'),)

 

그렇게 숨겨져있던 중간 테이블을 GroupMemberShip으로 명명하고 related_name을 통하여 그룹에서 접근할 수 있도록 만들어놓았다.

이렇게 하고 나니 now_group에서 해당 relationship record만 삭제할 수 있었다.

 

 

 

2주차, 3주차는 프로젝트의 완성도를 위해 많은 시간을 쏟아부어서 포스팅 할 시간이 적었던 것 같다. 정말 폭풍같은 한 주 한 주를 보내고있어서 배우는것도 너무 많고, 이를 정리해서 올리는게 쉽지 않다. 사실 위에서 적은 기술과제는 일주일동안 해결한 부분의 정말 일부분밖에 되지 않는다. 가장 생각나는 몇개들만 간추려서 올리고 있다.

 

프로젝트를 진행하면 할 수록 팀원들의 대단함을 느낀다. 가르쳐주지않은 무한스크롤링이나 북마크도 레퍼런스를 보고 열심히 만들어오는 유빈누나나, 보고있으면 머리가 핑핑도는 쿼리셋들을 혼자 쉘로 찍어보며 필터를 정리하는 현동이, 어마어마한 양의 group_management앱을 며칠만에 뚝딱 만들어놓았던 수현누나, 이메일/회원가입의 깔끔한 구현과 프론트엔드를 전반적으로 디렉팅하고있는 수경이까지 정말 열심히하는 사람들밖에 없다.

 

그래서인지 하루하루마다 기분이 많이 왔다갔다했던것같다. 누군가는 어느 기능을 다 끝내고 다른 코드를 짜고 있는데 나는 몇시간째 같은 부분에 코드 대여섯줄을 추가했다 삭제했다 하며 '이게 왜 안되지? 이게 왜 되지?'를 몇번이나 묻고있었다. 그런 날이면 집으로 돌아가는 길에 자괴감이 심해 집에 돌아와서는 코드를 열어보기도 싫었다.

 

그럼에도 불구하고 팀원들이 너무 좋았기 때문에 계속해서 붙잡고 매달렸던 것 같다. 내 옆의 누군가가 열심히 하고, 잘 한다는게 스트레스일수도 있지만 그만큼 나에게 큰 자극이 되었다. 무엇보다 아무도 독단적으로 행동하거나 서로에게 질타한 적이 없고, 오히려 자기가 만든 기능을 가르쳐주려하거나 자랑하려는모습(!!)이 너무 보기 좋아서 매일 아침에 새롭게 마음을 다잡고 팀플을 하러 나갔다.

 

모든 활동이 끝난 지금, 더 쓰고 싶은 말이 많지만 그건 정리해서 좀 이따 다음포스팅에 쓰기로.

 

댓글