안녕하세요! 방금 오늘의 점심 포스팅을 막 올린 참인데, 바로 또 프로젝트 포스팅을 적고 있네요..ㅋㅋㅋ
이번에 제작한 너의 모든 일정 서비스는 일정공유 및 관리를 위한 캘린더를 제공합니다. 하나의 캘린더에서 내가 속한 여러 팀의 일정을 관리할 수 있는 서비스를 만들고자 시작했습니다.
팀원은 총 4명으로 프론트엔드 2명, 백엔드 2명으로 구성했고 저는 이번에도 백엔드 겸 팀장 겸 배포를 맡아 프로젝트를 진행했습니다.
프론트는 react, 백엔드는 django를 사용했습니다.
기간은 8월 2일부터 9월 1일까지 약 한달간 진행했습니다...만! 팀구성이 다들 풀스택을 지향하는 분들로만 이뤄져있어서 프로젝트 진행 전에 포지션 관련 면담을 팀원들 한명씩 진행했고 결과적으로 각자가 지금까지 해왔던 포지션에 반대되는(?!) 포지션을 맡게 되었습니다. 백엔드 위주로 공부하던 사람은 프론트, 프론트 위주이던 사람은 백엔드
저는 어차피 이도류라 남는 포지션인 백엔드로~
그냥 프론트 세명하고 백 한명이서 하면 되지 왜 굳이 백을 갔냐고 물으신다면! 사실 시작할때만 해도 팀원이 총 5명이였습니다. 프론트 3명, 백엔드 2명 구성이였는데, 프론트 한분이 중간에 건강 상의 이유로 하차하시게 되었다는 슬픈 사실...ㅠ 중도 하차하신 분께서 맡으셨던 프론트 업무는 제가 가져와서 진행했습니다. 그래서 저는 처음에만 백엔드, 중간에는 프론트, 마지막엔 배포 담당인 느낌이랄까요? 하하.. 한분이 비면서 다른 백엔드 팀원분께서도 css를 도맡아서 해주셨습니다. django 코드 칠 때보다, css 만질 때가 더 행복해 보이시던 우리 팀원분..ㅎㅎ
중간에 말이 좀 딴길 샜는데, 다들 본인이 주로 맡았던 포지션이 아니신지라 일주일정도 좀 더 공부하는 시간을 가지고 본격적으로 프로젝트 진행해보자 해서 실질적인 작업은 8월 9일부터 시작했습니다.
풀스택을 지향하지만 한쪽 포지션으로만 살아왔던 3인방과 저의 우당탕탕 프로젝트 이야기 지금부터 들려드리겠습니다!! 좀 부족해도 그러려니 해주세요! ^^
이번 프로젝트는 캘린더라는 주제를 가지고 시작됐습니다. 문제는 어떤 캘린더를 만들 것이냐 였는데, 많은 아이디어가 나왔고 그 중 팀 일정관리가 가능한 캘린더를 만들기로 하였습니다.
이런 간단한 것보단 좀 더 다양한 기능들이 들어간 캘린더를 만드는 게 좀 더 공부가 되지않겠냐고 하시는 분들도 있습니다만, 저희는 간단하기 때문에 이런 기획을 채택하게 됐습니다.
위에서도 말씀드렸지만 저희 팀원들이 맡은 포지션이 본업이 아니다보니 복잡한 기능들이 들어가기 시작하면 부담이 될거라는 판단도 있었고, 프로젝트 기한이 있다보니 간단한 기능을 우선적으로 구현한 다음 기능적인 부분은 추가해도 늦지 않다고 생각했습니다.
물론 인생이란게 뜻대로 되는게 없더군요. 간단하다고 생각했던 초기 기획마저 기한을 꽉꽉 채우고서야 어느 정도 정리가 되었습니다..
일주일간 개인정비 시간이 지나고 본격적으로 프로젝트를 시작하기로 한 날짜가 되어 가장 먼저 진행한 건 데이터 모델링이였습니다. ERDCloud를 사용해서 진행했고 이번 프로젝트 자체가 모델을 복잡하게 만들게 없어서 금방 마무리할 수 있었습니다. ERDCloud 사용이 처음이라 선으로 관계를 표시해줬는데, 뭐가 어떤 의미로 사용되는 선인지 잘 몰라서 헤맸던 기억이 나네요..ㅎㅎ
초기에 모델링을 잘 해놔서 프로젝트 마무리까지 큰 변동없이 진행할 수 있었습니다.
데이터모델링 후 실제 django 모델작성은 다른 백엔드 팀원분께서 맡아 진행해 주셨습니다. 프로젝트 시작할 때만 해도 FK가 뭔데 하시던 분이 모델을 짜왔을 때의 감동이란..😂
모델을 다 작성한 이후엔 각자 맡은 API를 만들기 시작했습니다.
팀일정 관련된 부분이 제일 양이 많을 것 같아서 한명은 일정 관련 API를 담당하고 나머지 한명이 다른 API를 다 담당하기로 했고, 제가 그 나머지를 맡았습니다! 허허
제가 만든 API 목록과 명세서 일부입니다.
간단한 API들이라 따로 설명드릴 부분이 없네요.
작성한 API들은 클라우드타입을 통해 배포해서 프론트분들이 사용하실 수 있게 했습니다. 그때까진 이게 문제가 될 줄 몰랐죠..
API를 다 만들고 프론트 팀원들 헤매는 부분들 봐주면서 프로젝트가 진행되던 어느날 정말 중대한 문제가 생깁니다. 이번 프로젝트를 진행하며 제가 맡았던 부분 중에 결국 구현 못하고 포기한 한가지..
이번 프로젝트에서는 세션을 이용한 로그인을 방식을 이용하기로 했습니다. 특별한 이유가 있다기 보다는 이전에 오늘의 점심 프로젝트를 진행하면서 simple JWT를 사용하면 프론트에서 구현해야할 로직들이 어떤게 있는지 알고 있었고 이번에 로그인 기능을 맡기로 하신 프론트 팀원분 역량을 생각했을 때, 조금 무리가 있겠다 싶은 판단이 들어서 세션을 사용하자고 했습니다. 모바일 환경을 타겟으로 한 프로젝트도 아니였기 때문에 굳이 JWT를 사용하지 않아도 되겠다 싶었구요.
그런데 로그인을 맡아 주신 프론트 팀원분께서 중도하차하는 일이 생겼습니다. 당시에 다른 프론트분들은 기존에 맡고 있던 파트만 해도 버거워하던 시기라 로그인 기능은 제가 구현하기로 했습니다.
제가 이어받았을 때, 로그인/회원가입이 css 빼고는 어느정도 완성이 되어있던 상태라 css만 만져주고 라우터 구성하고 마무리를 했습니다. 이쯤 다른 프론트분들도 캘린더 부분 마무리하고 API들을 가져다 사용하려던 참인데, 한분께서 자꾸 401이 뜨는데 어떻게 해야 하냐고 물어 보셨습니다.
로그인이 제대로 안된 것 같으니 다시 해보라고 했더니, 로그인이 제대로 안된다고 하셔서 확인해보니 실제로 sessionid가 쿠키에 저장되지 않고 있었습니다.
원인은 바로 쿠키의 samesite 속성. django는 위에서 말했듯 클라우드타입으로 배포가 되어있었고, 프론트는 로컬호스트에서 작업 중이라 발생한 일이였습니다.
samesite를 none으로 설정하려면 필수적으로 secure 속성이 true여야 한다는 걸 알게 되고 프론트도 클라우드타입이든 넷틀리파이든 사용해서 배포하고 진행할까하는 생각도 잠깐 했는데, 그냥 테스트서버를 하나 배포하기로 했습니다. 어찌됐든 같은 백엔드와 프론트엔드가 같은 도메인이면 해결될 문제로 보였거든요.
너의 모든 일정이라는 서비스 자체가 로그인이 시작인 서비스이고, 로그인 없이는 API사용도 불가한 상태라 빠른 해결이 필요했습니다. 제가 테스트서버 올리겠다고 밤을 샌 이유입니다. 흑흑
테스트서버 아키텍쳐입니다. AWS는 오늘의 점심을 프리티어로 올려놔서 하나 더 올리면 요금청구될까봐 크레딧 있는 네이버 클라우드 플랫폼을 이용해서 배포했습니다.
그냥 배포만 해놓으면 팀원들이 코드 짤 때마다 제가 있어야 서버에 반영이 되는데, 제가 없을 수도 있고 좀 비효율적인 것 같아 github actions를 이용해 자동으로 배포되게 세팅해 줬습니다.
이후에 최종적으로 배포할 때, 어차피 테스트서버에 올라간 코드를 배포할거로 생각돼서 github에 올리면 해당 변경사항을 포함하여 도커 이미지를 리빌딩하고 도커허브에 까지 올리도록 workflow를 작성했습니다.
밤새 작업해서 서버띄우고 sessionid도 정상적으로 들어오는 걸 보고 집에서 두시간 정도 자고 나왔는데, 세상에 자고 나니 이번엔 헤더에 set-cookie 자체가 안들어오는 상황이 됐습니다.
기존에는 헤더에 set-cookie로 sessionid와 csrftoken이 들어왔었는데, 둘 다 안들어오다 sessionid만 들어오다 난리가 났더군요.. 주변에 물어봐도 정확한 원인을 아는 사람이 없었습니다. 원인을 모르니 해결도 못하고 있었구요. 결국 한나절을 해결하려고 낑낑대다가 문제의 꼬리조차 못잡고 로그인이 안되니 프로젝트도 진행이 안되고, 결단이 필요하다 싶어 로그인 방식을 세션에서 simple jwt로 변경하기로 했습니다.
그게 저녁 아홉시쯤일까요? 이틀연속 철야가 확정되는 순간이였습니다....ㅠㅠ 로그인 부분에서 백과 프론트 모두 제가 맡고 있었고 이전 프로젝트에서 simple jwt를 사용하면서 프론트에서 처리해줘야할 부분들에 대해서도 알고 있었기 때문에 실마리도 안잡히는 문제를 해결하는 것보다 로그인 방식 자체를 바꾸는게 확실하고 빠를거라는 판단을 할 수 있었습니다. 지금 돌이켜보니 기존에 로그인을 맡고 계셨던 팀원분이 그대로 계셨다면 다른 선택을 했을지도 모르겠네요. 허허
django에서 simple jwt 사용하는 법은 굳이 안적고 바로 프론트쪽 로직이나 적어보겠습니다.
simple jwt를 이용한 로그인 방식 사용 시, 프론트는 API요청 시 Authorization: Bearer access token 형식으로 access token을 요청 헤더에 담아줘야한다. 토큰의 탈취 문제로 access token은 굉장히 짧은 삶을 산다. 그렇다보니 프론트에서는 access token이 만료되었을 때, refresh token을 사용해서 새 생명을 불어넣어 주는 일도 해줘야 한다.
우리팀은 API호출하는데 axios를 사용하고 있었기 때문에 이러한 과정을 axios의 interceptors를 이용해서 처리해 줄 수 있었다.
instance.interceptors.request.use((config) => { const access_token = localStorage.getItem('access_token'); if (access_token) { config.headers['Authorization'] = `Bearer ${access_token}`; } else { return config; } return config; });
우선 위 코드를 통해 로그인하며 로컬호스트에 저장한 access token을 헤더에 담아주고
let refresh = false; instance.interceptors.response.use( (resp) => resp, async (error) => { if (error.response.status === 401 && !refresh) { refresh = true; const refresh_token = localStorage.getItem('refresh_token'); const response = await instance.post( '/api/v1/token/refresh/', { refresh: refresh_token, }, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, }, ); if (response.status === 200) { error.config.headers['Authorization'] = `Bearer ${response.data['access']}`; localStorage.setItem('access_token', response.data.access); localStorage.setItem('refresh_token', response.data.refresh); return instance(error.config); } if ( response.response?.status === 400 || response.response?.status === 401 ) { localStorage.clear(); return; } } refresh = false; return error; }, );
응답의 status가 401일 때, access token이 만료된 것으로 보고 refresh token을 이용해 재발급 요청, 성공적으로 재발급 받았을 때는 재발급 받은 access token을 헤더에 담아 기존 401을 반환받은 요청을 다시 보내주는 식으로 로직을 짜줬다.
만료되지 않은 access token을 이용한 요청보다야 약간 더 시간이 걸리긴 했지만, 사용자 모르게 백그라운드에서 access token을 refresh 하는 방식이 이것 밖엔 없는 것 같아 그대로 진행했고, 혹시라도 refresh token도 만료된 경우엔 로컬스토리지를 클리어 시키는 방식으로 다시 로그인하게 만들어 주었다.
다행히 simple jwt로 로그인 방식을 바꾼 뒤에는 다 정상적으로 작동을 했고, 철야한 보람...이 있는 일이였던 것 같다. 좀 더 결단이 빨랐으면 철야는 안해도 됐을텐데
로그인 방식을 바꾸면서 이전에 띄웠던 테스트서버는 굳이 필요 없어졌지만, 이왕 띄워 놓기도 했고 github에 코드 올리면 자동으로 도커 이미지까지 도커허브에 올려주게 만들어놨으니 그냥 이미지 공장 삼자 싶어서 내버려두고 프로젝트를 진행했다.
실제 배포 시에도 테스트서버에서 만들어진 도커 이미지를 활용해서 빠르게 배포를 진행할 수 있었다.
너의 모든 일정의 아키텍쳐이다. github actions를 제외하면 테스트서버와 같은 구조를 가지고 있다.
이미지도 테스트서버에서 만들어진 이미지를 가져다 사용하고 구조도 같은데 왜 굳이 프로덕트용 서버를 따로 뒀냐? 테스트서버는 github에 새로운 커밋이 push되기만 하면 자동으로 해당 코드를 받아오게 되어있는데, 따로 테스트 코드가 없어 에러 나는 코드라도 그냥 적용이 되어버린다.
나중에 유지보수적인 측면을 생각했을 때, 그냥 github에 push 했다가 잘못된 코드가 올라가서 서비스 자체가 먹통이 될 수 있기 때문에 서버를 따로 분리했고 테스트코드가 없는 대신 테스트서버에서 수동으로(?!) 테스트 해본 후에 정상적으로 작동하면 해당 이미지를 가져다가 프로덕션 서버에 적용시키는 방식을 사용하기로 했다.
이번 프로젝트에서는 github actions를 사용해 나름의 CI/CD(라고 해도되나 싶지만)도 구축해봤고 프론트엔드에서 jwt 핸들링도 해봐서 개인적으로는 배운게 많은 프로젝트였다.
세션을 통한 로그인이 참.. 쉬울거라 생각한 부분에서 고꾸라지니까 당시엔 힘도 좀 빠지긴 했는데, 지금은 대체 왜 그랬는지가 제일 궁금하다. 뭐가 문제인지 아시는 분은 댓글로 알려주세요 ㅠㅠ
끝나고 블로그에나 적지만, 사실 프로젝트 자체는 완성도가 떨어진다고 생각돼서 아쉽움이 크다. 그래도 팀원들도 각자의 위치에서 노력을 많이 해준걸 알고 있어서 더 채찍질하기가....ㅎ
여담이지만 이번 팀원들이 풀스택 지향이라 기존 본인 포지션에 반대되는 포지션을 맡았다고 했었는데, 셋 중 두명이 풀스택이 아니라 그냥 전향을 해버렸따..ㅋㅋㅋㅋㅋㅋㅋ 정말 사는게 알다가도 모르겠구만.
이번 프로젝트의 회고는 짧게 마치겠습니다! 내일부터는 오랜만에 개인 프로젝트를 진행할 것 같네요. 프로젝트가 끝나는데로 돌아오겠습니다. 그럼 이만 뿅!