[프로젝트 회고] 뮤빗라이브 1차 회고

최관수·2024년 5월 12일
3

회고

목록 보기
3/3

https://live.mubeat.tv/

최초 설계

리액트 개발자가 없는 곳

  • 기술 면접에서 개발팀 구성에 대해 질문을 했었다. 클라이언트 5명, 서버 3명으로 구성되어 있다고 답변 주셨고, 어쨌든 사내에 운영 중인 웹사이트가 있으니 1명 내지 2명 정도는 웹 프론트 개발자가 있을 거라 생각했다. 입사해 보니 클라이언트 5명은 모두 앱 개발자였고 회사에는 리액트로 개발된 프로젝트가 없었다. 역시 뇌피셜은 위험하다.

Windows에서 MacOS로

  • 개발팀은 전부 맥북을 쓰고 있었고, 물론 내게 Windows를 선택할 수 있는 기회가 있긴 했다. 하지만 전부터 MacOS 환경에서 개발을 해보고 싶기도 했고, 굳이 Windows를 취할 만한 이유도 없었다. 회사에서 m1 맥북 프로를 지원해줬고, 더 빠르게 적응하기 위해 개인 랩탑도 동일한 사양의 중고 맥북을 구매했다. 초기 일주일 정도는 단축키 때문에 애를 좀 먹었지만, 개발 환경 구축 과정에서 상대적으로 빠르게 설정할 수 있는 지점들이 있어서 좋았다.

VSCode에서 WebStorm으로

  • 난 몇 년 전 어떤 계기를 기점으로 성향이 많이 바꼈다. 물론 지금도 도전적인 누군가에 비하면 보수적이고 안정지향적이지만, 이전에는 적응이 필요한 변화 자체를 좀 줄이고자 했다면, 지금은 변화를 통한 성장을 우선 취하고자 한다. 실제로 그런 선택은 초기에 내재된 방어기제 때문에 생기는 숙명적인 거부감을 맞이하게 되지만, 그 거부감을 이겨내고 나면 좋은 경험으로 남기 마련이었다.
  • 회사에서는 IntelliJ를 쓰고 있었고, JavaScript를 쓰는 내겐 WebStorm과 VSCode 두 가지 선택지가 있었다. MacOS에서 VSCode 사용 자체가 문제가 없다는 건 부트캠프 동료들을 통해서 확인하기도 했고 Extensions에 이미 익숙해져 있었지만, 기회가 닿았을 때 WebStorm을 경험해보는 것도 좋다고 생각했다. IDE도 결국 도구일 뿐이라는 가벼운 마음으로 써봤으나 초기에 단축키는 역시 익숙하지 않았다. 사실 지금도 TIL 작성은 VSCode를 사용하고 클라우드 IDE는 VSCode와 단축키가 같아서 가끔 혼동하긴 하지만, WebStorm이 독자적으로 제공하는 기능들은 좋은 경험이긴 했다. 다만 회사에서 WebStorm을 제공해주지 않는다면 다시 VSCode를 사용할 것 같다. 다양한 기능들보다 묘한 무거움과 가끔 나오는 잔버그들 때문인지 DX가 유쾌한 느낌은 아니다.

개발 환경 설정과 초기 라이브러리 선택

CRA? Vite?

  • 앞서 말했던 것처럼 React로 구성된 프로젝트가 없었기 때문에 별도의 개발 환경 설정이 필요했다. 관련 빌드 도구는 Vite로 흐름이 넘어가는 추세라는 걸 인지하고 있었지만 첫 프로젝트인 만큼 익숙한 CRA를 취하는 게 좋다고 생각했다(지금 돌이켜 보면 사실 상관이 없어서 Vite를 취할 것 같긴 하다). 추후에 마이그레이션을 고려할 수도 있었기에 관련 아티클을 찾아봤는데, 생각보다 복잡하지 않아서 고민 없이 선택할 수 있었다.

README

  • README는 이전 회사의 경우 별도의 컨벤션이 없었고, 그간 개인 프로젝트나 팀 프로젝트에서는 간소하게 적어왔기 때문에 크게 신경을 못, 아니 안 쓰고 있었다는 표현이 맞다. 사수님이 README는 가급적 히스토리를 모르는 사람이 봐도 이해할 수 있도록 작성해달라 요청 주셨고, 그제서야 사내 다른 저장소의 README를 살펴보았다. 간소하지만 컨벤션이 있었고 그걸 기반으로 하나씩 작성하면서 별 생각없이 봤던 각각의 Semantic Versioning도 다시 보게 되고, 전체적인 개발 환경 설정의 흐름도 짚어보게 되면서 동시에 라이브러리의 라이센스도 다시 살펴보는 계기가 되어서 좋았다.

ESLint & Prettier

  • 물론 유명한 eslint-config-airbnb 같은 것들이 있지만 내가 잘 모르는 설정까지 그냥 넣어두고 쓰는 게 마음이 쓰였고, 애초에 명확한 컨벤션이 없고 거의 혼자 개발하게 되는 영역인데 불필요한 제약 사항을 추가할 필요는 없다고 생각했다. 그래서 추후에 필요할 때 하나씩 추가하자는 생각으로 최소한의 설정값만 세팅해두었다.

