활동내역.zip/Bootcamp

[Remember Plus 2주차] D3.js를 사용하여 2D 시각화 해보기

  • -
728x90

프로젝트 Repository

 

GitHub - SV-Summer-BootCamp-Team-F/frontend

Contribute to SV-Summer-BootCamp-Team-F/frontend development by creating an account on GitHub.

github.com


목표

프로젝트에서 D3를 사용하는 페이지를 하나 맡았다.

아래에 있는 사이트처럼 마우스를 통해 넓은 화면을 이동할 수 있도록 하며 확대, 축소 기능까지 넣는 것을 목표로 한다.

 

React D3 Tree

 

bkrem.github.io


프로젝트 스펙

React, Vite, TailwindCSS, TypeScript


Issue 1. D3.js 설치

우선, 2D 시각화에 특화되어있는 D3를 설치한다.

이 때, 주의해야 할 점이 하나가 있다.

바로 typescript를 사용하는 경우 패키지 설치 방법이 약간 달라진다는 것이다.

yarn add d3

아래의 패키지 또한 설치하여 타입스크립트에 맞는 d3를 설치한다.

yarn add types/d3

 

Issue 2. function => const로 바꿀 때 형식 주의하기

// 기존에 에러가 났던 이유 = 화살표 함수의 형식을 지키지 않았다
const foo = ()<type>{}

// 해결
const foo = ()<type> => {}

 


추가된 파일들

그렇게 전체적으로 추가되고 모듈화가 된 상태의 폴더구조는 다음과 같다.

src
 ┣ components
 ┃ ┣ relation
 ┃ ┃ ┣ Chart.tsx
 ┃ ┃ ┣ ChartContent.tsx
 ┃ ┃ ┣ RelationGraph.tsx
 ┃ ┃ ┗ ZoomableSVG.tsx
 ┣ hooks
 ┣ pages
 ┃ ┣ Relation
 ┃ ┃ ┗ RelationPage.tsx
 ┣ types
 ┃ ┗ types.ts
 ┣ utils
 ┃ ┗ generateDataPoints.ts
 ┣ App.tsx

App.tsx에 라우팅을 설정(Router V6 적용)
components/relation 페이지에 관계를 나타내기 위한 기능을 담은 컴포넌트를 분할 작성

pages에 실제로 relation page를 나타내기 위한 베이스 페이지 파일 작성

utils에 return 값이 html이 아닌 함수를 모듈화

types에 타입들을 모아두기 위해 types 파일 생성

 

Chart.tsx

기본 차트 생성을 담당하는 Chart 구성 요소 포함

창 크기와 표시할 데이터의 상태를 처리

창 크기 조정을 위한 이벤트 리스너를 설정

ZoomableSVG 및 ChartContent 구성 요소 사용

import { useState, useEffect } from "react";
import { ChartPropsType, DataPointType } from "../../types/types";
import generateDataPoints from "../../utils/generateDataPoints";
import ChartContent from "./ChartContent";
import ZoomableSVG from "./ZoomableSVG";

