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

5주차 수요일 팀과제 - 가위바위보 게임 만들기

by 욕심많은알파카 2020. 1. 31.

3주차부터 2주에 걸쳐 django에 대해 어느정도 학습하고 나니 꽤 자신감이 붙었었다.

처음 볼 때는 view가 무엇이고 template이 무엇인지, 이 부분에서 이 코드가 어떻게 동작하는지 전혀 감이 오지 않았는데,

여러 장고 강의를 듣고 실습도 해보면서 반복학습을 하니 세부적인 부분까지는 몰라도 어떤 방식으로 페이지를 구성할 지, 어떤 식으로 모델을 짜야할 지 등에 대해서는 어느정도 감을 잡게 되었다. 저번 4주차 설 과제, 재고관리 사이트도 Ajax구현을 제외하고는 뷰, 모델, 템플릿 구성 방법이 대부분 이미 배웠던 내용에서 나왔기 때문에 큰 어려움 없이 할 수 있었다.

 

그리고 5주차에 접어들어 수요일 팀과제를 받게 되었는데, Django를 사용하는 첫 팀과제였다.

 

가위바위보 게임의 개요는 이렇다.

 

- 소셜 로그인/회원가입 기능을 이용하여 로그인 할 수 있다.

- 로그인한 회원은 가위바위보 게임을 만들어 '자신을 제외한' 다른 회원을 지정하고, 무엇을 낼 것인지 정할 수 있다.

- 전적을 불러오면 해당 회원과 관련된(즉, 해당 회원이 가위바위보를 신청하거나 신청받은) 게임들'만' 나온다.

- 가위바위보를 신청받은 회원은 접속했을 경우 해당 게임에 대해 '대응하기' 버튼이 뜨고, 무엇을 낼 것인지 정해서 제출할 수 있다.

- 대응까지 완료된 게임들은 승패를 보여주고, 세부정보를 누를 경우 누가 무엇을 내고 승자가 누구인지 보여주는 페이지가 로드된다.

 

일견 간단해보이는 게임 구조이지만, 지금까지의 우리가 실습해왔던 내용에서 조금 더 발전한 내용들이 있었다. 처음에는 이전의 경험들을 토대로 호기롭게 '10시에 만나면 6시 전에는 다 끝낼 수 있지 않을까?' 라는 생각을 했었지만... 역시 현실은 달랐다.

 

메인페이지

 

전적보기

 

 

아래는 팀과제를 진행하면서 우리 팀이 겪었던 문제이다. 해당 문제를 어떻게 해결하고, 무엇을 느꼈는지 적어보았다.

 

1. 소셜로그인의 구현

애초에 팀과제 목표 중 '소셜로그인'이라는 지금까지 배워보지않은 기능이 들어있었기 때문에, 팀플 시작 전에 미리 한번 기능 구현을 연습해가기로 했다.

 

그러나 생각했던것과 달리 소셜로그인의 구현 자체는 어렵지 않았다. 일단 Auth 2.0에 등록되어있는 구글, 카카오 API를 이용해 세팅만 해주면 되기 때문이었다. API를 처음 써보는 것이라 많이 걱정했는데, 잘 설명해놓은 블로그와 깃헙이 많아 차근차근 따라하면 어려운 것은 없었던 것같다.

#구글 로그인 참조

https://ssungkang.tistory.com/entry/Django-13-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-kakaogoogle-fackbook-%EB%93%B1?category=320582

#카카오 로그인 참조

https://github.com/Eunhyang/til/blob/master/Django/Django%20kakao%20login.md

 

소셜 로그인 자체에서 맞닥뜨린 문제는 SITE_ID와 관련된 문제였는데, 대부분의 블로그들은 SITE_ID=1 로 설정하도록 적어놓고 이에 대해 설명해주지 않기 때문이었다.

API를 사용할 때 Django의 admin페이지 중 social application 항목에서 site를 설정해주는데, 최초에 기본적으로 example.com이 들어가 있다. 만약 그 아래에 내가 원하는 사이트 URL을 적어넣고 등록시킨다면 DB에는 총 두개의 SITE가 등록된다.

