바닐라 JS로 Notion을 클로닝한 프로젝트를 진행하며 느꼈던 점들을 정리해보고자 한다.
이번 프로젝트를 통해서 처음으로 애플리케이션을 컴포넌트 단위로 개발해볼 수 있었다. 물론 컴포넌트의 설계와 구성, 그리고 상태 관리 개념을 제대로 이해하지 못하여 매우 미흡하였지만.
설계
(최초의 계획과는 달라졌지만) 내가 구현한 컴포넌트 구조를 도식화하면 위와 같다.
계획🧐
프로젝트 시작에 앞서 고려했던 것은 크게 두 가지였다.
첫 번째는 Document List와 관련된 로직을 담당하는 Control Page와 Editor 쪽 로직을 담당하는 Content Page로 애플리케이션의 흐름을 나눠, 리스트에서의 변화와 에디터에서의 변화가 독립적으로 동작할 수 있도록 구현하는 것.
두 번째는 서버에 요청하여 응답 받은 데이터에 대해서 컴포넌트의 조작을 최소화 하여 데이터를 훼손하지 않는 것. 즉, 상위 컴포넌트에서는 서버로부터 응답받은 데이터를 최대한 있는 그대로 하위 컴포넌트로 흘려주고, 하위 컴포넌트에서는 전달받은 데이터에서 필요한 부분만 취하여 렌더링에 사용하고자 했다.
결과😢
결론적으로 말하자면, ‘컴포넌트와 상태 관리’에 대한 이해가 미흡하여 설계 단계에서부터 어긋났던 것 같다.
먼저 Control Page와 Content Page 사이의 상호작용을 지양하려다가 각 컴포넌트에서 동시에 서버에 접근하여 각각 데이터를 받아오게 되는 참사가 발생했다.
또한 상태 관리의 목적과 그 필요성에 대해 이해가 부족했었기 때문에 각 컴포넌트가 어떤 상태를 보유하고 그것을 어떻게 활용해야할지에 대해 전혀 고려하지 못했다. 데이터를 최대한 조작하지 않으려는 나의 방법은 ‘객체의 불변성’ 개념과도 맞지 않는 사용법이었다. 각 컴포넌트에서 관리할 상태를 제대로 규정하지 못하다보니, 결과적으로는 상태가 전혀 필요없는 로직을 구성하게 돼버렸다.
프로젝트 동작원리
내가 구현한 로직은 다음과 같다.
- main 컴포넌트는 App 컴포넌트를 호출한다.
-
App 컴포넌트는 라우팅을 담당한다.
- url 변화를 감지하는 customEvent를 정의하여 pathname을 파싱한다.
- pathname에 따라 하위 컴포넌트가 수행할 동작을 내려준다.
-
Control Page
- Document List를 렌더링 하기 위해 서버에 Document 정보를 요청한다.
- 새로운 Document를 생성하면 해당 Document 정보를 서버에 저장한다.
- 토글이 발생하거나, 선택이 된 Document의 정보를 Local Storage에 저장하여 새로고침이나 뒤로가기가 발생하더라도 해당 상태를 기억할 수 있도록 한다.
- 서버로부터 응답받은 데이터와 Local Storage로부터 가져온 데이터를 하위 컴포넌트로 전달해준다.
- 하위 컴포넌트에서 변화가 발생할 때마다, App 컴포넌트에서 라우팅을 다시 수행하도록 한다.
-
Content Page
- Content Page는 Editor에 활성화 시킬 Document 관리를 담당한다.
- pathname을 통해 특정 Document가 필요한 상황이라면 서버에 해당 Document 정보를 요청한다.
- Editor에서 변화가 발생하면, 해당 내용을 PUT 요청을 통해 서버에 반영한다.
- Document의 삭제가 발생하면 DELETE 요청을 통해 서버에 반영한다.
- 서버로부터 응답받은 데이터를 하위 컴포넌트로 전달해준다.
- 하위 컴포넌트에서 변화가 발생할 때마다, App 컴포넌트에서 라우팅을 다시 수행하도록 한다.
-
렌더링 담당 컴포넌트
5.1. Document List
- 상위 컴포넌트인 Control Page로부터 전달받은 데이터를 화면에 렌더링 한다.
-
생성한 DOM 요소에 이벤트 리스너를 등록한다.
5.2. Editor
- 입력과 수정이 가능한 editor를 렌더링한다.
- 상위 컴포넌트인 Content Page로부터 전달받은 데이터를 editor에 수정 가능 상태로 세팅한다.
문제점과 개선 방안
로직의 문제점
나는 특정 Document를 클릭하거나, 에디터를 통해 내용을 수정하거나, Document를 생성 또는 삭제하는 경우 등등 애플리케이션에서 발생하는 모든 변화를 다시 화면에 반영하기 위한 방법으로 App 컴포넌트에서 라우팅을 다시 하도록 설계했다. 그때마다 서버로부터 최신화된 데이터를 요청하고 응답하는 통신이 발생하게 되었고, 모든 변화에 대해서 새로운 데이터를 응답받아 전달 하려다보니 각 컴포넌트에서는 ‘상태’를 관리할 필요성이 없어졌다. 극단적으로 말하면 화면 렌더링을 담당하는 최하위의 컴포넌트들이 직접 서버에서 데이터를 가져다 쓰는 것과 다를 바 없는 로직이 되어버린 것이다.
개선 방안
1. 변화된 데이터의 렌더링이 필요한 경우와 그렇지 않은 경우의 구분
내가 구현한 로직에서는 새로운 이벤트가 발생하면 묻지도 따지지도 않고 서버로부터 최신 데이터를 응답받아 해당 데이터를 가지고 렌더링을 다시 수행한다고 했다.
그러나 애플리케이션에서는 분명 새로운 데이터가 렌더링에 반영되지 않아도 되는 상황이 존재한다. Document List에서 이미 존재하는 Document를 클릭한 경우, Editor에서 Document의 title이나 content가 변경되는 경우 등이 그렇다. 에디터의 경우 궁극적으로는 서버에 변경된 데이터가 반영되어야 하겠지만, 당장은 화면에 변경 사항을 다시 렌더링할 필요가 없었다.
이처럼 렌더링 상황의 특성을 구분한다면, 불필요한 서버와의 통신을 줄일 수 있고 렌더링 자체의 발생 횟수도 줄일 수 있을 것이다.
💡 이를 판단하기 위해서는 결국 기존의 상태와 새로운 상태의 차이를 비교하는 것이 우선적으로 필요하다는 생각이 든다. 무조건 데이터를 업데이트 하고, 무조건 렌더링에 반영하기 보다는 ‘상태 변화가 일어났는가?‘를 판단하여 다음 흐름을 쉽게 예측할 수 있도록 만드는 것이 중요하니..!
2. 상태의 구분과 관리, ‘불변성’
상황에 따라서 선택적으로 렌더링을 수행하기 위해서는 결국 각 렌더링 상황에 필요한 상태를 구분하여 관리해야할 필요성이 생긴다.
컴포넌트가 관리해야할 상태를 구분하는 방법에는 여러가지가 있을 것이다. 멘토님의 말씀을 들어보니 하나의 컴포넌트에서 모든 데이터를 관리할 필요도 없었다. 특정 컴포넌트만 사용하는 데이터가 있다면 해당 컴포넌트에서만 관리를 하면 될 것이다. 그러나 이번 프로젝트는 API가 다양하지 않고 컴포넌트 계층도 얕기 때문에 서버와의 통신을 최소화하면서 일관되게 흐름을 관리하기 위해서는 상위 컴포넌트에서 총체적으로 데이터를 관리한다면 좋을 것 같았다.
따라서 이번 프로젝트를 수정한다면,
서버와의 통신은 App 컴포넌트에서만 일괄적으로 담당하고, 응답받은 데이터를 하위 컴포넌트에 전달하는 과정에서 App 컴포넌트가 해당 컴포넌트에 필요한 부분만을 가공하여 전달하는 방식을 사용해보고자 한다. 여기서 한 가지 더 추가적으로 하위 컴포넌트에 전달되어야 하는 정보는 하위 컴포넌트가 렌더링이 필요한지 아닌지에 대한 여부일 것이다. 이를 통해 데이터를 하위 계층으로 전달하더라도, 렌더링이 불필요한 상황이라면 당장은 렌더링을 수행하지 않도록 구분하는 것이다.
만약 App 컴포넌트의 하위 컴포넌트에서 자신의 하위 컴포넌트에 또다시 데이터를 가공하여 전달해야 하는 경우, 해당 컴포넌트에서는특정 데이터를 관리하는 ‘상태’가 필요할 것이다. 반면 하위 컴포넌트가 동일한 내용의 데이터를 필요로 한다면 현재 컴포넌트에서는 굳이 상태를 가질 필요가 없어지는 것이다. 이는 매번 데이터를 직접적으로 조작하지 않고, 상태 관리라는 흐름을 통해서만 변화된 내용을 렌더링에 반영하기 위한 방법이다. 때문에 상태 관리는 불변성이라는 개념과도 관련 깊다.
멘토님의 설명에 따르면, 객체의 불변성을 유지하기 위해서는 ‘컴포넌트에 필요한 데이터만 가공하여 전달’하는 것이 필요하다. 즉 App 컴포넌트에서 서버로부터 응답 받은 덩어리 데이터를 각 컴포넌트의 상황에 맞게 가공하여 필요한 데이터만 전달해주는 것이다. 여기서의 불변은 ‘데이터를 변경하지 말자!’가 아니라 ‘객체를 직접 변경하지 말자!’라는 의미로 받아들이면 될 것 같다. 만약 변경이 필요한 상황이라면, 기존 객체(데이터)에서 필요한 부분을 추가하거나 제거한 새로운 객체를 생성하여 다음 컴포넌트에 전달하는 방법인 것이다. 함수형 프로그래밍을 떠올려보자..
나는 이번 프로젝트에서 상위 컴포넌트가 응답받은 데이터를 전혀 수정하지 않고 하위 컴포넌트로 전달하고자 했다. 그러다보니 불필요한 정보들도 함께 전달되었고 일부 데이터의 수정만 필요한 경우에도 전체 데이터를 새롭게 받아와야 한다는 문제가 생겨버린 것이다. 상태 관리의 목적, 불변성의 개념에 대한 이해가 부족했기 때문이라고 생각한다.
💡 멘토님께서 강조하시는 내용을 기억하자. 컴포넌트 구성, 상태 관리를 하는 것의 핵심은 상태 변화만으로 동작 흐름이 어떻게 달라지는지, 렌더링이 어떻게 달라지는지를 쉽게 예측할 수 있도록 만드는 것이다. 따라서 각 컴포넌트를 최대한 독립적으로 기능할 수 있도록 만들어주는 것이 중요하다. 나는 컴포넌트들이 의존적이었고, 상태 추적만으로 변화를 예측할 수 있는 흐름을 전혀 구성하지 못했기 때문에 효율적이지 못한 설계가 되어버렸다.
느낀점
프로젝트를 진행하며 혼자서, 그리고 팀원들과 정말 많은 이야기를 나누었다. 어느 컴포넌트에서 어떠한 상태를 가질 것인지, 이벤트 핸들링은 어디서 담당할 것인지 등등. 그러나 나는 보다 근본적인 고민이 부족했다고 생각한다. 왜 애플리케이션을 컴포넌트로 나누어 구성할 것이며, 왜 컴포넌트는 상태 관리를 통해 데이터의 흐름을 제어하려는 것일까에 대해서 좀 더 공부하고 고민했다면 프로젝트를 진행하며 맞닥들인 선택의 과정들에서 명확한 기준에 의한 합리적인 판단이 가능하지 않았을까 생각한다. 컴포넌트와 상태 관리에 대해서 좀 더 자세한 학습이 필요하다. 각각의 개념을 정확하게 파악하고, 효율적인 컴포넌트 구성과 상태 관리 방식들을 익힐 수 있도록 하자. 👉Vanilla Javascript로 웹 컴포넌트 만들기 👉Presentational and Container Components 👉데이터 상태 관리. 그것을 알려주마
또한 애플리케이션을 단순히 동작하도록 구현하는 것이 아니라, 렌더링이나 통신적인 측면에서 불필요한 비용을 최소화하여 보다 효율적인 애플리케이션 로직을 구현할 수 있도록 설계 과정에서 더 신경쓸 필요가 있다. 서비스를 제공하는 공급자의 입장에서도, 사용자의 입장에서도 모두 중요한 부분일테니까.
함수형 프로그래밍에서도 살짝 다뤘었지만 ‘불변성’에 대해서도 그 개념과 필요성을 정확하게 이해하는 것이 앞으로 코드의 품질을 높이는 데 중요하게 작용할 것 같다. 이밖에도 로컬스토리지를 활용함에 있어서의 XSS 같은 문제점도 알아보자.
그리고 무엇보다도 애플리케이션을 컴포넌트로 구성하고 개발하는 방식에 익숙해지는 것이 중요하다고 생각한다! 사이드 프로젝트 혹은 과제 하나를 하더라도 설계 단계에서부터 충분히 고민하고, 이를 코드로 구현하는 과정을 습관화 하자..
아, 그리고 CSS도😭