Next.js에서 ssr을 이용해 d3 그래프 그리기.
D3.js(Data-Driven Documents)는 그 이름에서도 알 수 있듯이 DOM에 data를 바인딩 하도록 도와주는 라이브러리다. 보통 데이터를 표현하기 위한 그래프를 svg 형태로 렌더링하여 웹 페이지에 나타내는 용도로 많이 쓰인다.
React에서는 div 요소에 ref로 svg를 붙이고(append) svg에 그래프 요소들을 하나 하나씩 붙이는 방법으로 쓰일 수 있다.
위 코드는 crs로 d3를 사용하는 기본 코드이다. useEffect를 안에서 div 요소에 svg를 붙여 svg에 그래프 요소들은 추가하여 그래프를 구성해 나간다.
이제 이 svg에 축과 막대, 선, 원 등을 추가하여 데이터를 표현하는 것이 d3 라이브러리의 사용법이라고 볼 수 있다.
CSR에서 DOM요소에 붙이기도 용이하며, 인터렉티브한 차트를 구성할 거면 어차피 CSR의 영역으로 가야하기에 대부분의 경우 SSR이 필요하지 않다. 하지만 복잡한 그래프는 그리고 싶다면 CSR로 d3 그래프를 렌더링 할 때 몇가지 단점이 있다.
Layout Shift : d3는 데이터를 기반으로 동적으로 DOM을 조작하는데, 이 때 데이터가 페이지 로드 후에 도착하게 되면 d3가 이미 렌더링 된 DOM을 조작하면서 페이지의 레이아웃에 변화를 일으킨다. 이는 물론 스켈레톤 ui 등을 이용해 쉽게해결할 수 있지만, 그래프의 레이아웃이 정적으로 남아 페이지의 유연성을 떨어트린다.
추가적인 Data fetching : 프론트엔드 개발자로써 d3를 쓰는 목적은 결국 data를 시각화 한다는 것임을 명심해야 한다. data를 클라이언트로 들고 와서 렌더링 하는 것은 필요한 수단이 될 수 있지만, 그 작업이 자원을 많이 소모하는 경우엔 피해야 하는 장애물이 될수도 있다.
보안 측면 : 웹 프로젝트를 구성하는 자료들을 공개하길 원하는 사람들은 없을 것이다. 때때로 d3를 용하여 svg를 그릴 때 수집 데이터와 같은 자원들을 더 노출 시킬 수 있다.
성능 측면 : 이런 추가적인 자원이나 계산들이 클라이언트로 오게 된다면 사용자 기기 성능에 크게 의존하는 형태가 될 것이다. d3는 비교적 무거운 작업을 수행한다는 점을 명심해야 한다.
지도와 같이 자양한 자원을 기반으로 그려야 하는 그래프들은 위의 모든 것들이 문제가 된다. 고해상도의 지도를 표현하기 위한 지리정보만 수십 메가바이트에 달하고, 그려진 지도위에 표시해야할 데이터들은 당연히 덤으로 딸려와야 한다. 또한 지리정보를 화면에 맞게 투영하고 그리는 작업 또한 웹 성능에 무시할 수없는 부분이 된다. 이런 작업들은 모두 csr로 처리하길 원한다면, 페이지의 다른 요소들이 렌더링 되는 시간과 더해져 해당 페이지의 초기 로딩 속도가 지나치게 오래 걸리는 문제가 발생할 수 있다. (naive한 방법으로 csr에서 지도를 그리는 초기 버전에서 10초 이상 소요됨)
d3의 장점은 만들어진 그래프를 svg로 만들어주고, 나아가 이 svg의 요소들을 이후에 조정하면서 데이터를 표현할 수 있다는 점이다. 따라서 기본데이터를 기반으로 svg를 만들어놓는다면, 위의 모든 작업은 서버에서 svg를 다운로드 하는 것으로 매우 간단해진다.
DOM에 svg를 그려 붙여주는(append) 것이 전부라면, 서버에서도 svg를 그릴 수 있기 때문에 이를 DOM에 붙여주기만 할 수 있다면 서버에서도 d3를 사용해 그래프를 랜더링 할 수 있다. 우리는 svg를 붙여줄 적당한 DOM을 이미 알고있다.
js dom을 사용하면 CSR에서와 같이 자연스럽게 d3를 사용해 그래프를 구성할 수 있다. 하지만 이 경우 상호작용을 등록하는 일이 뜻대로 되지 않을 수 있다. 그도 그럴게 SSR로 svg를 미리 준비해서 클라이언트에 보내는 것이기 때문에 상호작용을 위한 자바스크립트 코드가 정상적으로 작동할리 없다.
사실 Next.js에서 일반적인 방법으로 서버에서 렌더링 되는 컴포넌트를 만든다면(App directory에서는 기본적으로 모든 컴포넌트가 pre render 되긴 함), Next.js가 자연스럽게 hydration을 통해 자바스크립트를 html에 등록해준다. 하지만 d3는 Next.js의 hydration 메커니즘에 속하지 못하므로 유저가 직접 하이드레이션 해줘야 한다.
hydration을 하는 가장 간단한 방법은 svg를 만든 서버 컴포넌트에서 client component를 렌더링 하고 client component에서 (useEffect의 사용 적절성 여부는 잠시 접어두고) useEffect hook을 통해 이벤트 리스너를 등록해주는 일일 것이다. client component의 useEffect 훅은 컴포넌트가 렌더링 되면서 실행되기 때문에 svg에 성공적으로 이벤트 리스너를 등록할 수 있다.
SSR로 위 방법을 이용해서 라인차트를 그려보자. 빈페이지에 차트 컴포넌트만을 불러오는 간단한 구성이다.
차트의 viewbox를 설정하면 자신이 가질 수있는 최대 너비와 높이에서 비율을 유지하며 커지므로 부모 요소에 가로 세로를 정해줬다. width와 height를 직접 설정하여 크기를 조정할 수 있겠지만 viewbox를 설정하는 편이 웹을 좀 더 유연하게 구성할 수 있게 해준다.
jsdom에 svg를 붙이고 컨테이너를 선언하고 viewbox영역을 확인하기 위해 border를 주고 그안에서 padding을 설정해 svg를 선언한다. 이는 필수적인 작업은 아니지만, 본격적으로 차트를 그리기 전에 svg 범위와 정상적으로 렌더링 되는지를 확인하기 위함이다. 위 코드를 그래도 사용하면 아래와 같은 svg를 확인할 수 있다. 여기에서는 d3로 이벤트를 등록할 수 없다.
이제 도화지가 준비 됐으니, 여기에 원하는 요소들을 그려주기만 하면 된다. 아래는 라인차트를 그리기위한 x,y축과 라인을 그리는 간단한 코드를 제공한다.
svg에 데이터를 표현하려면 먼저 scale을 설정해줘야 한다. 위 코드에서는 x, y로 스케일을 선언하고 이를 이용해 이후 모든 요소들을 스케일하여 svg에 그려준다. 위 코드를 더하고 svg를 채웠던 `rect`를 빼주면 아래와 같은 간단한 선차트가 완성된다. (이곳에서 `.on()` 메서드로 이벤트를 등록할 순 있지만 작동하지는 않는다.)
정적인 데이터를 이용하여 그래프를 화면에 표기하는 것만이 목적이라면 여기까지가 필요한 작업의 전부이다. 하지만 그래프에 상호작용을 입히고 싶다면 이젠 클라이언트에 그 일을 위임해야 한다. 어떤 상호작용을 하느냐에 따라 컴포넌트에 width, height등 그래프를 그리는데 필요한 변수들이나 데이터등을 prop으로 넘겨줄 수 있다. 이 예제에서는 svg에 마우스를 올렸을 때 선의 색깔을 바꿔보도록 한다. 하지만 여기서 먼저 해야할 일이 있다. mousemove 이벤트는 해당 요소가 채워지지 않은 영역에서 작동하지 않는데, 우리의 그래프는 선 영역과 축 영역만 채워져있다. 말그대로 svg는 빈 도화지 이므로 바탕을 칠해줘야 한다. 그리고 효과를 줄 요소를 선택해야 하니 클래스명을 지정하여 요소를 특정할 수 있게 해줘야 한다.
server component에서 여기까지 준비 됐다면 client component에서 이벤트를 등록할 수 있다.
이제 커서 위치에 따라 그래프 선의 색이 바뀌는 것을 확인할 수 있다.