CSS 프레임워크 선택 - SCSS

  • 가장 먼저 고민한 건 CSS 프레임워크였다. 내게 가장 익숙한 건 애초에 SCSS로 다 설계하는 거였고, 클래스로 제어하는 프레임워크의 경우 초기 설계는 쉽지만 별도의 디자인을 적용하기 위해 커스텀하는 데에 공수가 들어가는 점이 신경 쓰였다.
  • 회사에 React 기반의 프로젝트는 없었지만 기존 Django 기반의 프로젝트는 Bootstrap을 사용하고 있었고, Bootstrap을 써본 경험도 있고 내가 편한 거보다는 회사에 맞추자는 의도로 초기 세팅을 해두었다.
  • 하지만 Bootstrap은 개발 당시 단순히 취했던 기술 스택이기도 하고 기존 웹들은 내 입사로 대체될 가능성이 높았다. 커스터마이징에 대한 부담이 있었기에 결국 SCSS 환경으로 재구축하였다. 물론 tailwind에 대한 고려도 있었다. 다만 HTML(React에서는 JSX가 해당되겠지만) Markup 영역의 가독성이 안 좋은 것도 그 나름의 문제고, CSS 자체에 대한 부담이 없는 내겐 자유도가 높고 익숙한 SCSS가 더 나은 선택지였다. CSS-in-JS는 최근엔 런타임 성능 비용이 높다는 인지도 커지고 있었고, 물론 zero-runtime 방식도 취할 수 있다지만 애초에 DX가 좋았던 기억이 없어서 제외했다.
  • 혹여나 추후 tailwind로 대체될 가능성을 열어두고 별도 세팅을 하고, breakpoint나 변수는 tailwind 기반으로 세팅해서 가져다 쓸까 생각도 해봤지만, 불필요한 번들사이즈를 먹기도 하고 SCSS 환경에서도 대부분 가능하기에 추후 삭제했다.

Video.js

  • 프로덕트 내 주요 BM이 비디오 상품의 판매였고 기존에 클립을 재생하고 있는 Django 기반의 웹사이트가 있었는데 거기서 취하고 있는 라이브러리가 Video.js였다. 리액트 친화적인 다른 라이브러리가 있었으나 사실 그 당시엔 m3u8이나 ts, segment 같은 것들에 대한 이해가 거의 없을 때라 일단 기존 코드를 참고하려면 같은 라이브러리를 취하는 게 맞다고 생각했다.
  • 하지만 이건 양날의 검이기도 했다. Video.js는 오래된 라이브러리인 만큼 자료나 플러그인이 방대했지만 망망대해에서 원하는 물고기를 건져야 하는 느낌이었고, React 기반의 예제 또한 부족해서 적합한 예제를 찾았다 하더라도 React에서 바로 작동하기 어려운 구조거나 별도의 수정을 거쳐야 하는 경우가 많았다. React 친화적인 라이브러리라고 느끼진 못했고, QA 종료까지 수정에 수정을 거듭할 수밖에 없었다.

Day.js

  • 이전 회사에선 moment.js를 썼지만 이미 deprecated 됐고 번들사이즈도 적은 Day.js를 선택했다.

Axios

  • Fetch API를 우선 사용하고 추후 필요한 시점에 취할까 하다가 이전 회사에서 사용했던 터라 코드 구조에도 익숙해서 선택했다. 이건 프로젝트 도중에 알았지만 별도의 intercepter에서 에러 핸들링을 하거나 refresh token을 제어할 수 있다는 점에서 다양한 옵션을 취할 수 있었다.

Redux Toolkit? Recoil? Zustand?

  • RTK는 상대적으로 가장 많이 써봤지만 사용 당시 개발 경험이 좋지 않았던 건 코드량이 불필요하게 많게 느껴졌기 때문이었다. 그리고 FLUX 패턴에 대한 이해가 부족해서 RTK 자체가 어렵게 느껴지기도 했다. 우선 가장 사용 빈도가 높았던 RTK로 세팅.
  • 단, 본격적으로 개발에 들어가기에 앞서 전역 관리를 위한 라이브러리를 다시 한번 체크해봤고 Recoil, Zustand가 최종 후보였으나 러닝커브가 상대적으로 낮은 Recoil을 선택했다. 아직 정식 버전이 안 나왔고 유지보수의 텀이 길다는 우려가 있긴 했지만, 어쨌든 Meta가 관리하는 라이브러리라는 점이 유효했다. 동시에 캐싱이 필요한 시점에 TanStack Query를 도입할 생각이 있었기 때문이기도 했는데, TanStack Query 적용 범위가 늘어날 수록 Recoil은 덜어낼 수 있다는 판단이었다.

gitignore는 IDE도 고려해야 한다 - WebStorm의 케이스

  • 앞서 말한 것처럼 VSCode만 사용해왔기 때문에 .gitignore는 프레임워크나 언어 정도만 고려하면 되는 줄 알았다. https://www.toptal.com/developers/gitignore 를 통해서 최초 .gitignore 세팅을 했지만 IDE 키워드는 따로 넣지 않았다.
  • IntelliJ를 써본 사람은 알겠지만 루트에 별도의 .idea라는 폴더가 생성된다. 해당 유저의 검색, 사용 기록 등을 기록하는 파일인데 당연히 세팅된 환경이나 사용자에 따라 달라지는 파일이기 때문에 버전 관리를 할 필요가 없다. 이 폴더에 대한 인지가 전혀 없던 와중에 git initgit push로 원격 저장소에 해당 폴더가 올라가게 되었고, 그 찝찝함에 저장소를 다시 만들고 IDE 정보를 넣은 .gitignore를 재생성해서 세팅했다.

