🔗 URL : https://hellosonic-vanilla-js-notion-hellosonics-projects.vercel.app/
🚀 들어가며
드디어, 기다리고 기다리던 바닐라 자바스크립트로 개인 프로젝트를 진행하게 되었다.
개인적으로 데브코스의 많은 커리큘럼 중에서도, 바닐라 자바스크립트로 첫 프로젝트를 진행한다는 점이 인상깊었다. 자바스크립트 기본기를 탄탄하게 다져야만 이후 커리큘럼인 리액트 학습과, 백엔드와의 팀프로젝트를 제대로 진행할 수 있다고 생각했기 때문이다.
이전까지 현업자분들의 강의를 수강하며, 데이터의 흐름과 콜백 함수의 전달 등 이전까지 내가 코딩테스트를 대비하며 전혀 겪어보지 못한 새로운 어려움을 해결하기 위해 노력했고, 이번 프로젝트에 그간의 노력들을 다 담아보기로 결심했다.
지난 한 달동안 과제를 수행하며 각각의 기능을 담당하는 컴포넌트 간 의존성을 낮추는 데에 집중했듯이, 이번에도 컴포넌트 간 의존성을 낮추기 위해 최대한 노력했다. 또한 강의에서 배웠던 로컬스토리지, history API를 통해 사용자에게 쾌적한 UX를 제공하기 위해 노력했다.
✅ 레이아웃
HTML 레이아웃은 크게 두 가지 영역으로 구분되어 있다. 왼쪽(side-bar)에는 Header 컴포넌트와 API 통신을 통해 저장된 Documents의 목록을 보여주는 PostList 컴포넌트가 존재한다. 오른쪽(edit-page)에는 선택된 Document의 제목과 내용을 편집할 수 있는 공간인 Editor 컴포넌트, 선택된 Document의 하위 Documents로 바로 이동할 수 있는 링크 버튼이 담긴 LinkChildPost 컴포넌트가 존재한다.
프로젝트 진행 초기 단계에 노션 클로닝을 위한 대략적인 레이아웃은 설계하고 개발에 착수했다. 그러나, 트리 구조의 Documents 목록을 어떤 태그를 활용하여 구현할 것인지, 하나의 Document를 보여주기 위해서는 어떠한 태그가 필요할지 등 더 많이 생각하고 자세하게 설계했어야만 했다. 이를 프로젝트 진행 중간에 깨달았다.
✅ Document 목록의 트리 구조
나는 트리 구조의 Document 목록을 화면에 렌더하는 컴포넌트를 구현할 때 재귀를 활용하였다. 내가 코딩테스트를 준비하며 공부했던 DFS, 재귀가 여기서 비슷하게 활용된 것에 대해 신기했고, 이래서 알고리즘 공부도 중요하다는 것을 깨달았다.
사실, 처음에는 많이 헤맸던 것 같다. 그리고 중간에 하위 Document들이 open된 상태(사진에서 'd'와 같이 하위 Document들이 보여진 상태)와 close된 상태의 상위 Document의 토글 버튼에 각각 다른 이미지를 주고 싶었는데, 초반에 생각했던 방식에서는 구현이 어려워서 재귀 함수 안의 코드들을 뜯어고쳐야만 했다.(그래서 프로젝트 초기 단계에서 디테일한 레이아웃 설계의 중요성을 깨달았다..)
DOM 구조를 설명하자면 같은 depth의 document는 동일한 ul 태그에 포함되어 있고 상위 document는 li태그에, 하위 documents는 또 다른 ul태그들에 포함되어 있는 구조이다.
✅ 주요 구현 내용
- 바닐라 자바스크립트만을 이용하여 노션을 클로닝했다.
- 화면 좌측에 Root Documents를 불러오는 API를 통해 Documents를 렌더링했다.
- 화면 좌측의 Documents를 클릭하면 오른쪽 Edit-page 영역에 해당 Document의 title과 content를 렌더링했다.
- Edit-page 하단 영역에는 선택된 Document의 하위 Documents로 이동하는 버튼을 생성하였다.
- Document에 하위 Document가 있는 경우, 해당 Document 아래에 트리 형태로 렌더링된다.
- 새로고침해도 표시된 Documents들이 유지되도록 구현하기 위해, 로컬 스토리지를 활용하였다.
- Document 우측의 추가 버튼을 클릭하면, 클릭한 Document의 하위 Document로 새 Document를 생성하고 편집 화면으로 넘기도록 구현했다.
- 편집기에 입력되는 내용을 콜백 함수를 전달하여 화면 좌측의 Document의 title이 즉각적으로 변경될 수 있게 구현했고, API 통신을 통해 지속적으로 서버에 저장되도록 구현했다.
- History API를 이용해 SPA 형태로 만들었다.
- 루트 URL 접속 시에는 Document 선택이 되지 않도록 구현했다.
- /documents/{documentId} 로 접속 시, 해당 Document의 title과 content를 불러와서 편집기에 로딩되도록 구현했다.
✅ 노션 클로닝 프로젝트 진행 중 겪었던 어려움과 해결 경험 회고
컴포넌트 간 의존성, 불필요한 렌더링 방지 등 효율적인 구현을 위해 고민했지만, 자바스크립트로 진행하는 첫 프로젝트라 많은 어려움이 있었다.
▪︎ API 호출이 처음이라 쉽지 않았다.
나의 경우, 부끄럽지만 처음에는 API가 무엇인지조차 잘 알지 못했다. 그동안 과제에서는 더미 데이터를 이용하여 과제를 수행했고, 이번 과제에서 처음으로 API 통신을 해보았다. 그렇기 때문에 처음에는 API를 호출하는 것이 굉장히 어색했다. (더군다나 비동기, async, await 를 잘 모르다보니 초반에 API 호출 함수를 정의하는데에도 무척 애먹었다.)
하지만, 팀원분들에게 적극적으로 물어봤고, 프로젝트를 진행하면서 많은 것을 배울 수 있게 되었다. 현재는 API가 무엇이고, 왜 필요하고, API를 가지고 어떤 식으로 백엔드 개발자와 협업하는지, API 통신으로 가져온 데이터를 각 컴포넌트에 어떻게 전달하는지 등을 설명할 수 있을 정도로 성장할 수 있었다.
또, 프로젝트 마감일인 다음 날에는 우리 팀의 멘토님과 커피챗 시간에 비동기 처리가 왜 필요한지 라이브 코딩을 통해 교육을 받았다. (멘토님 감사합니다.🥹) 비동기 처리를 능숙하게 할 수 있을 정도로 학습해서 더욱 쾌적한 UX를 사용자에게 제공할 수 있는 개발자로 성장하고 싶다.
▪︎ 재귀를 통해 Documents 목록을 구현하던 중..
트리 구조의 Documents 목록을 구현하기 위해 가장 먼저 재귀 함수를 생각했다. depth 끝까지 탐색하며 documents 목록을 화면에 그려주면 간단할 것이라고 생각했다. 하지만 이를 DOM 구조에 적용시키기란 쉽지 않았다. 내가 예상한대로 DOM 트리가 생성되지 않았고, 초반에는 많이 헤맸지만 구현에 성공했다. (이 후에, 토글 버튼 때문에 이 구조를 뜯어고치긴 했지만,, 다시 고칠 때 구현은 처음보다 훨씬 쉬웠다.)
▪︎ 삭제 버튼 클릭 시 이전 Documents 목록에 현재의 Documents 목록이 추가되어 렌더되었다.
저번 과제에서는 innerHTML를 사용했기 때문에 위와 같은 문제가 발생하지 않았다. (innerHTML은 이전의 내용을 싹 다 날리기 때문에) 하지만 이번 과제에서는 DOM 요소를 생성해서 DOM 트리에 추가하는 방식을 활용했고, 그 때문에 삭제 버튼 클릭 시 다시 렌더가 되면서 이전의 Documents 목록에 현재의 Document 목록이 추가되었다.
이를 해결하기 위해 플래그를 활용하였다.
한 번 렌더가 되었을 때는 target 요소인 div의 자식 요소들을 초기화하고 Documents 목록을 렌더하는 것이다. 이 방법은 올바른 방법인지는 모르겠다. 현재는 코드 리뷰 확인사항에 요청해둔 상태이다.
▪︎ App 컴포넌트가 지나치게 뚱뚱해졌다.
이것은 프로젝트 중간에 팀원분에게 피드백 받은 사항이다. 나는 컴포넌트 간 의존성을 줄이기 위해 각 컴포넌트끼리는 최대한 직접적으로 데이터나 함수를 전달하지 않고, 모두 App 컴포넌트를 거쳐서 데이터를 전달하려고 했다. 예를 들어, PostList(화면 좌측의 Document 목록을 생성하는 컴포넌트)에 클릭 이벤트가 발생했을 때, Edit-page의 편집기에 해당 Document를 띄워줘야 한다. 구현 초기에는 클릭 이벤트 핸들러를 App.js에서 정의해주었다. 즉, App 컴포넌트를 브릿지로 활용하려고 했다. 그러다 보니 App 컴포넌트의 코드가 지나치게 길어졌다. 가독성도 많이 떨어졌다.
이를 해결하기 위해 route 함수와 CutomEvent를 활용하였다. 아래와 같다.
특정 Document를 클릭했을 때, URL도 변경이 되어야 하고 해당 Document의 편집기를 불러와야 한다. 따라서 router.js에서 정의된 push 함수의 파라미터로 해당 Document의 id를 전달하게 되면, CustomEvent가 생성되어 이벤트를 실행시킨다. window에서는 이벤트를 받아서 핸들러(콜백 함수)를 실행시킨다. 핸들러의 내용은 history API를 통해 URL을 변경시키고 파라미터로 전달 받았던 onRoute 함수(App.js의 this.route())를 실행시킨다. App.js에서는 URL의 pathname에 따라 상태 변경이 필요한 컴포넌트의 상태를 변경시킨다.
route, history API, CustomEvent를 통해 특정 컴포넌트에서 발생한 이벤트의 핸들러를 App.js까지 힘겹게 옮기지 않을 수 있게 되었다. 그리고 코드도 훨씬 짧아지고 가독성이 좋아지게 되었다.
처음 이 로직을 이해하는 데에 어려웠지만, 한 번 흐름을 알고 나니까 중복 코드를 줄일 수 있게 되었고, 결론적으로 구현하는데에 무척 편리했다.
▪︎ 여러 이벤트를 처리하기 위해 이벤트 위임을 활용했다.
이전 과제의 코드 리뷰 기간에 팀원분에게 피드백 받았던 사항이다. 이벤트가 가장 하위 요소에서 발생했더라도 가장 상위 요소까지 핸들러가 동작한다는 점을 이용해서 가장 상위 요소에서 e.target을 통해 어디서 이벤트가 발생했는지 확인 후 핸들러를 동작시켰다. 또한, 이벤트 핸들러를 Event 디렉토리에 분리해서 유지보수적인 측면에서의 효율을 높이고, 코드 가독성을 높일 수 있었다.
▪︎ 편집기에서 keyup 이벤트 발생 시 편집기 선택이 풀리는 현상 발생
편집기 기능을 담고 있는 Editor 컴포넌트에서 keyup 이벤트가 발생하게 되면 Editor를 리렌더링하게 된다. 이 과정에서 편집기 선택이 풀리게 된다. 즉, 마우스로 다시 선택해서 입력을 반복해야하는 번거로움이 발생하게 된다.
이를 해결하기 위해 특정 Document 클릭 시에 EditPage에 전달하는 초기 상태 값(객체)에 isRender 프로퍼티를 추가해서 전달해주었다. 그리고, 처음 렌더가 될 때 isRender 값을 변경시킴으로써 렌더가 발생하지 않게 구현했다.
▪︎ 새로고침 시 화면에 보여지는 Documents 목록을 유지하기 위해 로컬스토리지를 활용했다.
새로고침을 해도 화면에 보여지는 Documents 목록을 유지하고 싶었다. 고민하다가 다른 노션 클로닝 프로젝트에서 아이디어를 얻었다.
새로고침을 해도 화면에 보여지는 Documents 목록을 유지하기 위해 로컬스토리지의 showId 키의 값에 Documents의 id를 저장하고 새로고침(렌더) 시에 로컬스토리지에 Documents의 id가 포함되어 있다면 화면에 보여지도록 구현했다. 이 때, Root Document는 항상 보여지는 Document이므로 로컬스토리지에 저장되지 않는다.
위의 사진에서 Root Document를 제외한 3개의 Document의 id들이 로컬스토리지의 showId의 값으로 담겨있다. 로컬스토리지에 id들이 저장되어있기 때문에 새로고침을 해도 로컬스토리지에 담긴 id에 해당하는 document들이 보여지게 된다.
만약 토글을 눌러 위의 사진과 같이 Document를 숨기게 된다면 로컬스토리지에도 반영이 된다. 이대로 새로고침을 해도 유지가 된다.
참고로 로컬스토리지를 활용하여 현재 선택된 Document만 background-color 속성 값을 다르게 줄 수도 있었다. 위의 사진에 보이는 selectedListId가 바로 그것을 구현하는데에 활용됐다.
▪︎ Documents 목록이 계단식으로 보일 수 있도록 구현했다.
트리 구조의 Documents 목록을 화면에 렌더링 할 때, 동일 Depth의 Document는 동일한 세로선에 위치하게 하고, 하위 Document는 한 칸 들여서 위치하게 하고 싶었다. 이를 구현하기 위해 Documents의 목록을 생성하는 재귀에서 파라미터로 받은 depth에 따라 paddingLeft 속성값을 설정해주었다.
▪︎ Document에 마우스 포인터를 올리면 CSS 속성 값이 변경되도록 구현했다.
처음에는 이것을 이벤트 핸들러로 구현하려고 했다. 하지만 일부는 구현이 되었지만 일부는 동작하지 않았다. 이번 기회로 hover를 알게 되었고, hover에 대해 공부할 수 있는 기회가 되었다.
▪︎ Open된(로컬스토리지에 저장된 id가 있는) Document들의 상위 Document의 토글 버튼 변화 구현
가장 구현이 힘들었던 부분이었다. 거의 하루 반이 걸려서 구현에 성공했다. 아래와 사진과 같이 하위 Documents들이 화면에 보여지고 있다면 아래 화살표 이미지, 보여지지 않고 있다면 오른쪽 화살표 이미지로 변경되도록 구현하고 싶었다.
우선 이것을 구현하는 가장 핵심은 요소의 className을 add했다가 remove해야하고, 요소의 className에 따라 토글 버튼의 이미지를 변경하는 것이었다.
정말 오랜 시간을 투자한 만큼 다양한 방법을 사용해보았는데, 구현에 성공한 것처럼 보였어도 버그가 발생했고 Fix를 정말 여러 번 했다. 그러던 중, 상위의 Document의 토글 버튼은 하위의 Document의 id들이 로컬스토리지에 저장되어 있는지에 따라 버튼 이미지가 달라지게 되고, 따라서 재귀함수에서 상위의 Document에 해당하는 함수를 실행했을 때, 하위의 Document들의 id를 탐색하면 되겠다 싶었고, 그렇게 하려고 했으나 실패했다.
이 과정에서 두 가지 방법으로 시도했는데, 첫 번째는 DOM 요소 관점에서 접근한 것이었다. 즉, querySelector 메서드를 통해 하위 Document의 id가 로컬스토리지에 담겨있는지 확인하려했다. 뒤늦게 깨달았지만 당연히 이 방법으로는 실패하게 된다. 왜냐하면 아직 하위 Document에 해당하는 요소를 생성하지 않았기 때문이다.
두 번째로는 상위 Document에 해당하는 함수에서 API 통신을 통해 하위 Documents의 id를 알아내는 아이디어였다. 하지만 이 아이디어는 제대로 시도도 안해보고 그만두었다. 왜냐하면, API 통신으로 인해 Documents 목록 렌더가 상당히 느려지고, 토글 버튼을 한 번 누를 때 마다 딜레이가 발생하게 되었다.
두 가지 방법을 써보았는데 실패해서 낙담하고 있었을 때, 문득 반대로 생각해서 하위 Document에 해당하는 함수를 실행할 때 상위 Document의 요소에 접근해서 버튼 이미지를 변경하면 되지 않을까 싶었고, 구현에 성공했다.
🛸 마치며
데브코스에서의 첫 프로젝트가 이렇게 끝이 났다. 이전에 과제를 수행할 때는 헤맸었던 적도 많았다. 항상 강사님 코드를 따라치며 이해하려고 노력했으나 쉽지 않았다. 또, 내 것이 된다는 느낌도 적었었다.
이번 프로젝트에서는 혼자 힘으로 최대한 구현해봐야겠다고 다짐했고, 팀원들과의 피드백을 제외하고는 강사님의 이전 영상 속 코드를 참고하지 않으려 노력했다. 그 결과, 데이터의 흐름을 이해할 수 있었고 이전 과제에서 버벅댔던 기능도 이제는 보다 더 수월하게 구현할 수 있게되었다. 더 나아가서는, 단순히 구현에 급급한 모습이 아닌, '어떻게 해야 더 효율적으로 구현할 수 있을까' 라는 질문을 던지는 나로 성장할 수 있었다.
이번 프로젝트는 성공과 실패를 동시에 느낄 수 있었다. API 통신, history API, 상태 관리, UX 개선에 대한 고민 등 프로젝트 진행 과정에서 학습하고 느꼈던 감정들은 나에겐 큰 경험이었다. 또, 노력 끝에 원하는 기능을 구현한 점에서도 성공적이라는 평가를 하고 싶다. 하지만 해결해야 할 숙제들이 많다. 디바운스, 낙관적 업데이트 등 강의에서 배웠던 내용을 프로젝트에 녹여서 더 효율적으로 로직을 구현하고 싶다는 욕심도 있고, 인터넷으로 하나하나 찾아가며 했던 CSS는 갈 길이 아직 멀다.
다음 프로젝트에서는 더 발전된 모습을 발견할 수 있길 바라며 이만 회고를 마친다.
'프로그래머스 데브코스' 카테고리의 다른 글
[VanillaJS] 이벤트 핸들러를 지연시키는 디바운스(debounce) (0) | 2023.11.05 |
---|---|
[MIL-1] 230919 ~ 231026 프론트엔드 데브코스 회고 / 첫 프로젝트, 모딥다 스터디, 백준 스터디, 커피챗 (2) | 2023.10.28 |
[데브코스] 성장 중.. 과제 수행, 첫 PR, 코드리뷰 후기 (2) | 2023.10.23 |
[JavaScript] SPA와 History API (2) | 2023.10.17 |
[JavaScript] 인프런 코어 자바스크립트 수강후기 (0) | 2023.10.15 |