const Chart: React.FC<ChartPropsType> = ({ data, n, maxR }) => {
  // 차트의 크기를 위한 상태 설정 (창의 넓이와 높이)
  const [dimensions, setDimensions] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  
  // 로컬 데이터를 위한 상태 설정
  const [localData, setLocalData] = useState<DataPointType[]>(data);

  // 데이터를 업데이트하는 함수
  const updateData = () => {
    setLocalData(generateDataPoints(n, maxR));
  };

  useEffect(() => {
    // 창 크기가 변경될 때 차트의 크기를 업데이트하는 함수
    const handleResize = () => {
      setDimensions({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // 창 크기가 변경될 때마다 이벤트 리스너 호출
    window.addEventListener("resize", handleResize);

    // 컴포넌트가 언마운트될 때 이벤트 리스너 제거 (메모리 누수 방지)
    return () => window.removeEventListener("resize", handleResize);
  }, []);  // 빈 의존성 배열을 사용하여 마운트시에만 이벤트 리스너를 설정

  return (
    // 차트를 그리는 SVG와 그 내용을 ZoomableSVG 컴포넌트와 ChartContent 컴포넌트로 분리
    <ZoomableSVG width={dimensions.width} height={dimensions.height} updateData={updateData}>
      <ChartContent width={dimensions.width} height={dimensions.height} data={localData} />
    </ZoomableSVG>
  );
};

export default Chart;


ChartContent.tsx

제공된 데이터를 기반으로 차트의 시각적 요소를 생성

import { ChartContentPropsType } from "../../types/types";

// ChartContent는 width, height, data를 props로 받아 차트의 내용을 그려주는 컴포넌트입니다.
const ChartContent : React.FC<ChartContentPropsType> = ({ width, height, data }) => { 
  // translate를 통해 차트의 중심을 SVG의 중심으로 이동합니다.
  return (
    <g transform={`translate(${width/2}, ${height/2})`}>
      {/* 
        data 배열을 맵핑하여 각 데이터 포인트를 원으로 표현합니다.
        각 원의 중심점은 (x, y)이며, 반지름은 5, 색깔은 skyblue입니다.
      */}
      { data.map(({ x, y }, i) => (
        <g key={i}>
          <circle cx={x}cy={y}r={5}fill="skyblue"/>
        </g>
      ))}
    </g>
  );
};

export default ChartContent;


RelationGraph.tsx

시각화를 위한 기본 상위 구성 요소 역할을 하는 RelationGraph 구성 요소가 포함

임의의 데이터를 초기화하고 차트 컴포넌트를 사용한다.

import React, { useState } from "react";
import { DataPointType } from "../../types/types";
import generateDataPoints from "../../utils/generateDataPoints";
import Chart from "./Chart";

// RelationGraph는 상호작용 가능한 차트를 생성하고 보여주는 컴포넌트입니다.
const RelationGraph: React.FC = () => {
  const n = 100; // 데이터 포인트의 수를 정의합니다.
  const maxR = 100; // 데이터 포인트의 최대 반지름을 정의합니다.

  // 초기 데이터를 생성하고, 이를 state로 관리합니다.
  // generateDataPoints는 n개의 무작위 데이터 포인트를 생성하는 함수입니다.
  const [data, setData] = useState<DataPointType[]>(generateDataPoints(n, maxR));

  return (
    <div>
      <div className="flex items-center justify-center min-h-screen min-w-full border-black">
        {/* Chart 컴포넌트는 생성된 데이터를 사용하여 차트를 그리고 상호작용을 처리합니다. */}
        <Chart data={data} n={n} maxR={maxR} />
      </div>
    </div>
  );
};

export default RelationGraph;

 

ZoomableSVG.tsx

확대/축소 및 이동 기능이 있는 차트용 SVG 컨테이너를 렌더링

import { zoom, D3ZoomEvent, select } from "d3";
import { useRef, useState, useEffect } from "react";
import { ZoomableSVGPropsType } from "../../types/types";

// ZoomableSVG는 상호작용 가능한 SVG 컴포넌트입니다. 이 컴포넌트는 확대/축소 기능을 제공하며, 
// 데이터가 변경되면 확대/축소 상태를 초기화하는 버튼을 포함하고 있습니다.
const ZoomableSVG: React.FC<ZoomableSVGPropsType> = ({ children, width, height, updateData }) => {
  // svgRef는 SVG 요소를 참조하는 데 사용되는 참조입니다.
  const svgRef = useRef<SVGSVGElement | null>(null);
  
  // k, x, y는 SVG 요소의 확대/축소 및 이동을 위한 상태 변수입니다.
  const [k, setK] = useState<number>(1); // 확대/축소 비율
  const [x, setX] = useState<number>(0); // x 위치
  const [y, setY] = useState<number>(0); // y 위치

  // resetZoom 함수는 확대/축소 상태를 초기화합니다.
  const resetZoom = () => {
    setK(1);
    setX(0);
    setY(0);
  };

  useEffect(() => {
    // zoomHandler는 확대/축소 이벤트를 처리하는 핸들러입니다.
    const zoomHandler = zoom<SVGSVGElement, unknown>().on(
      "zoom",
      (event: D3ZoomEvent<SVGSVGElement, unknown>) => {
        // 이벤트에서 변형 정보를 가져와 상태를 업데이트합니다.
        const { x, y, k } = event.transform;
        setK(k);
        setX(x);
        setY(y);
      }
    );

    // SVG 요소가 준비되면 zoom 핸들러를 이에 적용합니다.
    if (svgRef.current) {
      select(svgRef.current).call(zoomHandler);
    }
  }, []);

  return (
    <svg ref={svgRef} width={width} height={height}>
      {/* 
        transform 속성을 이용해, 확대/축소와 이동을 적용합니다. 
        이 때, children은 확대/축소와 이동이 적용되는 SVG 요소들을 나타냅니다.
      */}
      <g transform={`translate(${x}, ${y}) scale(${k})`}>{children}</g>
      {/* 
        foreignObject 요소는 SVG 내에서 HTML 콘텐츠를 포함할 수 있도록 합니다. 
        여기서는 'Update Data' 버튼을 추가하였습니다.
      */}
      <foreignObject x={10} y={10} width={200} height={200}>
        <button
          onClick={() => {
            updateData(); // 버튼 클릭 시 데이터를 업데이트하고
            resetZoom(); // 확대/축소 상태를 초기화합니다.
          }}
        >
          Update Data
        </button>
      </foreignObject>
      {/* 텍스트 요소를 SVG에 추가합니다. */}
      <text x="10" y="10" fill="black">
        Hello World
      </text>
    </svg>
  );
};

export default ZoomableSVG;


RelationPage.tsx

실제로 렌더링되는 기본 페이지

추가 페이지 수준 컨텍스트 또는 기능을 포함할 수도 있다.

import RelationGraph from "../../components/relation/RelationGraph";

const RelationPage = () => {
  return (
    <div className="flex items-center justify-center w-screen h-screen">
      <RelationGraph />
    </div>
  );
};

export default RelationPage;

 

types.ts

프로젝트에서 사용되는 TypeScript type들의 모음

// 각 데이터 포인트를 나타내는 타입입니다. x와 y 좌표를 가집니다.
export type DataPointType = {
  x: number;
  y: number;
};

// ZoomableSVG 컴포넌트의 속성 타입입니다. SVG의 가로, 세로 크기,
// 업데이트 함수, 그리고 자식 요소들을 props로 받습니다.
export type ZoomableSVGPropsType = {
  children: React.ReactNode; // SVG 내부에 렌더링 될 요소들
  width: number; // SVG의 가로 크기
  height: number; // SVG의 세로 크기
  updateData: () => void; // 데이터를 업데이트하는 함수
};

// ChartContent 컴포넌트의 속성 타입입니다. Chart의 가로, 세로 크기,
// 그리고 데이터를 props로 받습니다.
export type ChartContentPropsType = {
  width: number; // Chart의 가로 크기
  height: number; // Chart의 세로 크기
  data: DataPointType[]; // 데이터 포인트들의 배열
};

// Chart 컴포넌트의 속성 타입입니다. 데이터, n(생성할 데이터 포인트의 수),
// maxR(데이터 포인트 생성에 사용되는 최대 반지름)를 props로 받습니다.
export type ChartPropsType = {
  data: DataPointType[]; // 데이터 포인트들의 배열
  n: number; // 생성할 데이터 포인트의 수
  maxR: number; // 데이터 포인트 생성에 사용되는 최대 반지름
};

 

generateDataPoints.ts

시각화에 사용할 임의 데이터 포인트 집합을 생성하는 유틸리티 함수

// types/types에서 DataPointType을 가져옵니다.
import { DataPointType } from "../types/types";

// generateDataPoints는 임의의 데이터 포인트를 생성하는 함수입니다.
const generateDataPoints = ( n : number , maxR : number ): DataPointType[] => {
  // 데이터 포인트를 저장할 빈 배열을 생성합니다.
  const dataPoints: DataPointType[] = [];
  
  // n 개의 데이터 포인트를 생성합니다.
  for (let i = 0; i < n; i++) {
    // 반지름 r은 0부터 maxR 사이의 임의의 값입니다.
    const r = Math.random() * maxR;
    // 각도 t는 0부터 2π 사이의 임의의 값입니다.
    const t = Math.random() * 2 * Math.PI;
    
    // (r cos(t), r sin(t)) 형태의 임의의 점을 생성하여 배열에 추가합니다.
    // 이는 극 좌표 (r, t)를 직교 좌표 (x, y)로 변환하는 과정입니다.
    dataPoints.push({
      x: r * Math.cos(t),
      y: r * Math.sin(t),
    });
  }

  // 생성된 데이터 포인트 배열을 반환합니다.
  return dataPoints;
};

// generateDataPoints 함수를 내보냅니다.
// 이를 통해 다른 파일에서 이 함수를 import하여 사용할 수 있습니다.
export default generateDataPoints;

 

.ts와 .tsx의 차이는 무엇일까?

사실 이 둘의 이름에 따른 기능적인 차이는 없다.

다만, return 값에 html 형식이 포함되어있다면 .tsx를, 포함되어있지 않다면 .ts로 나누어

유지보수의 관점에서 나눈다고 생각할 수 있다.

 

utils랑 hooks의 차이점은 무엇일까?

간단하게 말해보자면 react의 함수인 useState, useRef, useEffect 등이 들어간 함수면 hooks로 분류되고, 그게 아닌 순수 자바스크립트(여기서는 TypeScript)로 이루어진 로직이면 utils에 포함된다.

 

아래의 글을 통해 확인해볼 수 있다.

 

[Folder Structure] Hooks? Utils? 차이점이 뭘까?

프로젝트 개발을 하던 중에 함수를 따로 모듈화 하던 중 갑자기 궁금해 진 것이 있었습니다. "이렇게 특정 기능을 재사용하거나 관리하기 쉽게 따로 모듈화를 하는 건 좋은데, 어느 폴더에 넣어

time-map-installer.tistory.com


결과 화면(GIPHY Capture. The GIF Maker 사용, AppStore)


Next Issue. 개선이 필요한 부분들

1. 업데이트 했을 때 위치 다시 중앙으로 돌리기

  • 업데이트 버튼을 통해 중앙으로 돌리고 줌 상태도 초기화하는 데 성공했지만 마우스로 드래그를 하려고 할 시 바로 이전의 위치와 줌 상태로 돌아간다는 점을 해결해야 한다.

좌측부터 문제가 발생한 순서

 

2. types 파일의 이름 변경 필요성

  • 타입의 종류를 한 번 더 나누어 유지보수에 도움이 될 형태로 이름을 변경하면 좋을 것으로 보인다

3. 관계도에 맞는 그래프 새로 설정 필요

  • 임시로 점 형태의 그래프를 나타내었지만, 실제로 구현해야 하는 부분은 노드와 노드간의 관계를 나타내는 그래프이므로 이 부분에 대한 작업이 필요하다

4. SVG에 배경 넣기

  • 현재는 SVG를 화면 크기에 맞게 넓혀둔 상태이고, 그 위에 UI들을 넣어둔 페이지의 구성을 하고 있다
  • SVG에 배경을 넣는 방법을 찾아서 실제 디자인 컨셉에 가깝게 구현할 예정이다

End

728x90
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.