초기 폴더 구조 설계

  • 초기 단계에선 이 부분에 대한 고민이 가장 컸다. 이전 회사에서 접했던 Vue 2와 Vue 3, 사이드 프로젝트에서 다뤘던 React, 부트캠프에서 진행했던 Next.js의 구조는 비슷하면서도 다른 부분이 있었고, 개발자 혹은 취하는 디자인 패턴에 따라 달라질 수 있는 부분이었다. 기존에 React로 진행한 프로젝트가 있다면 해당 폴더 구조를 일종의 컨벤션 삼아 참고하면 되겠지만 앞서 말했듯이 참고할 만한 프로젝트는 없었다. 그리고 실제로 Vue 2에서 간단한 컴포넌트 설계는 퍼블리셔인 내가 할 수도 있었는데, 잘못된 컴포넌트 설계 때문에 뜯어 고쳤던 경험이 있었기에 더 조심스러울 수밖에 없었다. ChatGPT에게 답을 구해보기도 하고 다른 리액트 프로젝트의 저장소를 뒤져가며 조금씩 구조를 잡아갔다. 두 가지에 주안점을 두었는데, 일단 내가 명확히 납득하고 빠르게 이해할 수 있는 구조여야 한다는 점과 목적에 맞게 구분 지어져야 한다는 점이었다. 이 두 가지는 결국 유지보수성이라는 하나의 목적을 기반으로 생각했던 것인데, 추후 내가 아닌 누군가 이 코드를 보더라도 명확하게 이해할 수 있었으면 하는 바람과 동시에, 내가 이 구조를 누군가에게 설명해야 한다면 조금이라도 더 명확하게 할 수 있는 구조이기를 바랬다.
  • 이전 프로젝트에서는 아토믹한 패턴을 사용하지 않았고, 로직이 길어지면서 코드의 가독성이나 유지보수성이 안 좋아졌던 경험을 했던 터라 이번엔 버튼 하나라도 로직에 따라 일부 아토믹하게 쪼개보는 게 좋겠다고 판단해서 적용했다.

웹폰트 협의

  • 이미 구축된 웹사이트의 경우 웹폰트가 적용되어 있지 않거나 명확한 컨벤션이 존재하지 않았다. 담당 디자이너님에게 회의를 요청해서 컨벤션의 필요성을 말씀드리고 영문은 Poppins, 그 외 언어는 Pretendard로 협의했다. 그 외 breakpoint나 담당 디자이너님이 원하던 레퍼런스 페이지 체크 등 별도의 소통이 필요한 부분들도 작업 전에 미리 논의했다.

KEEP

  • 가장 좋았던 경험은 최초 개발 환경에서부터 라이브러리 세팅까지 하나하나 해봤다는 점이다. 선택의 기로에 섰을 때 어떤 근거로 취해야 하고, 그 근거가 어떤 흐름에서 합리적이어야 하는지 사고할 수 있는 경험이었다. 이전 회사에서는 기존 프로젝트를 clone 떠서 일일히 세팅할 필요가 없었고, 부트캠프는 같은 목적으로 여러 명의 개발자가 협업을 하기 때문에 스스로 모든 걸 체크할 필요도, 결정할 필요도 없었다. 내가 맡은 영역에서만 명확하게 체크하면 될 일이었고, 그 외에는 주도적으로 리서치한 사람을 기점으로 논의를 거쳐서 결정하면 될 일이었다. 하지만 clone 뜰 React 프로젝트가 없고, React와 관련된 세부적인 논의를 할 사람이 없는 상황에서는 스스로 감내해야 할 것이 많다. 스스로 리서치하고, 리서치 결과를 검증하고, 필연적으로 결정해야 하고, 그 결정을 내린 나에 대한 의심과 싸워야 하고, 끝내 의심의 터널을 거쳐 최종 결정에 도달해야 한다. 사실 인생도 비슷한 거 아닌가. 긴 터널을 통과했지만 유익하고 값진 경험으로 받아들일 수 있었다.
  • alert, dialog, toast, custom select, modal 등 기본 UI 요소를 별도의 라이브러리 없이 직접 구현한 경험이 좋았다. 직접 구현한 이유는 이 또한 커스텀과 연관되어 있는데, 다른 라이브러리를 활용했을 때 확장성이 떨어지거나 커스텀이 번거로운 경험을 해봤기 때문이었다. alert이나 dialog는 전역에서 사용 가능하게 설계해야 했고, 보통 2개 이상의 타입으로 나오는 경우가 많기 때문에 확장성을 고려해서 설계했다. Vue 2에서 이 요소의 컴포넌트 설계 시 여러 개의 타입을 고려하지 못하고 짰다가 불필요한 협의와 누더기 같은 코드로 고생했던 경험이 도움이 되었다.
  • 초기부터 API 호출 로직을 하나의 Custom Hook으로 분리해서 재사용했던 것이 편리했다. 이전에 Vue 2에서는 왜 axios를 통한 API 호출을 모듈화하지 않았는지 모르겠지만, 반복적인 코드 때문에 오히려 가독성이 떨어졌던 경험이 있었다. 그 경험 때문에 애초에 useApi라는 별도의 hook을 만들고 그 hook을 통해 호출하도록 설계했다.
  • 데이터 가공은 백엔드 주도로 하는 편이 낫다. 사실 개발 초기를 돌이켜보면 환율에 맞춰서 프론트에서 금액을 바꿔야 하나, 구매 금액 대비 포인트 적립 등을 프론트에서 계산해야 하나, 이런 생각들을 했던 기억이 나는데, 사수님에게 여쭤 보니 여긴 백엔드 주도로 개발을 한다는 것이었다. 계산과 관련된 로직은 백엔드에서 처리한 후에 API를 통해서 내려주는 방식이었다. 개발이라는 것이 진행을 하다 보면 결국 수정을 거듭하기 마련인데, 이 과정에서 서버에서 데이터가 변동되면 프론트까지 값을 바꿔야 하는 경우가 많다. 특히나 프론트 주도로 개발을 하고 그 과정을 넓은 영역에서 복잡하게 처리하다 보면 휴먼 에러가 발생하기 마련인데, 애초에 그런 경험은 유쾌하지 않았다.