id site
1 example.com
2 새로 추가한 URL

이 상태일 경우, settings.py에서 SITE_ID를 1이 아닌 2로 지정해주어야 정상적으로 소셜로그인이 기능한다.

그렇게 하는게 싫다면, 애초에 example.com이 추가되지 않도록 admin페이지에서 기본적으로 있는 example.com을 삭제해주고 SITE_ID=1을 지정하면 된다.

 

소셜로그인 이후에 user정보는 일반적으로 유저정보를 저장하는 User테이블에 저장되므로, 해당 테이블에서 회원의 정보를 가져올 수 있었다.

 

2. 모델 구성

팀이 처음 모인 후 점심시간을 포함하여 정말 서너시간정도를 모델만 구성하는데에 사용했던 것 같다.

 

처음에 내가 생각했던 모델 개념은 이랬다.

- User 테이블을 참조하는 Post 모델을 만들고, 해당 Post모델에서 Attacker_choice와 Defender를 설정한다.

- Post 모델에 대해서 OneToOne 모델을 만들어 해당 모델에서 Defender_Choice를 결정한다.

처음 내 생각으로는 누군가가 가위바위보를 신청했을 경우 한 사람만 대응할 수 있다는 개념이, 한 Posting에 대해 특정한 1인만 Comment를 달 수있다는 것과 같다고 생각했다.

 

그러나 팀원들간에 FK지정을 어떤 방식으로 할 것인지, OneToOne 모델을 사용해야 하는지에 대해 논의 한 결과, 두 개의 모델을 만들 필요 없이 하나의 모델만을 만들면 되며, 특정 1인만 대응할 수 있다는 개념은 view에서 현재 로그인한 사람과 defender의 일치여부를 검사하여 구현가능하다고 보았다.

 

Battle 모델 코드

위의 Battle 모델은 attacker와 defender에 대해 각각 한번씩 User테이블을 참조하도록 FK지정해놓았으며, 게임 생성시에 몇가지의 필드를 받고 대응시에 몇가지의 필드를 받아야 하는 게임 특성 상 대부분의 필드에 null=True, blank=True 속성을 지정해놓았다.

winner 필드는 view에서 배틀이 끝나면 attacker와 defender 중 승리한 사람의 이름으로 할당되도록 만들었다.

 

모델을 짜는데에는 오랜 시간이 걸렸지만, 한번 짜고 난 뒤 다시 갈아엎거나 고친적은 거의 없었다. 어떤 방식으로 다른 테이블을 참조할 것이며, 정보를 불러올 것이냐에 대해서 충분히 고민했기 때문이라고 생각한다. 꼼꼼하게 밑그림을 그리고 시작하는 스타일의 팀원이 여럿 있어 다행이라고 느꼈다(내가 모델을 구성했으면 아마 두 세번은 갈아 엎었을 것이다).

 

아쉬운 점은 위의 코드에서 굳이 choice_list는 각 battle 객체마다 저장할 필요가 없는 정보라는 것이다. 클래스 바깥에 지정해두고 choice를 고를 때 참조하게 두는것이 코드의 정갈함이나 DB사용량에 조금 더 도움이 되지 않았을까 싶다.

Model 클래스에 대한 개념이 약간 잘못잡혀있었던 것 같다. Model은 migrate를 위한 쿼리문을 만들어주는 밑그림 클래스일 뿐이다. 따라서 Model 클래스 내에 choice_list가 필드값이 아닌 튜플 형태로 존재한다고 해도 실제로 DB에 choice_list는 저장되지 않는다. 대신 attacker_choice, defender_choice의 속성인 choices=choice_list에서 choice_list에 해당 튜플문이 들어가게 된다.

즉, Battle 모델 클래스 내에 choice_list가 존재한다해서 battle 객체마다 choice_list가 저장된다는 표현은 잘못된 표현이고, 실제로는 Battle 모델 클래스 내에 있든 밖에 있든 choice_list는 attacker_choice와 defender_choice를 선택할 때 옵션으로 들어가있다는 표현이 맞다.

