daangn마켓 클론 후기 (Web API와 MVC 모델을 이용한)

Dengo·2022년 10월 11일
1

Project

목록 보기
2/3
post-thumbnail

바닐라... 이젠 싫어 ,,,

시작

지난 4-5주 동안 저는 Web API만을 이용하여 (바닐라 자바스크립트로) 당근마켓을 클론 코딩했습니다.
리액트를 사용하기 전에 리액트의 컴포넌트 컨셉을 바탕으로 프로젝트 개발을 한번 경험해보고자 하였습니다.
과정동안 어떤 틀을 잡고 시작을 했고, 계획은 어떻게 했고, 어떤 오류를 범했는지 등등 회고를 하고자 글을 적게 되었습니다

시작하기에 앞서 ...

  1. 글에서 재고하고자 하는 바가 방대하여 글의 두서가 다소 없을 수 있으나 개발한 타임라인에 맞추어 구현한 내용과 깨달은 내용들을 순서대로 적어보도록 하겠습니다.

  2. 코드 사진을 첨부한 부분은 바로 이어서 주목할 부분을 짚어 놓았으니 전부 이해할 필요 없이 해당 부분만 봐주시면 될 것 같습니다

설계

안드로이드 컨셉 차용

저는 이전에 안드로이드 앱 개발을 1년 가까이 했던 경험이 있습니다.
그래서 당근마켓 클론코딩이라는 주제를 하기로 마음을 먹었을 때
가장 먼저 떠올랐던 방식이 바로 안드로이드 개발을 했을때의 패턴 이었습니다.

백지에서 부터 시작하는지라 많은 것을 계획하지는 않았으나 지금부터 만들고자 하는 앱의 화면 전환 컨셉을 안드로이드의 액티비티 컨셉에서 차용하고자 하였고 예를 들면 앱의 상단 네비게이션 바를 "Toolbar"라고 하는 컴포넌트를 만들어 여기에 내가 사용할 아이콘과 텍스트만 추가하여 만들 수 있게끔 하자라고 마음을 먹었습니다.

MVC Model

하지만 사실 이번 프로젝트를 처음 시작한 골자는 바로 상태와 컴포넌트, 뷰로 이루어진 MVC 모델 위에서 해보고자 마음을 먹었었습니다.
개발자 황준일 - Vanilla JavaScript로 Component 만들기
안드로이드의 컨셉을 흉내내야 겠다고 판단한 것도 사실 개발자 황준일님의 블로그에서 컴포넌트 만들기를 공부한 후의 판단이었습니다

어라...? 하지만 이 둘은...!

혹시라도 안드로이드 개발을 경험해보신 분들이라면 눈치채셨을 수 있겠지만 안드로이드의 컨셉을 그대로 가져온 다는 것은 컴포넌트 컨셉과 맞지 않는데요 이는 추후 오류 발견부분에서 자세히 적어보도록 하겠습니다.

Activity 와 Activity Stack

Activity란?
쉽게 설명하자면 안드로이드에서 "화면"에 해당하는 부분 입니다.

안드로이드에서는 이 액티비티라는 단위를 통해서 앱을 구성하는데요,
화면의 전환 또한 액티비티가 겹쳐지면서 일어나는데, 이때 화면 전환 전의 액티비티를 유지하기 위한 자료구조로 Stack을 사용합니다.
간단하게 설명하자면 이렇고 각각을 어떻게 구현했는지 간단하게 살펴보겠습니다

App.js


먼저 기본적으로 Activity가 올라가는 App.js의 코드입니다.
Component 클래스에는 render라는 메서드가 등록되어있고 간단히 설명하면 여기서는 template으로 본인의 innerHTML을 업데이트 하고 바로 mounted메서드를 실행시켜 그 자식 요소를 마운트 시키는 컨셉입니다.
(더 자세한 코드는 제 깃헙에..)