PROBLEM

  • 크로스 브라우징을 놓친 것, 더 정확히 말하면 그 중요성을 얕보고 있었다는 점이 문제였다. 스스로 결정해야 될 고민이 많고 그 이후에 트러블슈팅을 진행하면서 크로스 브라우징은 마지막에 테스트해서 한번에 정리하면 되겠지, 싶은 마음이었던 것이 오히려 마지막에 부담으로 다가왔다. 다른 것보다 라이브러리 영역에서 호환성 이슈가 있었고, Safari야 애초에 사고뭉치라 그렇다쳐도 Firefox에서도 몇 가지 호환성 이슈가 생기면서 꽤나 어질한 경험이었다.
  • 처음에는 나름 추상화하면서 잘 고안했다고 생각한 컴포넌트가 나중엔 생각 이상으로 props가 늘어났다. 하나의 prop을 선언하는 건 쉬운 일이지만 늘어날수록 복잡도가 증가하고, 덜어내는 건 갈수록 더 까다로울 일이 될 수밖에 없다.
  • dummy data를 json으로 사용하려면 백엔드 개발자와 협의 후 선언하는 게 나은 것 같다. 물론 처음엔 단순히 binding 하는 영역에만 dummy data를 사용하려는 목적이었지만, API 작업이 조금 미뤄지면서 결국 dummy data 기반으로 props와 별도의 로직까지 적용했다. 문제는 추후 API가 나왔을 때 dummy data와 일부 구조가 달라지면서 이미 선언해놨던 영역을 수정하는 게 도리어 번거로운 작업이었다.
  • 이전 회사에서 작업할 땐 명확한 요구정의서나 기획이 없다 보니 XD로 만든 디자인 작업물에 별도의 description을 붙여서 작업을 했었다. 그런 경험 때문인지 초기에는 디자인 시안 기준으로 작업을 하다 보니 놓친 세부 기획이 일부 있었다.
  • 기획서상 명시된 요구사항만 체크하거나 단순히 로직 짜는 거에 치중하다 보면 전체적인 UX의 큰 흐름을 놓치는 경우가 있었다.
  • 라이브 스트리밍 자막이 일반 클립과 동일하게 별도의 자막 파일이 적용된다고 생각했는데(잘못된 뇌피셜), 타이밍에 맞게 CMS 상에서 지속적으로 입력하는 기획이었다. 막바지에 해당 기능을 체크하고 나서야 부랴부랴 기능 구현을 하는 바람에 버그도 있었고, 그 과정에서 QA를 진행하는 기획자님을 괴롭히는 일이 되기도 했다. 물론 서비스를 시작한 프로덕트는 아니었고 끝내 수정하긴 했지만, 돌이켜보면 반복되서는 안 될 실수였다고 생각한다. 역시 뇌피셜은 위험하다.

TRY

  • 크로스 브라우징 테스트는 주기적으로 테스트하면서 가져가자. 코드는 지속적으로 변하기 때문에 어느 지점에서 크로스 브라우징 이슈가 생기는지 주기적으로 체크를 해야 나중에 해당 이슈의 해결책을 비교적 빠르게 찾을 수 있다.
  • prop을 추가할 때 한번 더 생각하자. prop을 추가하는 건 5초면 끝나는 일이지만 나중에 prop 하나 제거하는 건 1시간이 걸릴 수도 있을 일이다. 물론 경솔하게 추가한 건 아니지만 2번 고민할 것을 3번 고민하는 태도 자체가 중요하다.
  • API 작업이 아직 되어 있지 않을 때는 아예 dummy API를 받는 게 가장 좋다. 부득히 dummy data로 작업을 해야 한다면 백엔드 개발자와 협의를 통해 어떤 구조로, 혹은 어떤 타입이 될지 개괄적으로나마 인지를 한 상태에서 사용하는 편이 좋을 것 같다. 그래야 불필요한 작업을 덜어낼 수 있다.
  • 기획이 기준이 되어야 한다. TC도 결국 기획서 기준으로 작성되기 때문에 기획서에 명시된 부분을 꼼꼼하게 챙겨서 작업할 필요성이 있다.
  • UX는 역시 생각을 많이 해야 한다. 지엽적인 부분에 치중해 전체적인 UX 흐름을 놓치면 그게 언젠가 부채로 돌아온다. 조금이라도 세밀한 UX의 흐름이 있다면 잠깐이라도 코딩하는 손을 멈추고 전체적인 UX 흐름을 고민해볼 필요가 있다.
  • 명확히 파악하지 못한 기능은 그저 뇌피셜로 뭉갤 것이 아니라 좀 더 꼼꼼하게 살펴볼 필요가 있다. 이미 앱과 기존 웹에 구현되어 있는 기능이어서 신규 기획서에는 명시적으로 적혀 있진 않았지만, 찝찝한 구석을 기획자나 다른 클라이언트 개발자에게 미리 확인했더라면 좀 더 빠른 시기에 발견해서 작업할 수 있었을 거라고 생각한다.

그 외 작업 과정

