All Articles

다노 매거진 리뉴얼 프로젝트 회고

매거진 리뉴얼 프로젝트

프로젝트 요약

  • 프로젝트명 : 다노 매거진 리뉴얼 프로젝트
  • 프로젝트 소개 : 다노앱 내 4개 탭 중 하나인 매거진 탭 전면 개편
  • 기간 : 2020년 9월 ~ 11월, 약 2개월 반
  • 인원 : 프론트엔드 1명, 서버 1명
  • 역할 : 서버
  • 담당 업무

    • 커스텀 장고 어드민으로 매거진 어드민 제작
    • 인증, 북마크, 좋아요 기능이 들어간 매거진 API 생성
    • 특정 페이지 캐시 적용
    • Celery, RabbitMQ로 배치 작업 처리
    • Docker 환경 설정
    • AWS S3, CloudFront 연동해 CDN 적용
    • Slack logger로 에러 트래킹
    • 라이브 이후 기능 추가 및 유지보수
  • 프로젝트 구성

    • Python
    • Django
    • MySQL
    • Redis
    • Celery + RabbitMQ
    • Docker
    • S3 + CloudFront
    • jQuery + 약간의 JavaScript
    • Ajax
  • 최적화된 결과물은 ‘다노’ 앱 내 매거진 탭에서 확인하실 수 있습니다만, 웹으로도 확인하실 수 있습니다. 웹으로 확인하기

기억에 남는 작업

이미지 상품 태그 어드민

이미지 상품 태그 최종 결과물

(+) 버튼을 터치(클릭)하면 지정한 상품 판매 페이지로 이동한다

인스타그램 쇼핑태그, 이케아 등에서 볼 수 있는 이미지 위 상품 태그 기능을 쓸 수 있도록 어드민을 만드는 작업이 가장 오래 걸린 작업인 동시에 매우 고심했던 작업이다.

비슷한 라이브러리가 있을 것이라는 기대와는 달리 첫 단서를 찾는 데에도 상당히 오래 걸렸다. 클릭했을 때 그 위치에 마커가 찍히고 좌표도 프린팅할 수 있는 이 코드를 찾았을 때 이거면 다 됐다고 생각했지만!!!! 더 큰 벽이 기다리고 있었다. 장고 어드민에 이식해서 이미지 정보와 좌표 정보가 DB에 저장되도록 만드는 것이 어려웠다. 그리고 사용성을 생각해 적당히 보기 좋게 만드는 것도. 장고 기본 어드민 커스텀에도 한계가 있어서 쓰기 적당한 수준으로 만드는 것도 꽤 공을 들여야했다.

처음에는 별도 창을 띄워 태그 좌표 정보를 입력하게 하려고 했는데 장고 어드민과 함께 쓰려니 그게 더 어려웠다. 팝업을 띄우는 것까진 했지만, 그 팝업 창에서 입력한 데이터를 DB에 저장하고, 장고 어드민 화면에 저장한 데이터(좌표)를 띄워줘야 하니 그 연결고리를 어떻게 구현할지 답을 찾지 못한 상태였다. 우선 장고 어드민에 이식하는 건 나중 문제로 미뤄두고, 해야 할 일을 잘게 쪼개 목록을 만들었다.

  1. 이미지를 클릭했을 때 해당 위치 좌표를 저장하고 그 위치에 마커 찍기
  2. 마커 삭제 기능
  3. 마커를 삭제했을 때 마커 고유번호 갱신(상품 1,2,3에 해당하는 마커를 찍었을 때 상품 2를 삭제하면 상품 3이 상품 2가 되도록 번호를 다시 부여하는 것)