중요한 부분을 짚어보겠습니다
1. setState에서 상태 변화가 일어나면 메서드 안에서 render가 실행되게 해놓았습니다
2. Observer는 함수들을 등록시켜놓고(코드의 subscribe) update메서드로 상태를 업데이트 하면 등록한 함수들을 한꺼번에 실행시켜주는 클래스입니다. (전역 상태 관리를 위한 클래스)
3. App.js에서는 activityStack의 가장 최상단 Activity만 렌더링 되게 구현되어있습니다.
4. App.js의 constuctor에서 setState를 Observer에 등록시켜놓고 바로 update 해주는 것을 확인 할 수 있습니다. 따라서 constuctor에서 render를 굳이 따로 실행시키지 않아도 됩니다.

Activity


pushActivity
Activity Stack에 Activity를 추가하는 과정입니다. 보시다 시피 update가 일어나고 있죠.
즉 이 메서드는 화면 이동을 해주는 메서드 입니다.

popActivity
역시 이번엔 pop을 시킨후 update를 해주고 있습니다.
특이점이 있다면 clearEvents를 해주는 것인데, pop을 하면서 사라진 화면에 걸려있던 이벤트 들을 모두 지워주는 메서드 입니다.
참고로 이벤트를 모으는 부분은 Component 클래스에서 해주었습니다.

이제 이 Activity를 상속받은 진짜 화면 코드를 보도록 하겠습니다

MainActivity (홈 화면)


일단 주목을 할만한 부분은 당연하게도 MainActivity에서도 render를 직접적으로 실행시켜주고 있지 않습니다.
왜냐면 App.js의 mounted에서 Activity Stack 최상단의 Activity의 render를 실행시켜주고 있고 이것은 Observer에 의해 액티비티 스택에 추가된 시점에서 일어나니까요.

그리고 화면이 렌더링 되는 과정을 다시 한번 소개하자면
template이 MainActivity 인스턴스를 생성할때 넣은 선택자 '#root'의 innerHTML에 업데이트 하고
MainActivity의 경우 상단 툴바(네비 바), 리스트, 바텀 네비게이션으로 분류한 각 영역에 mounted 메서드를 통해 또 컴포넌트들을 생성시켜 그 자리를 채우는 방식입니다.

History API

사실 Activity Stack을 무사히 구현해서 다행이긴 한데 이미 이런 문제를 지원하기 위한 Web API가 있다는 사실을 뒤늦게 깨달았습니다,,,

Activity Life Cycle

차라리 Activity Stack의 컨셉을 차용할거라면 안드로이드의 Activity Life Cycle까지 간략하게 구현할걸 그랬다는 생각이 들었습니다. 물론 구현한다 한들 이것을 이번 프로젝트에서 적극적으로 사용했을까 싶긴 한데 만약에 구현 했다면 onStart와 onStop 메서드를 만들어서 여기에 일단 이벤트 관리 부분을 넣었을 듯 싶네요.

어쨌든 스스로 반성하건데 Activity Stack까지 생각해서 구현했던건 과하지 않았나 싶은 판단입니다.
(그래도 한편으로는 뿌듯...)

View와 Toolbar 이야기

안드로이드에는 View라는 개념이 있는데 다름이 아니라 안드로이드의 화면을 구성하는 모든 요소는 이 View를 상속받는 요소들입니다. 즉 이게 안드로이드 컴포넌트들의 뿌리죠.
예를들면 이런 식입니다. View를 상속받은 TextView (글자), ImageView (이미지) 등...

또 고백하건데... 제대로 할거면 이 View를 구현했어야 했습니다.
Toolbar를 만든 이야기와 함께 풀어보도록 하죠

Toolbar

툴바를 처음 만들때의 계획은 이랬습니다

이거 하나 만들어서 이 화면 저 화면에서 써먹어야지 ㅋㅋ
툴바 만들고 ListView, BottomNavigation 다 만들고 재활용 하는식으로 해야지 ㅋㅋ
완벽하다 완벽해 ㅋ~


(쾅쾅!! 하지마! 하지말라고! 야!)
이번 프로젝트에서 가장 오만한? 판단 중에 하나였습니다.
결론 부터 말하자면 Toolbar를 만든다는 판단 자체는 나쁘지 않았다고 생각합니다.