video.js

  • 사실 초기에 라이브 스트리밍 기능을 리렌더링과 연결지어 생각하진 못했다. 당연히 라이브 스트리밍 기능을 서비스하는 페이지는 중간에 state가 바뀌더라도 리렌더링이 되어서는 안된다. 라이브 스트리밍 채팅 기능의 삭제 로직이 비디오 플레이어 컴포넌트에 영향을 미쳐 리렌더링되는 이슈가 있었고, 고민 끝에 영향을 받지 않는 컴포넌트에서 useState로 선언하는 방식을 취했다. 일부 어쩔 수 없이 구조상 그대로 선언해야 하는 경우는 useState 대신 useRef로 대체 사용하여 리렌더링 없이 기능 구현 가능하게 수정했다.
  • 또 추가로 Alert Dialog나 Confirm Dialog 같은 경우 전역 변수를 통해 관리하고 있었기 때문에 채팅 신고나 삭제 Dialog가 노출될 때 구독 중인 Recoil atom을 기준으로 리렌더링 이슈가 있었다. 별도의 로컬 Confirm Dialog로 컴포넌트를 분리하면서 해결하기는 했으나 근원적인 해결책인지는 다소 의문이 드는 부분이 있다.

감당하지 못한 컴포넌트 패턴

  • 앞서 말한 것처럼 전체적으로 아토믹하게 설계했는데, 문제는 비즈니스 로직이 상대적으로 복잡한 결제 modal이나 채팅이 붙어 있는 라이브 스트리밍 페이지의 경우 props나 Recoil을 통해서 전체 비즈니스 로직을 온전히 짜는 데에 자주 트러블이 생겼다. 물론 그 과정에서 트러블슈팅을 해냈으면 문제 없겠지만 한정된 시간 안에 작업을 해야 되기에 아쉽지만 일부 아토믹 패턴을 포기하고 컴포넌트를 합치는 형식으로 해결하기도 했다.

해외 결제와 PG사

  • 사실 이 부분은 프론트에서 처리할 로직이 복잡하다기보다 서버 개발자님과 소통할 일이 많았다. 애초에 계약 예정인 PG사 기반의 구현되어 있던 로직이 없어서 새로 구현해야 했기 때문에, 몇 가지 케이스를 두고 테스트하면서 response를 조정했다.