코드의 정갈함을 위해서 choice_list를 빼주는 것도 괜찮은 선택이지만, 클래스 내에서 choice_list를 바로 보여주는 것도 직관적으로 어떤 선택지가 있느냐를 보여주는 방식이기 때문에 결과적으로 해당 부분에는 문제가 없는것같다.

 

 

3. 모델폼의 사용

모델을 짜고 view와 개략적인 템플릿을 만든 뒤 맞닥뜨린 문제는 모델폼의 사용이었다.

Django에서는 클라이언트의 입력을 받는 두가지 강력한 도구를 제공하는데, 바로 폼(Form)모델폼(ModelForm)이다. 모델폼정확히 말하면 폼을 상속받아 몇가지 기능을 추가하고 사용하기 쉽게 만든 도구인데, 그런 점 때문에 오히려 문제가 생겼었다.

 

가위바위보 게임은 한 번에 모든 필드를 입력받는것이 아니라 총 두번에 걸쳐 필드를 입력받는다. 배틀 생성시에 attacker와 defender, attacker_choice가 결정되고 배틀 대응시에 defender_choice와 winner가 결정되는 방식이다. 따라서 각 상황에서 특정 필드만 지정할 수 있도록 특정 폼태그만 출력시켜야한다.

 

그러나 battle객체에 대한 모델폼을 만들고 fields를 '_all_'로 지정할 경우, 템플릿단에서 {{form.as_table}}을 사용할 시 당연히 모든 필드가 한번에 입력되도록 폼 태그를 출력한다.

 

이 문제에 대해서 생각해본 해결책은 다음과 같았다.

 

#1 모델폼이 아닌 그냥 폼을 사용한다

 

그냥 폼을 사용하면 템플릿에서 일일이 태그를 만들어 정보를 받아야 하겠지만, 커스텀 할 수 있다는 부분에서 특정 필드를 제외 할 수 있을거라고 생각했다. 그러나 모델폼의 is_valid 함수 등이 너무 유용했기 때문에 모델폼을 포기할 수 없었다. 만약 폼을 이용한다면 is_valid 함수를 직접 만들지 않는한, defender나 choice를 None으로 정할 수 있다는 문제를 피할 수 없다.

 

#2 모델폼을 2개 만든다.

 

사실 참 간단하고 별거 없는 해결책이지만, 생각해내는 데 까지 참 오랜 시간이 걸렸다. 같은 객체에 대해서 서로 다른 필드만 입력시켜주는 모델폼을 두번 만들어 본 적이 없어서 그랬던 것 같다.

 

BattleModelForm1은 배틀 생성시에, BattleModelForm2는 배틀 대응시에 사용되는 모델폼이다.

 

이처럼 각각 두개의 모델폼을 사용하면 상황에 맞는 폼들만 템플릿에서 출력해 줄 수 있었다.

 

이제 문제는 battle_create 뷰 함수에서 만들어진 form객체가 어떻게 다른 함수인 battle_response 뷰 함수에서 사용될 수 있냐는 것인데, 이는 특정 Battle객체에 form의 내용을 넘겨줌으로써 해결했다.

 

우리가 지금까지 배웠던 모델폼 사용법에서, form.save를 이용하여 모든 필드가 채워진 하나의 객체를 바로 생성하는 방식이 많았다.

# 해당 폼에서 얻은 내용을 속성으로 하는 새로운 객체를 만든다
form.save()

# 해당 폼에서 얻은 내용을 특정 객체(battle)에 집어넣는다
battle = form.save()

'''
단, instance=battle을 지정하여 form을 생성했다면
해당 form은 인스턴스로 미리 주어진 특정 배틀 객체에 대해서만 정보를 입력받는 form이 된다.
따라서, 그냥 form.save()를 하더라도 instance로 불러온 특정 battle객체에 대하여 저장된다.
'''

 