각각 내용을 짚어보자면,

  • 이미지를 클릭했을 때 해당 위치 좌표를 저장하고 그 위치에 마커 찍기

    이미지를 <canvas></canvas> 위에 올리고 캔버스 위에서 클릭 이벤트가 발생한 위치 좌표를 읽는다. 장고 어드민 기본 GNB나 기타 메뉴 영역 때문에 사진 위 가로 100px 세로 100px 위치에 찍었더라도 메뉴 영역만큼 좌표가 밀려찍혀서 위치를 보정하는 작업도 필요했다.

    사용하는 단말기에 따라 뷰포트 사이즈가 달라질 것이므로 전체 이미지 사이즈 대비 좌표 위치로 상대 좌표 정보도 절대 좌표 위치와 함께 저장한다. API 요청 시에는 상대 좌표 정보를 보내준다.

    이미지 어느 위치에 어느 상품을 연결한 마커를 찍었는지 어드민 사용자가 알아야 하므로 좌표 정보는 어드민 페이지 상 마커와 마커 고유번호를 찍는 데에도 이용한다.

  • 마커 삭제 기능과 마커 고유번호 갱신

    마커를 추가할 때는 번호를 차례로 증가시키면 되므로 구현이 어렵지 않았다. 하지만 마커를 삭제할 때는 더 까다로웠다. 가장 최근에 추가한 마커를 삭제할 때는 마지막 번호만 삭제하면 되지만, 가장 첫 번째 마커나 중간 마커를 삭제하면 번호가 비어버리는 문제가 있었다.

    사실 시간에 쫓기기도 했고 작업 난이도도 있어보여서 고유번호는 변경하지 않는 방향으로 하려고도 했는데, 상품 1, 상품 2, 상품 5, 상품 8 이렇게 이빨 빠진 모양새가 보기 좋지 않고 사용성도 해치기에 마커 삭제 시 번호를 갱신하기로 결정했다. 이 기능에서 한동안 막혀, 다시 작업을 세분화했다.

    1. 일단 삭제 버튼 클릭 시 좌표 정보와 연결 상품 정보가 들어간 input 박스 제거, 이미지 위 마커 제거까지 구현한다.
    2. 삭제 후 남아있는 마커 수를 다시 세서 가장 첫 번째 찍힌 마커부터 다시 번호를 매긴다.

    마커에 해당하는 element에 마커 고유번호로 id를 부여해줬으므로, 삭제 버튼 클릭이벤트 발생 시 고유번호로 해당하는 element id를 찾아(getElementById) remove() 해줬다. 남은 마커를 다시 넘버링하기 위해 남은 마커 수를 가져와 반복문을 돌리면서 고유번호를 다시 부여했다.

이미지 태그 주요 기능 구현 과정

이미지 태그용 어드민 초~중기 모습. 아직 넘버링 새로고침을 적용하지 않았을 때.


이미지 태그 주요 기능 구현 과정

좀 더 깔끔하게 장고 어드민 기본 input box, button 모양으로도 수정해봤다 (지친 고양이=나)

주요 기능을 구현한 후에는 장고 템플릿에서 change_form을 상속받아 어드민에 이식했다. 캔버스 위에서 읽어온 좌푯값은 Ajax를 이용해 DB에 저장했다. 해당 어드민 페이지를 불러올 때는 GET 메소드로 DB에 저장된 좌표 정보를 불러와 화면을 구성하도록 짰다. 이렇게 jQuery와 JavaScript, Ajax를 조합해서 어드민을 완성했다.

시간에 쫓겨 코드 효율이나 가독성, 그리고 더 좋은 개발 방향을 충분히 숙고하지 못한 것이 아쉽다. 하지만 큰 문제를 잘개 쪼개 하나씩 해치워나가면서 큰 기능을 완성했다는 점에선 점수를 주고싶다. 나름 UI/UX 디자인을 공부한 예비 디자이너였던 사람으로서 사용성도 상당히 고려했다. 장고 어드민이라는 한계 안에서 어렵사리 완성한 결과물을 무척이나 전시하고 싶지만 대외비 영역이라 판단했으므로… 마음속에 묻어둔다.

사용자 인증 기능

다노 앱에서는 아이디 하나로 마이다노, 다노샵, 매거진 서비스를 이용할 수 있다. 매거진은 웹으로 접속해 비회원 상태로도 이용할 수 있지만, 앱에서는 북마크, 좋아요 기능을 이용할 수 있다. 북마크, 좋아요 기능을 위해 다노 회원 정보를 받아와 이 회원이 유효한 회원인지 판별하고, 유효한 회원일 경우 해당 회원의 북마크 글 정보, 좋아요 정보를 저장하고 불러오는 API를 구현했다.

회원 테이블과 글 테이블을 참조하는 북마크 테이블 및 좋아요 테이블을 생성해, 데코레이터로 회원을 판별한 뒤 회원 정보와 글 정보를 저장해 이용했다.

Celery, RabbitMQ를 이용한 작업 스케줄러 세팅

매거진에서 필요한 데이터 중 다노샵에서 주기적으로 가져와야할 데이터가 있어서 작업 스케줄러가 필요했다. 어떤 작업(특정 API에서 데이터를 긁어오는 것)을 반복해서 실행하도록 하기 위해 cron과 Celery + RabbitMQ 조합 중 어떤 것을 쓸 것인지 먼저 결정해야 했다. 각 방식의 장단점이 무엇인지 사수님이 이야기해주셨는데 몇 개만 적어보자면,