하지만 이거 하나 가지고 모든 화면에서 툴바를 구성한다는 판단 자체는 잘못되었고
이번처럼 간단하게 만드는 프로젝트에서는 Toolbar를 상속받는 MainToolbar를 따로 만들어야 했습니다.
제가 구현한 Activity와 MainActivity 처럼요.
(프레임워크 만들 것도 아니고 왜 그랬냐고 나 자신...)

왜 지금과 같이 한다면 문제가 있는걸까요? 왜 View를 아예 구현했어야 한다고 한걸까요?
코드와 함께 살펴보도록 하겠습니다.

Toolbar의 코드 일부인데, 다른 부분 말고 주목할 부분은 template의 return 부분 입니다.
하나만 생각하고 둘은 생각 못한... 저의 못난 코드 ....

코드를 보면 대충 파악이 되다시피 왼쪽 요소와 오른쪽 요소를 props를 통해 전달받으면 알맞게 넣는 계획 이었습니다.

grid를 통해서 3등분을 한 후에 양쪽의 칸에는 flex를 주어 컴포넌트를 넣으면 일렬로 정렬되는... 그런 모습을 상상한 것이죠.
사진을 보시다시피 구현에는 성공했습니다.
하지만 코드를 고백해 보도록 하겠습니다 🤦‍♂️ (아찔)

주목할 부분은 다름이 아닌 두번째 라인의 isIcon에 따른 분기 코드 부분 입니다.
즉 이런거죠.
툴바를 구성하는 요소는 텍스트, 아이콘, 버튼을 비롯하여 제약이 없어야 하지만 생각이 짧았던 저는

흠... 어차피 왼쪽에는 텍스트 하나만 적당히 들어가는거 같고, 오른쪽에는 아이콘만 들어가는거 같네
그럼 props로 받을때도 스스로 그렇게 코딩을 하면 되는거잖아?!

네... 멍청한... 녀석입니다
우선 왼쪽에는 텍스트만 들어가는게 아니고 당장 Left Chevron 그러니까 뒤로가기 버튼이 들어가는 화면이 대다수고, 심지어 제품 상세 화면에서는 뒤로가기 버튼과 홈 버튼 까지 들어갑니다...
하지만 저렇게 멍청하게 생각을 한것이죠.
그래서 isIcon 즉 텍스트냐 이미지냐를 분기 시켜서 알맞은 태그를 사용하는 template 코드를 짜야 했습니다.
(이렇게 까지 솔직하게 써도 되는걸까,,?)

자 그럼.. 멍청한건 그렇다 치고 저는 어떻게 했어야 했던 걸까요?

두 가지의 해결 방법이 있습니다.
1번. 하드 모드 - View를 만들고 TextView, ImageView를 만든다.
이렇게 되면 들어가는 요소가 icon이냐 글자냐 버튼이냐를 막론하고 오로지 데이터 타입이 View인 인스턴스를 넣는다는 개념으로 추상화가 됩니다. 이것에 기본적으로 클릭리스너를 단다던지 하는 인터페이스를 만들고 하면 말이죠.

2번. 이지 모드 - Toolbar에 기본적인 내용만을 담고 이를 상속한 MainToolbar 등등을 만든다.
앞서 설명 드렸듯 이게 가장 합리적인 방법이라고 생각했습니다.

실제로 저는 이렇게 Toolbar에서 저의 멍청함을 깨닫고 List와 BottomNavigation은 그냥 전부 화면 별로 따로 만들었는데요, 공통 클래스를 구상하는데에 필요한 공수에 따른 것 이기도 했지만 View가 구현이 안되어있는 문제가 제일 컸습니다.

차라리 코드의 중복을 조금은 피할 수 없더라도 화면 별로 만들어 직관적인 개발을 하자고 마음먹은 것이죠.

DetailActivity (제품 상세 화면)

이 화면을 만들면서 크게 3가지의 이슈가 어려웠는데 한번 살펴보도록 하죠

Swipe Carousel 만들어본 후기 - Written by 나

씁,,후.. 너네는 이런거.... 만들지... (이하 생략)

Grid Start, Grid End

앞서 MainActivity와 마찬가지로 이 화면 또한 Toolbar - Body - Bottom 의 세 등분으로 구성이 되어있습니다. 그리고 이 세 등분은 Grid로 구분이 되어있는데요.
MainActivity와는 다르게 DetailActivity에서는 아래 사진처럼 툴바와 본문의 영역이 겹쳐야 하는 것을 알 수 있습니다.