트러블슈팅

  • 아이폰 safe area 대응

    • meta tag의 viewport 설정으로 아예 safe area를 없애거나 body 자체에 요소의 컬러와 맞물리는 배경색을 입혀서 자연스럽게 대응하는 방식이 있었다. safe area를 없앨 경우 safe area 주변에 별도의 인디케이터가 들어가는 경우 추가 대응이 필요할 수 있어서 body에 배경색을 적용하는 방식을 취했다.
  • Safari 등 모바일 브라우저에서 100vh 이슈

    • Safari 등 특히 모바일 브라우저 상에서 노출되는 영역(ex) 주소 영역) 때문에 100vh 적용한 element 위를 덮어버리는 이슈가 있었다. 브라우저 상에서 덮어지는 영역을 제외하고 1vh를 재계산하여 다시 렌더링하는 방식을 취했다. 추가로 불필요한 버그를 방지하기 위해 모바일 메뉴가 나올 때는 body의 scrollbar를 없애서 스크롤이 되지 않도록 적용했다.

    • https://velog.io/@edie_ko/Tip-모바일-브라우저에서-100vh-적용-오류-해결-iosandroid

  • 무한스크롤 및 추가 데이터 페칭할 때 사용성 저하 이슈

    • 별도의 로딩스피너가 없는 상황에서 무한스크롤이 적용된 영역의 바닥에 닿았을 때 API를 재호출하면서 생기는 이슈였다. 우선 로딩스피너를 전역 관리로 변경해서 적용하였고, API 호출 로직을 변경해서 불필요한 재호출이 없도록 수정하였다.
  • Recoil 전역 관리에 해당되는 컴포넌트 리렌더링으로 자식 컴포넌트 리렌더링 이슈

    • 해당 컴포넌트 내에서 구독 중인 Recoil atom의 영향을 받는 confirm, alert dialog가 뜰 때마다 라이브 스트리밍 컴포넌트가 리렌더링되는 이슈였다. 우선 별도의 로컬 Confirm Dialog로 컴포넌트를 분리하면서 해결했다.
  • video.js 플레이어 인스턴스 상태에 따른 분기 처리 이슈

    • 해당 라이브 스트리밍 클립의 상태를 체크해서 스트리밍 중이 아닐 때 video.js 인스턴스 생성을 막았는데, 재생 버튼이 순간적으로 노출되는 이슈가 있었다. 인스턴스 생성을 막기 이전에 발생하는 이슈로 추측됐는데, 당장 사이드이펙트가 우려되는 부분이 있어서 스트리밍 중이 아닐 때 별도 클래스를 추가하고, 그 클래스의 영향을 받고 있을 때 해당 버튼을 CSS로 숨김 처리하는 방식을 취했다.
  • 데이터 페칭 속도보다 컴포넌트 변경이 빠를 때 레이아웃 시프트 생기는 이슈

    • 카테고리에 맞는 컴포넌트와 데이터 변경을 하는 영역이었는데, 데이터 페칭보다 리액트 내의 컴포넌트 변경이 더 빠르다 보니 레이아웃 시프트가 생기는 이슈가 있었다. 우선 로딩 스피너의 적용을 받게 했고, 추가로 데이터 페칭을 끝나기 전에는 각각의 컴포넌트가 그려지지 않도록 삼항연산자로 처리해두었다.
  • 단순 null checking이 불안정할 수 있는 이슈

    • 특정 정보 섹션이 없는 경우 null로 떨어지길래 데이터 바인딩에서 null만 checking 하도록 작업해두었는데, 추후 운영 기능에서 섹션 타이틀 생성 시 string 빈값이 자동 기입되는 것으로 변경되면서 빈 string 값이 들어오는 이슈가 있었다. 단순히 null만 체크하는 것이 아니라 falsy한 값은 전부 checking 하는 것으로 수정해서 해결했다.
  • 플레이어 별도의 딤처리 및 플레이어 인스턴스 상태에 따른 분기 처리

    • video tag를 사용할 때 poster attribute를 별도로 지정하지 않는 경우 src attribute나 source tag에서 사용하는 영상의 첫 번째 프레임을 썸네일로 사용한다. 기본값을 사용하려고 했기 때문에 크게 신경을 안 쓰고 있었는데, 해당 video tag가 들어가는 페이지의 경우 일반 클립과 라이브 클립으로 나눠지고, 라이브 클립은 방송 상태에 따라 영상이 항상 삽입되는 건 아니라는 점을 간과했다. 그래서 라이브 클립의 상태값을 체크해서 상태에 따라 별도의 figureimg tag를 통해 호출되는 이미지 소스를 삽입하는 형식으로 처리했다. 다만 특정 경우에는 단순히 클립 상태값 뿐만 아니라 플레이어 인스턴스 상태를 체크해야 했고, 플레이어 인스턴스 상태에 맞춰서 별도의 분기 처리를 진행하였다.
  • 동적 라우팅의 경우 url params에 유효하지 않은 값이 나올 때 별도의 에러 핸들링 필요한 이슈

    • 동적 라우팅을 하는 페이지의 경우 기존에 존재하던 상품이 삭제됐다거나 사용자가 url을 별도로 입력한다거나 하는 이슈로 값을 제대로 받아오지 못할 수 있어서 별도의 에러 핸들링이 필요하다. 작업하면서 인지는 했지만 메인 페이지나 에러 페이지로 보내거나 혹은 별도의 dialog를 띄우는 등의 기능 정의가 필요했고 임의로 처리할 순 없기 때문에 백로그에 적어둔 이슈였다. 어차피 제대로 된 id를 담아서 보내지 않으면 400 에러가 떨어지고 response로 정의된 에러코드가 날아오도록 서버에서 처리하기 때문에 해당 에러코드로 에러 핸들링을 진행했다. 기존의 404 페이지의 경우 와일드카드로 설정되어 있었기 때문에 해당 페이지로 라우팅하기 위해서 별도의 ‘page-error’라는 path로 라우팅하게 핸들링했고, 해당 path는 존재하지 않기 때문에 와일드카드의 404 페이지로 떨어지게끔 작업했다.
  • custom select의 option이 동적으로 들어가는 경우 여러 케이스를 고려해서 설계해야 하는 이슈

    • selectoption은 원하는 대로 커스텀하는 것이 불가능하기 때문에 주로 custom select를 만들어서 사용하게 되는데, PC 화면상에는 중간 정도에 위치해서 동적으로 늘어나도 이슈가 없었다. 다만 모바일로 전환하면서 해당 custom select가 바텀시트에 붙게 되고, option의 높이가 바텀시트의 높이를 넘치면서 의도치 않은 스크롤 버그와 option이 가려지게 되는 이슈가 있었다. 담당 디자이너님과 협의 후 바텀시트의 높이를 조정하고, option이 동적으로 들어오는 여러 케이스를 체크해서 수정 진행하였다.
  • cloudFront cookie 갱신 관련

    • cloudFront에서 받아오는 소스의 cookie가 만료되는 경우 더 이상 별도의 소스를 가져오지 못하는 문제가 있었다. 다른 기능 구현에 급급하다 보니 cookie 갱신에 대한 생각을 못하고 있었다. 가장 간단한 방법은 cookie의 expire 조건을 확인해서 만료되기 전에 해당 API를 재호출하는 거지만, 아직 릴리즈가 된 상황은 아니고 수정 기간을 며칠 확보할 수 있어서 라이브러리를 조금 뜯어보기로 했다.
    • 영상 호출은 HLS를 취하고 있었고, m3u8을 최초로 받아오면 그 안에 명시되어 있는 대역폭별 m3u8 다시 한번 호출하면서 시간 단위로 잘게 쪼개져 있는 ts 파일을 호출한다. 최종적으로 ts 파일을 호출했을 때 cookie가 만료된 경우 영상을 호출하지 못하는 이슈였기 때문에 라이브러리 재생 중간 단계에서 최초의 에러를 캐치해서 cookie를 갱신해주면 되지 않을까 하는 생각이었다.
    • 체크해봤을 때 video.js 내부에서 VHS나 HLS를 호출할 때 별도의 내부 핸들러를 감지할 수 있는 방법이 있긴 했다. 플레이어 인스턴스의 tech 객체로 접근해서 IWillNotUseThisInPlugins: true;로 옵션을 설정하면 내부 API에 직접 접근할 수 있다. 하지만 IWillNotUseThisInPlugins를 통해서 접근하려는 API들은 공식 API가 아닌 내부 API의 일부로 다소 호환성 문제가 발생할 수 있어서 라이브러리에서 권장하는 방법은 아니었다. 우선 해당 옵션을 허용하면서 HLS 내부 호출에서 vhs-rendition-blacklistedhls-rendition-blacklisted 발생을 감지할 수 있었고, 해당 이벤트가 감지되면 신규 cookieList 받아올 API 재호출 후 useRef에 별도의 값을 담아주었다(useState로 담으면 리렌더링 발생). 최종적으로 uri 값을 바꿔주는 videojs.Hls.xhr.beforeRequest 안쪽에서 cookieList를 갱신하려 했다.
    • 갱신이 됐지만 몇 가지 문제가 있었다. 우선 특정 대역폭의 ts 파일 수신에서 403 오류가 처음 떨어졌을 때 바로 갱신이 되지 않고 라이브러리 내부에서 여러 대역폭의 m3u8을 바꿔가면서 수십 차례 403 오류를 뱉는 문제가 있었고, 끝내 갱신은 됐지만 특정 케이스에서는 플레이어가 뻗기도 했다. 최초 403 오류가 났을 때 ts 파일 수신을 제어하고 그 사이에 cookie 갱신을 진행한 다음에 다시 수신을 시작하는 방법이 필요했다.
    • video.js나 http-streaming 플러그인 내부에서 ts 파일 수신 자체를 제어할 방법을 찾아보았다. 꽤 리서치를 해보고 여러 방식으로 코드를 수정하며 시도해봤지만, 내부적으로 buffering이나 segment 다운로드를 제어할 방법이 없는 것으로 확인되었다.
    • 내부적으로 에러를 감지했을 때 player.src에 신규 sources를 주입하는 형식으로 작업해봤으나, cookie는 제대로 업데이트되지만 src에 새롭게 주입하는 경우 브라우저에 따라 플레이어가 재시작되거나 플레이어가 pause되는 이슈가 있었다. 그래서 플레이어의 현재 재생 지점인 currentTime을 받아와서 해당 시점에서 다시 플레이하는 방식으로 접근해보았으나, 별도의 play 내장함수로 작동한다고 해도 로딩스피너가 뜨거나 순간적으로 사용자 경험을 해치는 자잘한 문제가 있었다. 그리고 궁극적으로 최초 문제였던 대역폭을 순회하면서 403 오류를 뱉는 문제가 해결되진 않았다.
    • 더 이상 시간을 지체할 순 없어서 결국 최초 고려했던 API 재호출을 통한 cookie 갱신을 취하기로 했다. 우선 개발 서버와 운영 서버, 그리고 영상 클립별 expire 조건을 체크하고, 각각 조건에 따라 만료 직전에 별도로 호출해서 useRef 내에 cookie 값을 갱신했다. 각각 배포 환경, 혹은 클립 타입은 별도의 환경변수나 상수로 지정한 값을 가져와서 분기 처리했다. 사실 이 과정에서 꽤 힘들기도 했고 결국 원하는 방식으로 트러블슈팅을 하진 못했지만, A가 안 되면 B, B가 안 되면 C, C가 안 되면 D라는 식으로 순차적으로 고민하고 해결책을 찾아봤던 경험은 꽤 좋은 경험이라는 생각이 든다.
  • CORS 해결

    • 프론트에서 CORS 관련해서 할 수 있는 건 한정적이기 때문에 Django 기반에서의 레퍼런스 자료를 찾아서 서버 개발자님에게 요청했다. 이건 개발자 단톡방의 누군가의 조언이기도 했는데, 개발은 협업의 연속이기 때문에 단순 요청보다는 레퍼런스 자료를 찾아서 좀 더 명확하고 친절하게 요청하는 것이 좋다는 조언을 적용했다. 그 레퍼런스 자료가 도움이 됐는지는 모르겠지만 스스로 자료를 찾으면서 배우는 것도 있고, 그 배움의 과정이 이후의 소통도 유연하게 만드는 순기능이 있었다.
  • antialiased 제거

    • antialiased 는 웹 퍼블리셔 시절부터 관성적으로 써왔던 거라 base.cssreset.css쪽에 webkit-font-smoothing: antialiased; 형식으로 적용해놨던 속성인데, 문제는 배경 대비 뚜렷하지 않은 폰트의 경우 흐리게 보이는 이슈가 있었다. 처음엔 이유를 모르다가 base.css에 적용해놓은 걸 확인하고 구글링을 통해 관련 이슈와 아티클을 확인할 수 있었다. 사실상 최근 들어서는 안티 패턴에 가깝다는 걸 확인했고 해당 속성을 제거했다.
  • safari blurry 이슈

    • 특정 케이스에서 프로젝트에 적용했던 svg가 흐릿하게 보이는 이슈가 있었다. 더 정확히 말하면 svg를 img tag로 넣었을 때 이슈가 발생했는데, 해결책은 object, iframe, embed tag로 변경하면 해결되는 이슈였다. 다만 tag 수정보다는 다른 확장자의 파일을 적용하는 편이 혹여나 있을 사이드이펙트가 적을 것 같다는 생각에 png로 교체하는 방법을 택했다.
  • throttling 적용

    • 스크롤의 위치에 따라 헤더의 컬러나 크기를 조정하고, 최상단 이동 버튼 노출 등의 기능이 있었기 때문에 스크롤 이벤트를 사용하고 있었다. 다만 불필요한 이벤트를 실행할 필요는 없어서 setTimeout을 통해 100ms마다 실행되도록 별도의 throttling을 구현해두었다.
  • video.js

    • 처음 세팅할 때 최신 버전인 8 버전으로 세팅했으나 사용하고자 했던 videojs-hls-quality-selector 플러그인이 7 버전에서만 지원하고 있어서 7 버전으로 낮췄다. 꼭 최신 버전이 답은 아니었다.
    • 하지만 이후에 해당 플러그인 내부에서 dreprecated된 video.js 함수를 사용하고 있었고, 이후 2 버전 릴리즈로 8 버전에 대응해놓은 것을 확인했다. 어차피 플러그인 이슈가 없다면 8 버전을 안 쓸 이유도 없었고 deprecated 된 부분은 언젠가 대응해야 하기 때문에, 플러그인 버전을 올리면서 video.js도 8 버전으로 올리고 마이그레이션이 필요한 부분은 공식 문서를 참고해서 진행했다.
    • https://videojs.com/guides/videojs-7-to-8/
  • 배포 환경에서 invalid hook call

    • 로컬 환경에서는 별도의 에러가 없어서 배포를 진행했는데 배포 후에 화면에 제대로 그려지지 않아서 확인해보니 invalid hook call 때문이었다. React에서 Rules of Hooks이 존재하는데, 그 Rules of Hooks을 위반해서 invalid hook call 이 발생한 것으로 확인돼서 수정했다.
  • Safari 재생 오류 체크

    • Safari에서만 특정 재생 오류가 발생했는데, video.js 내에 verrideNative: true; 옵션으로 해결할 수 있었다. 해결이 가능한 이유는 각 브라우저마다 기본 미디어 재생 기능으로 영상 재생을 처리하는데, overrideNative를 허용하면 기본 미디어 재생 기능을 비활성화하고 JavaScript 기반의 처리 방식을 사용한다. HLS와 같은 스트리밍 포맷을 다룰 때 특히 중요한데, 다양한 브라우저에서 일관된 사용자 경험을 제공한다는 점에서도 필요한 옵션이라는 생각이 들었다.
  • 로그인 관련 Safari, Firefox 대응

    • 로그인이 필요한 페이지를 별도의 링크나 의도된 유저 플로우와 무관한 방식으로 접근하는 경우 라우터를 통해 접근을 막아두었는데, 특정 케이스의 경우 그 사이에 API를 호출하거나 요소를 렌더링하면서 에러를 뱉는 경우가 Safari나 Firefox에서 일부 확인되었다. 그래서 로그인과 관련해서 사용하는 hook 내에 별도의 state를 추가하고 해당 statefalse인 경우에는 API 호출이나 요소 렌더링 자체를 막아두었다.
  • video.js 자막 기능 관련 Safari용 별도 이벤트 핸들러 추가

    • Safari에서만 자막 기능이 제대로 활성화되지 않아 체크해보니 Safari에서 해당 이벤트를 처리할 때 다른 타이밍에 발생하거나 다른 조건에서 발생하는 것으로 추정되어 Safari 용 코드를 별도로 넣었다. 사실 해당 코드는 Chrome이나 Firefox에서 큰 문제 없을 수도 있지만 혹시나 사이드이펙트가 우려되어서 UAParser를 통해 브라우저를 체크하고 Safari에서만 작동되도록 추가했다.
          if (browser.name === 'Safari') {
            player.on('loadedmetadata', () => {
              player.textTracks().addEventListener('change', trackChangeHandler);
            });
          }