cron

  • 장점: 세팅이 간편하다.
  • 단점: 설정한 시간에 무조건 실행하므로 어떤 이슈가 있을 때는 이전 작업과 동시에 돌 수 있다.

Celery + RabbitMQ

  • 장점: cron 단점 상쇄
  • 단점: (선택사항이지만) 안정적 운영을 위해 컨테이너도 따로 띄워야 하고 서버도 따로 둬야하므로 세팅이 어렵다.

그리고 매거진이 처한 상황은 이랬다.

  • 데이터를 긁어오는 작업이 에러로 잠시 멈춘다고 해도 매거진 서비스에 크리티컬한 영향을 끼치는 것이 아니다.
  • 따라서 매거진 서비스의 안정적인 운영을 위해서는 배치 작업을 분리할 필요가 있다.

따라서 이 상황에 더 맞는 Celery를 사용했다.

매거진에 적용하는 배치 작업은 규모가 작아 메인 서버에 함께 포함할 수도 있었지만, 문제가 생겼을 경우 메인 서비스에까지 영향을 미칠 수 있으므로 잠시 죽어도 전체 서비스에는 큰 영향이 없도록 Celery용 서버를 분리했다. 같은 이유에서 컨테이너도 분리했다.

작업 내용은 API에서 데이터 긁어오는 게 전부였기 때문에 task 코드를 짜는 것 자체는 어렵지 않았는데 도커로 띄우는 과정에서 많이 헤맸다. 최초 개발환경 세팅을 도커 기반으로 하지 않아서 뒤늦게 라이브 환경과 동일하게 맞추려다보니 compose 파일이 무엇인지, 어떻게 구성해야 하는지 같은 아주 기초적인 부분 파악하는 데에 시간을 많이 잡아먹었다. 처음에 mysql 버전을 8로 세팅해서 5.7로 억지로 다운그레이드 한 것이 발목을 잡은 부분도 크다. 스펙 정의는 사전에 철저하게 해야함을 배웠다.

참고) 내가 기억하기 위해 정리하는 Celery와 RabbitMQ 간단 개념

celery는 비동기 작업 대기열이고 rabbitmq는 celery와 함께 사용하는 메시지 브로커다. 쉽게 말해 작업(task)을 수행하는 일꾼(celery worker)가 필요한데, 이 일꾼에게 작업을 나눠주는 것이 브로커(rabbitmq)의 역할이다.

Celery와 RabbitMQ 동작 도식

Broker
Broker(RabbitMQ)는 작업 큐를 생성하고 큐에 작업을 발송한다. 큐에서 worker로 작업을 전달한다.

Consumer(Celery Worker)
Consumer는 작업을 수행하는 Celery Worker다. 다뤄야 하는 작업 양에 따라 워커를 하나 이상 둘 수 있다.

설명 출처 - Python Celery & RabbitMQ Tutorial

즉 크롤링 작업(task)용 코드를 짜고 그 태스크를 worker가 일정 주기로 실행하도록 queue에 쌓게 만들었다. celery beat가 5분 단위로 돌도록 설정해놓았고 작업에 문제가 생기면 슬랙으로 알람이 오도록 했다. 지금까지 발생한 대부분 에러는 (아마 네트워크로 인한) 일시적 현상으로, 큰 문제없이 잘 돌고 있다는 점에서 안심. 방화벽 문제로 DB에 접근하지 못했을 때는 대차게 에러메시지가 오긴 했다.

최적화하기