이것을 css의 grid-row-start, grid-row-end 속성을 툴바와 본문 각각에 주어 해결할 수 있습니다.

css의 grid는 정확히 다음과 같이 생겼는데,
툴바의 시작과 끝은 '1번 행 - 2번 행' 임을 알 수 있고
본문의 시작과 끝은 '1번 행 - 3번 행' 임을 알 수 있습니다. (코드는 생략,,)

Scroll Event

당근 마켓의 상품 상세 화면에서는 스크롤에 따라 상단 툴바의 색상이 달라져야 하는 기능이 있습니다.
결과 부터 말씀드리자면 Activity Stack과 더불어 Observer를 사용해서 해결하였습니다.
참고 화면... 같이 보시죠

굳이 이렇게 그림으로 그리기 까지 한 이유는 props drilling으로 scroll event에서 발생한 값을 Toolbar에 전달할 수 없음을 나타내기 위함 입니다.

위와 같은 구조로 되어있으니 언뜻 가까워 보이는 두 컴포넌트간에 전역 상태 관리로 Observer를 사용하게 된 것 입니다.

해당 코드는 DetailBody 즉 스크롤이 일어나는 부분에 스크롤 리스너를 달아준 모습 입니다.
따로 설명은 안드렸지만 Toolbar에서 state 변화가 있으면 그 mode에 따른 투명, 일반 모습을 렌더링 되게 구현해놓았고 위의 코드로 인해 스크롤 이벤트에 따라서 툴바의 상태가 업데이트 될 것을 알 수 있습니다.

하지만 위와 같이 해놓고 끝내면 문제가 있는데요.
바로 스크롤을 하는 내내 이벤트가 발생하여 그 만큼 렌더링도 일어난다는 점 입니다.

이 부분의 해결을 위해 Throttle이나 Debounce와 같은 기술로 해결 할 수도 있었으나
그냥 간단하게 update가 일어나는 시점은 scrollObserver의 mode가 각각 normal일때, transparent일때만 일어나게 해주는 코드를 추가한다면 딱 scrollTop이 305를 넘나들때만 옵저버가 update 될 수 있을 것 입니다.

여기까지 해서 이번 프로젝트에서 구현한 큼직한 부분들은 모두 다루어 보았습니다.

고해성사 타임

위에서도 실컷 하긴 했지만 아직 더 남았습니다... 잘못을 뉘우칠 부분들이...

설계 오류

설계에 대한 결론 부터 적어보자면 위의 두가지 컨셉을 동시에 차용하기로 판단한 것은 오류였습니다.
안드로이드에서 저는 Binding이라는 방식으로 각 요소에 id값으로 접근하여 조작을 하며 개발을 했었습니다.
이는 Web API 혹은 jQuery의 DOM 조작 방식과 상당히 유사하죠.

반면에 황준일님 블로그에서 기본 컨셉으로 차용한 컴포넌트와 상태관리 컨셉은 DOM조작을 최대한 피하기 위한 컨셉이라고 볼 수 있습니다.

즉, 제가 범한 오류는 안드로이드의 컨셉을 차용하더라도 "상태관리와 컴포넌트 렌더링" 이 부분을 지켜가며 설계를 했어야 했는데 생각이 짧아 두가지를 동시에 시도하려 하였고 잘못된 것을 개발을 한창 진행하던 주차에 깨닫게 되었습니다,,,

Class List 활용 안함


코드는 또 다시 Toolbar의 코드입니다...
drawCurrentMode를 보시면 저런 저런... inline style을 DOM 조작을 통해건들고 있죠
사실 저 코드를 짤때는 어쩔 수 없다고 판단했습니다.

css에서 tranparent라는 클래스를 만들어서 툴바의 클래스에 추가하는 방법을 물론 생각했으나
코드의 template을 보시다시피 툴바의 최상단 태그는 액티비티안에 있습니다. 그러니까 여기 없다는 말입니다.