그러나 이미 DB에 저장되어 있는 특정 객체를 불러와 form.save()의 반환값을 할당하면, form에서 넘겨준 필드 값을 그대로 객체의 필드값에 업데이트시켜준다. 즉, form은 그냥 원래 있던 객체에 정보를 넘겨주는 전달자의 역할만 하는것이다.

단 주의할 점은, 위에서도 말했듯이 instance를 미리 지정해 놓으면 해당 form은 그 객체에 대해서만 기능하는 폼이 된다. 따라서, 아래 코드에서 battle = form.save() 부분은 굳이 'battle =' 부분을 쓸 필요가 없다.

일반적으로 battle = form.save()를 하는 순간 바로 폼에서 얻은 정보로 battle객체를 생성하는데, 그렇게 하지 않고 몇가지의 정보를 더 받은 뒤 save시키고 싶다면 battle = form.save(commit=false)라는 옵션을 추가시켜주면된다. 그러면 생성 즉시 DB에 저장하지 않고, 해당 battle 객체에 대하여 몇가지의 정보를 추가적으로 더 받은 뒤 battle = form.save()를 통하여 저장할 수 있다.

생성시의 view함수
대응시의 view함수

덕분에 우리는 battle_create 뷰 함수에서 attacker와 defender, attacker_choice만 저장한 battle객체를 DB에 저장시켜놓고 나서,

battle_response 뷰 함수에서 해당 객체를 id로 찾아 다시 defender_choice만 넣어줄 수 있었다. 이 과정에서 is_valid를 이용하여 넘겨진 정보가 올바른 형식인지 확인하는 모델폼의 기능도 사용할 수 있었다.

 

이후 result_winner 함수까지 수행하면 해당 battle 객체의 모든 필드가 채워진 상태가 된다.

 

 

4. 쿼리셋의 사용

가위바위보 게임의 핵심은 '자기자신에게는 싸움을 걸수 없다'는 것이다. 로그인한 회원은 가위바위보 배틀을 신청할 때 defender를 지정할 수 있지만, 자신이 아닌 회원만 지정할 수 있다.

 

그런데 문제는 Battle객체에서 defender를 받아올 때 attacker를 포함한 모든 회원을 가져온다는 것이었다.

 

우리는 모델폼을 사용할 때 특정 레코드(테이블 내의 특정 데이터, 즉 여기서는 attacker)를 제외하고 받아오는 것이 아니라, 그냥 조건을 걸지 않고 필드를 입력받는 것만을 배웠기 때문에 어떻게 해야할 지 감이 잡히지 않았다.

 

이 문제에 대해서는 정말 마지막까지 고민했었다.

 

#1 template단에서 select태그 사용

 

for문으로 회원마다 select 태그를 다 만들어주되, 로그인한 유저를 제외하고 만들어주는 방법을 생각했다. 그러나 템플릿에서는 최대한 로직을 만들어주지 않는것이 좋다는 컨벤션이 있었고, 또 그런 방식을 사용한다면 모델폼을 사용하는 의미가 없다는 생각이 들어 포기했다.

 

#2 is_valid 함수 만들기

 

다음에는 is_valid함수를 만들어 만약 POST 정보로 defender가 attacker와 같게 지정되어 넘어온다면 에러메시지를 출력하고 다시 입력을 받게 하는 방법을 생각했다. 그러나 자기자신을 선택지에 뜨게 하는 것은 막을 수 없다는 점에서 근본적인 해결은 아니었고, 임시방편일  뿐이었다.

 

 

결론적으로, 팀원 중 한 명이 모델폼으로 필드를 전달할 때 특정 필드의 특정 레코드를 제외하는 쿼리셋을 사용하여 해결했다.

 

exclude 쿼리셋이 핵심이다

위 코드를 보면 form의 defender로 필드를 넘겨줄 때 User테이블의 객체중 request.user.id, 즉 현재 로그인한 유저(로그인 한 유저만 공격을 시도할 수 있으니 자동적으로 attacker가 된다)를 제외한 나머지 회원들만을 넘겨주었다.

 