매거진에는 사진, 영상, 움짤 등 다양한 미디어가 한 페이지에 있는 경우가 많아, 로딩 시간을 단축할 수 있도록 신경썼다.

  1. gif 대신 mp4 쓰기
  2. Redis를 이용해 캐시 적용
  3. CloudFront를 이용해 CDN 적용

  • gif 대신 mp4 쓰기

    품질이 썩 좋지도 않으면서 파일 크기만 큰 gif 대신 mp4를 쓸 수 있도록 mp4 파일 첨부 시 뮤트, 자동재생, 반복재생 되도록 처리했다. 또한 WYSIWYG 에디터에 YouTube 플러그인을 추가해 비디오를 좀 더 쉽게 첨부할 수 있게 했다.

  • Redis를 이용해 캐시 적용

    메인 페이지, 글 상세 페이지 등에 캐시를 적용했다. 페이지 최초 로딩 시 캐시 키를 생성해 저장했다가 다음 로딩 시 해당 캐시 키로 데이터를 읽어온다.

    전체 캐시는 일정 시간이 지나면 일괄 삭제된다. 일정 시간이 지나지 않았더라도 배너를 추가/수정한다거나 글을 작성/수정했을 때, 바뀐 내용이 즉시 반영되어야 하므로 캐시가 깨진다. 만약을 대비해 어드민 페이지에 전체 캐시를 수동으로 삭제할 수 있는 메뉴도 만들었다.

  • CloudFront를 이용해 CDN 적용

    취업 준비하던 시절 S3에 이미지 업로드 테스트를 해보다 CloudFront라는 게 있다고 해서 연동해봤던 적이 있다. 이번에 스토리지를 S3로 세팅하다보니 자연스럽게 그때 일이 생각났고 이미지 서빙 속도가 빨라진다고 해서 S3랑 CloudFront를 연동해봤는데 매거진 스토리지에도 한번 적용해볼까요? 라고 화두를 던졌다.

    알고보니 다른 서비스에서는 CDN을 적용했는데 매거진 작업에서는 시간도 없고 서버를 맡은 나도 초보고 하니 사수님 생각에 CDN은 일단 나중 문제였던 것이다. 그제서야 슬금슬금 피어나는 CTO님과의 면담 내용… 캐싱을 이용해 콘텐츠를 더 빨리 불러올 수 있도록 하는 것이 있고 그것을 세팅해주는 것도 필요하다고 하셨었지… 찾아보니 CloudFront가 바로 그 CDN이었고 그렇다면 안 쓸 이유가 없지 않냐는 합의가 이루어져 뚝딱 적용했다.

잘한 점

프로젝트 완주

매거진 개편 후 메인화면

진짜 너무 영롱하다

인턴이 한 프로젝트를 처음부터 끝까지 끌고갈 수 있는 기회가 얼마나 될까. 정말 귀중한 기회였고 이것을 허무하게 날리고 싶지 않아서 두 달 반을 정~~~말 아등바등 열심히 달렸다. 부족한 실력을 시간으로 메우려고 야근, 주말 출근, 추석 연휴 (하루 빼고) 출근 그 외 빨간날 출근… 실력이 부족해 잔업을 하는 거라 정규 근무시간 외에도 일한다는 것이 부끄러울 때도 있었다. 내공과 경험이 더 있었다면 기간을 단축할 수 있었겠다는 아쉬움은 있지만 포기하지 않고 최종 프로덕 배포까지 했다는 것이 자랑스럽다.

아래 아쉬운 점에서 기술할 내용이지만 검색기능 고도화, 성능 고도화 작업 등은 배포 전에 미처 못 했지만 개발 요건에 있었던 필수 기능들은 모두 구현했다.

문제 해결 방식

구현할 수 있는 기능은 1레벨 수준인데, 5레벨쯤 되(어보이)는 기능을 보니 과연 할 수 있는 것인지 난감했다. 이럴 때 기능을 쪼갤 수 있을 때까지 쪼개고, 가장 쉬운 것부터 구현했다. 지금까지 구현한 기능을 붙이고 다시 작은 기능을 구현해 붙이는 것을 반복하니 엄두가 나지 않았던 큰 덩어리를 완성했다. 문제를 잘게 쪼개는 것이 나에게 맞는 해결방식임을 배웠다.

아쉬운 점

설계 리뷰

설계 리뷰라는 것을 아예 모르고 냅다 달려들었다가 프로젝트 중반 쯤에 그 존재를 알았다. 최초 설계 리뷰 시 받을 피드백이 두렵고 설계 리뷰 준비, 피드백 반영 때문에 초반에는 시간이 오래 걸리겠지만, 오히려 개발 과정에서 시행착오를 상당히 줄일 수 있을 것 같다. 굵직한 프로젝트에서는 꼭 설계 리뷰를 해보고 싶다.

성능

목으로 나는 비둘기를 아시는지. 프로젝트 진행하는 동안 입에 달고 살던 말이다. 기한이 있는 일이니 일단 어떻게든 돌아가게만 만들어 놓자는 목표로 성능 생각하지 않고 달렸다. 단순하게만 구현해놓은 검색 기능과 사이트 전체 성능을 신경쓰지 못한 것이 아쉽다. 라이브 극초반에 피드백을 받고 테스트용 슬랙 로깅을 끈 것이 전부다.

기록

전력투구 하느라 기록할 시간이 없었다. 지금 와서야 더듬더듬 기억을 되살려보는데, 곱씹어볼만한 많은 부분을 놓친 것 같아 아쉽다.