그래서 클래스를 추가할 수 없고 DOM 조작을 통해 해결해야한다...! 라고 판단했으나
Class List라는 Web API가 또 있는지 뒤늦게 알아버린 것 입니다..

React의 Component에서 return을 괜히 최상단 태그로 감싸고 있는게 아니다

이것을 확실하게 깨달았다고 생각한게 바로 위의 Toolbar 예시를 통해서였습니다. (Class List 부분)
컴포넌트를 마운트 시킬 틀은 그냥 틀대로 두고 컴포넌트마다 최상단 태그로 감싸주었어야 했습니다.
그래야 해당 컴포넌트 자체의 조작이 훨씬 자연스럽고 간단해졌겠지요.

물론 class list의 DOM 조작으로 해결이 가능한 부분이었다고는 하나 만약에 컴포넌트에서 그냥 최상단 태그로 Toolbar를 감싼채로 구현했다면 그냥 template의 구현에서 해결하면 됐을 부분 입니다.

즉 태그의 Depth가 깊어지는 것을 감수하고서라도 이렇게 구현하는 것이 체계적이었을 것 입니다.

class 이름과 id

각 태그에서 class와 id의 컨벤션을 제대로 정하지 못하여 제 깃헙을 방문해 보시면 아시겠지만... 굉장히 중구 난방으로 작성하신 것을 보실 수 있습니다.

class가 특히 어려웠는데 프로젝트를 시작하기에 앞서 프로젝트 전반에 걸쳐 중복되는 디자인 요소가 무엇이었는지를 한번 살펴봐야 했다고 생각했습니다.

이를 통해서 class의 중첩 적용을 활용해야 했고 id의 사용을 줄였어야 했습니다.

사실 id는 이벤트의 중복을 막기 위함도 있었는데 이 부분은 여전히 생각해봐야 할 주제 인 것 같습니다..

사실 이 class와 id 부분이 이번 프로젝트를 마무리 하며 제일 찜찜한 부분이긴 합니다만 추후에 진행할 프로젝트에서 계속해서 더 나은 방법을 고민해봐야 해결될 문제겠죠?

마무리


(비키십시오. 당신이 절 만들 수 있을 확률은 0.1% 미만입니다)

처음에 계획 했을 때만 해도 내가 이걸 할 수 있나 막막했습니다.
그냥 만드는 것도 아니고 MVC에... 추가적인 설계에...
그러나 황준일님 블로그를 시작으로 물꼬를 트기 시작했고 (황준일! 황준일!)
미약하게나마 끄적끄적 진행 할 수 있었습니다.

프로젝트를 진행하는 내내 블로그에 이번거는 반드시 올려야겠다라고 생각했고
진행기간이 사실 계획 했던 것보다 길어지기도 했는데 더 길게 진행하면서 나름대로 깨달은 부분도, 반성하게 된 부분도 많아 분량이 많아질 것을 예상하였는데 정말로 길어져서 작성하는데에 상당히 힘드네요.

그럼에도 불구하고 고생했던 프로젝트 인지라 이러한 생각들 하나하나가 반드시 저의 자산이라고 생각했고 글이 길어지더라도 꼭 회고해야겠다고 다짐했습니다.

글을 더 길게 쓴다면 황준일님 블로그에서 읽은 내용까지 정리 할 수도 있었는데 이건 추후에 따로 적는게 훨씬 이번 글을 위해서도, 다음글을 위해서도 좋을 것 같네요.

또 고해성사로 고백하자면...(왤케 많아...)
사실 계획했던 기능들이 몇개 더 있는데 구현하지 못한 것 입니다.
그래서 이번 프로젝트 또한 아쉬움이 남았지만 여기서 더 길어진다면 너무 지치고 늘어질 것 같아서 일단 중단을 하였습니다.

미련이 남은 프로젝트로 기억되겠지만 그럼에도 Web API 그러니까 바닐라 자바스크립트로 여기까지 만든 것만으로도 저의 공로를.. 스스로에게 조금은 칭찬하고 싶네요 (고생했다 나 자신)

여기까지 읽으신 분이 있을까 싶지만 긴 글 읽어주셔서 감사합니다

dengoyoon/mock-daangn repo

profile
Software Engineer (전산쟁이)

0개의 댓글