HTML, CSS, 순수 자바스크립트만 이용해서 동적인 그래프를 구현해보았다.
사용성 좋은 UI/UX를 만들기 위해 열심히 이것저것 작업했고,, (이 화면 말고도 더 있음)
좋은 아키텍쳐로 구현하기 위해 일주일 동안 밤도 새가며 고민했다.
리액트 없이 JS만으로 뭔가를 만들어본 적은 처음이었다.
리액트의 지원 없이 개발해보니, 더 많이 고민하며 개발하게 됐다.
그리고 useState
가 그리웠다,,
가장 중요하게 생각한 것은
데이터
와렌더링
간의 관계다.
리액트가 그렇듯, 데이터가 변하면 관련된 UI 컴포넌트는 저절로 싹 다 변하게 하고 싶었다.
결과물이야 솔직히 어떤 코드로도 만들 수 있지만, 이런 구조가 가장 바람직하다고 생각했다.
class Data {
constructor(id, value) {
this.id = id;
this.value = value;
}
// 해당 UI 렌더링
render() {
}
}
이런 생각도 해봤다. 데이터가 생기면 바로 UI를 보여주기 위해서...
하지만, 이렇게 되면 new Data()
로 데이터를 추가할 때는 보여줄 수 있지만, 데이터의 수정 삭제가 어렵다. 또한 데이터와 렌더링의 역할 분리가 되지 않고 있다. Data 클래스는 엄청나게 커진다.
이상한 걸 이미 알고 있어서 금방 접고, DataManager
를 구현했다.
// data-manager.js
class DataManager {
_dataStore = new Map();
// 데이터 추가
addData(data) {
if (!data) return;
this._dataStore.set(data.id, data);
}
getDataById(id) {}
updateDataById(id, newValue) {}
deleteData(id) {}
hasData() {}
isExistId() {}
...
}
데이터 추가, 수정, 삭제 시 전체 화면을 다시 그릴 필요는 없다고 생각했다.
따라서 다음과 같이 RenderType
을 설정했다.
// data-manager.js
const RenderType = {
ADD: 0,
UPDATE : 1,
DELETE : 2,
CLEAR : 3,
}
게임 개발을 할 때 Enum
값을 이용해 상태(State) 패턴을 많이 사용했는데,
자바스크립트에는 Enum
이 없으니, 비슷한 불변 효과를 주기 위해 Object.freeze()
와 Symbol()
을 활용했다.
const RenderType = Object.freeze({
ADD: Symbol('ADD'), // 데이터 추가
UPDATE: Symbol('UPDATE'), // 데이터 수정
DELETE: Symbol('DELETE'), // 데이터 삭제
CLEAR: Symbol('CLEAR'), // 데이터 초기화
});
그리고 아래와 같이 render()
함수에 type
을 인자로 받아 type
에 따라 필요한 부분만 랜더링되도록 구현했다.
// main.js
const render = (type, data = null) => {
switch (type) {
case RenderType.ADD:
renderGraph(data);
renderCard(data);
break;
case RenderType.UPDATE:
break;
case RenderType.DELETE:
graph.remove();
card.remove();
break;
case RenderType.CLEAR:
break;
}
updateJSONTextarea(); // 공통로직
};
RenderType
을 통한 분기만으로는,
여전히 사용자가 클릭을 했을 때 데이터를 추가하고, render() 함수도 번거롭게 같이 호출해야 하는 코드였다.
// main.js
const onClickAddButton = (e, data) => {
e.preventDefault();
dataManager.addData(data); // 데이터를 추가하고,
render(RenderType.ADD, data); // 추가된 데이터의 그래프 및 카드 렌더링
}
어떻게 하면 따로 render()
함수를 호출하지 않아도 자동으로 UI가 그려질까 고민했다.
이 때, 전 회사에서 사수 분이 알려주셨던 디자인 패턴 중 하나인 Observer 패턴
이 생각났다.
Observer 패턴
새로운 유튜브 영상이 업로드 되었을 때 모든 구독자들에게 알림이 가듯이, 데이터에 어떤 변화가 생겼을 때. 자신의 구독자에게 변화를 알리고 콜백 함수를 실행하게 하면 어떨까? 라는 생각이 들었다.
여기서 유튜브 채널은 DataManager
이고, 구독자는 render()
함수가 된다.
이 개념을 바탕으로 DataManager
를 리팩토링했다.
class DataManager {
_dataStore = new Map();
_subscriber = new Set(); // 여기에 구독
// 구독자에게 알림을 보냄
_notify(type, data = null) {
for (const callback of this.subscribers) {
if (!callback) return;
// 콜백 실행
callback(type, data);
}
}
// 유튜브 영상 올라옴
addData(data) {
if (!data) return;
this._dataStore.set(data.id, data);
// 구독자에게 알림 호출
this._notify(DataChange.ADD, data);
}
}
그리고 이제 더 이상 RenderType
이 아니라 데이터의 상태 변화를 나타내는DataChange
타입으로 이름을 바꿔주었다.
const DataChange = Object.freeze({
ADD: Symbol('ADD'), // 데이터 추가
UPDATE: Symbol('UPDATE'), // 데이터 수정
DELETE: Symbol('DELETE'), // 데이터 삭제
CLEAR: Symbol('CLEAR'), // 데이터 초기화
});
드디어 내가 원하는 방식 되었다.
이제 따로 render()
함수를 부르지 않아도 데이터가 변하면 저절로 UI가 그려진다.
onClickAddButton = (e, data) => {
e.preventDefault();
dataManager.addData(data); // 한 줄만 있으면 됨, render()는 따로 호출하지 않음
}
내가 구상하던 구조를 실제로 만들어보니 뿌듯했고, 드디어 코드에 대한 미련이 없어졌다.
그리고 앞으로 더 다양한 디자인 패턴을 익히고 코드에 적용시켜보고 싶은 욕심이 생겼다.
꾸준히 성장해보자 !!