이 방식을 통해서 defender선택지에서 자기자신을 없애버릴 수 있었다. 정말 간단한 쿼리셋인데 팀원 대부분이 쿼리셋을 훑고 넘어가는 수준으로만 공부했기 때문에 생각해내기가 너무 어려웠다. 특정 필드도 아니고 특정 필드의 특정 레코드를 지우는 방법은 구글링해도 한국문서에서는 잘 나오지 않는 방법이라 stackoverflow등을 참고했다.

 

 

5. git 사용의 어려움

호기롭게 이번 팀과제의 시작을 git으로 시작했지만, 결국 branch를 딴 뒤 merge하는 과정에서 rebase의 사용, pull의 에러 등 여러 문제를 맞닥뜨려 제대로 사용하지 못했던 것같다. 혼자서 Repository를 사용할 때에는 내 코드의 어느부분이 커밋별로 바뀌고 추가되었는지를 알 수 있어 참 편하다고 생각했는데, 개인 기록 말고 협업에 이용하기에는 정말 공부가 훨씬 많이 필요할 것 같다.

 

그럼에도 불구하고 git을 쓰고 싶다고 느낀 점은 프론트엔드를 하면서부터였다. 사실 view함수나 model, form등은 로직만 이해하면 코드 몇 줄을 따라치거나 복사해서 카톡으로 넘겨주면 되기 때문에 못느꼈었는데, 각 태그가 이리저리 얽힌 template들을 카톡으로 넘겨받으며 작업하는것이 너무 힘들었다. 다른 사람의 컴퓨터에서 작동하던 css가 내 컴퓨터에서는 작동하지 않기도 하고, 겹치는 부분들을 수정하고 지워주기도 정말 귀찮았다. 이 점은 내가 아직 정말 큰 프로젝트를 겪지 않아서 그런걸지도 모르겠다. 사실 코드가 많아지면 백엔드를 git 안쓰고 작업하는것도 너무 힘들것 같다.

 

 

 

 

총 5개의 큰 문제를 맞닥뜨리고 해결하고 나니 많은 것을 느꼈다.

 

내가 배운 개념이라고 해도 완벽하게 활용하지 못하는 부분이 너무 많았다. 모델폼에 대해서도 분명히 배웠던 부분인데 모델폼 두개 나눌 생각을 하는데에 세 시간이 걸렸다. 쿼리셋도 분명 배운 부분인데 나는 objects.all()과 objects.get(id=pk)만 사용했던 것 같다. 필터와 Q도 제대로 알지못했다.

 

또, 사실 지금까지 팀플을 하면서 시간을 n배로 들이면 혼자서도 할 수 있지 않을까 라는 오만한 생각을 했었는데, 이번 기회로 그 생각을 정말 많이 반성했다. 이번 과제에서는 비록 협업을 체계적으로 하지는 못했어도, 팀원 각자가 가진 강점으로 각자 문제를 해결했었다. Model에, Template에, 각각의 View에, 쿼리셋에... 같이 피로그래밍 과정을 했음에도 불구하고 수많은 부분에 각자가 알고 있는 지식이 다 달랐다. 내가 해결할 수 없는 문제들은 전부 팀원들이 해결해 주었다. 만약 내가 혼자 이 모든 걸 다 짤수 있었을까 하면 정말 시간이 n배가 아니라 n^2배는 들었을 것 같다. 괜히 백엔드 프론트엔드가 나눠져있는 것도 아니구나. 괜히 백엔드에서도 DB가 나뉘어져 있는게 아니구나. 괜히 풀스택 개발자가 귀한것도 아니구나. 그런 생각이 정말 많이 들었다.

 

의욕만 넘쳐서 팀플을 시작했는데, 하면 할수록 정말 깨닫는게 많은 것 같다. 피로그래밍 하는 이유의 반 정도는 팀플때문인 것 같기도 하다. 이번 팀과제를 계기로 더 열심히 해야겠다는 생각이 샘솟는다.

댓글