그 외 느낀 점

  • 웹 퍼블리셔로서의 2년 동안 비록 얕더라도 Vue2, Vue3의 경험이 없었으면 맨땅에 초기 세팅은 더 애먹었을 거라는 생각이 들었다.
  • 회고를 쓰면서 느낀 건데 큰 규모의 작업은 아니지만 꽤 많은 고민이 있었고, 그 과정에서 꿈에도 나오고 빈번하게 두통이 있을 정도로 힘든 경험이기도 했다. 힘든 만큼 성장했기를 바람과 동시에, 분명한 성취가 다른 작업 속에서 실제로 체감되기도 한다.
  • lottie는 협업하는 디자이너님 덕분에 처음 알게 된 라이브러리인데, 일찌감치 알았다면 gif나 mp4 가지고 했던 불필요한 고민들을 줄일 수 있었을 것 같다. 벡터 기반으로 용량 대비 품질이 좋고, JSON 기반으로 작동하기에 명확하게 소통하면서 간편하게 공유할 수 있다는 점, 그리고 크로스 플랫폼에 바로 대응이 가능하다.
  • 그럴 듯한 프로덕트가 나왔다 하더라도 아직 나라는 밭에 거름을 얇게 뿌려낸 정도가 아닐까 싶다. 단순히 스스로를 낮추는 것이 아니라 내 밭에는 드문드문 방치된 척박한 부분이 있었는데, 스스로 고민하며 경험하는 과정에서 척박한 부분을 온전히 바라볼 수 있었고, 그곳에 얇게나마 거름을 뿌려낼 수 있었다. 이 시점에서 방치한다면 다시 척박한 땅이 되겠지만, 물과 볕을 주며 관심을 갖는다면 언젠가 수확할 수 있지 않을까. 나를 온전히 바라보되 스스로를 너무 낮추지 말고, 작은 성공에 독려하며 하나씩 해내가는 게 최선임을 느낀다.

이후에 한 것

유지보수 하면서 현재에도 추가적으로 작업하고 있는데, 특정 기점 이후에 진행한 작업은 2차 회고에서 다룰 예정이다.

  • 웹 접근성 개선
    • button tag에 text node가 없는 경우 aria-label 추가로 웹 접근성 개선
  • Sentry 적용
    • Sentry 적용으로 에러 추적
  • Core Web Vital 관련 최적화
    • 웹폰트 최적화
      • 로컬 woff2에서 dynamic subset으로 변경 + load 위치 변경
      • fetchpriority, eslint rulesreact/no-unknown-property ignore 추가
    • 이미지 최적화
      • aspect ratio 관련 최적화 작업
  • 상수화
    • payment_type, video_type, error_code

이후에 해야 할 것

  • 불필요한 prop 제거
  • 불필요한 렌더링 제어
  • Core Web Vital 관련 최적화
  • 웹 접근성 보완
  • Sentry 세부 옵션 적용
  • TanStack Query 적용
  • 테스트코드 작성
  • refresh token, axios interceptor
  • Long polling → WebSocket
  • 불필요한 API 호출 줄이기
  • CRA에서 Vite로 마이그레이션

회고 레퍼런스

profile
평소엔 책과 영화와 음악을 좋아합니다. 보편적이고 보통사람들을 위한 서비스 개발을 꿈꾸고 있습니다.

2개의 댓글

comment-user-thumbnail
2024년 5월 13일

함께 SCSS로 날아다니던 시절이 떠오르네요..^^ 열쩡열쩡가득했던 시절이 스쳐지나갔던 포스트였습니다...

1개의 답글