junyeokk
Blog
React·2024. 12. 02

Recharts

데이터를 시각화해야 할 때 차트 라이브러리를 직접 만드는 건 현실적이지 않다. SVG 좌표 계산, 축 레이블 배치, 애니메이션, 반응형 처리 등 신경 써야 할 게 한두 가지가 아니다. D3.js가 가장 강력한 시각화 라이브러리지만, React와 함께 쓰면 DOM 제어권이 충돌하는 문제가 있다. D3는 직접 DOM을 조작하고, React도 Virtual DOM으로 DOM을 관리하기 때문이다.

Recharts는 이 문제를 해결한다. D3의 계산 로직(스케일, 레이아웃)은 내부적으로 사용하되, 렌더링은 React 컴포넌트로 한다. 즉 React의 선언적 방식 그대로 차트를 만들 수 있다.


왜 Recharts인가

React 차트 라이브러리는 여러 가지가 있다.

라이브러리특징단점
Chart.js (react-chartjs-2)Canvas 기반, 가볍고 빠름Canvas라서 개별 요소 스타일링 어려움
VictoryReact 네이티브, 선언적번들 크기 큼, 복잡한 차트에서 성능 이슈
NivoD3 기반, 다양한 차트 타입커스터마이징 자유도 낮음
RechartsD3 기반 SVG, React 컴포넌트 조합매우 복잡한 시각화에는 한계

Recharts의 핵심 장점은 조합(Composition) 이다. 차트를 하나의 설정 객체로 정의하는 게 아니라, <BarChart>, <XAxis>, <Tooltip> 같은 컴포넌트를 조합해서 만든다. React 개발자에게 가장 직관적인 방식이다.


기본 구조

Recharts의 차트는 항상 같은 패턴을 따른다: 컨테이너 → 차트 → 구성 요소.

tsx
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts";

const data = [
  { name: "1월", sales: 400 },
  { name: "2월", sales: 300 },
  { name: "3월", sales: 600 },
  { name: "4월", sales: 200 },
  { name: "5월", sales: 500 },
];

function SalesChart() {
  return (
    <BarChart width={600} height={300} data={data}>
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="name" />
      <YAxis />
      <Tooltip />
      <Bar dataKey="sales" fill="#8884d8" />
    </BarChart>
  );
}

data 배열의 각 객체가 하나의 데이터 포인트다. dataKey로 어떤 필드를 축이나 막대에 매핑할지 지정한다. 이게 Recharts의 핵심 개념이다 — 데이터 필드 이름을 dataKey로 연결하는 것.


주요 차트 타입

BarChart (막대 차트)

카테고리별 값을 비교할 때 사용한다. 가장 기본적인 차트 타입이다.

tsx
<BarChart data={data}>
  <CartesianGrid vertical={false} />
  <XAxis dataKey="category" tickLine={false} axisLine={false} />
  <Tooltip />
  <Bar dataKey="value" fill="hsl(200, 70%, 68%)" radius={8} />
</BarChart>

radius로 막대 모서리를 둥글게 만들 수 있다. [8, 8, 0, 0]처럼 배열로 각 모서리를 개별 지정하는 것도 가능하다. CartesianGrid에서 vertical={false}를 주면 세로 격자선을 숨겨서 더 깔끔하게 보인다.

여러 데이터 시리즈를 겹치지 않게 보여주려면 <Bar>를 여러 개 넣으면 된다:

tsx
<BarChart data={data}>
  <XAxis dataKey="month" />
  <Bar dataKey="revenue" fill="#8884d8" />
  <Bar dataKey="cost" fill="#82ca9d" />
</BarChart>

PieChart (파이 차트)

전체에서 각 항목이 차지하는 비율을 보여줄 때 사용한다.

tsx
import { PieChart, Pie, Cell, LabelList } from "recharts";

const data = [
  { platform: "Blog A", count: 45 },
  { platform: "Blog B", count: 30 },
  { platform: "Blog C", count: 25 },
];

const COLORS = [
  "hsl(210, 60%, 70%)",
  "hsl(150, 60%, 70%)",
  "hsl(30, 60%, 70%)",
];

function PlatformChart() {
  return (
    <PieChart width={400} height={300}>
      <Pie data={data} dataKey="count" nameKey="platform">
        {data.map((entry, index) => (
          <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
        ))}
        <LabelList
          dataKey="platform"
          className="fill-background"
          stroke="none"
          fontSize={12}
        />
      </Pie>
      <Tooltip />
    </PieChart>
  );
}

파이 차트에서 각 조각의 색상을 다르게 하려면 <Cell> 컴포넌트를 사용한다. data.map()으로 각 데이터 항목마다 <Cell>을 렌더링하고, 색상 배열에서 인덱스로 색을 꺼내는 패턴이다.

<LabelList>는 파이 조각 위에 직접 라벨을 표시한다. dataKey로 어떤 필드를 라벨로 보여줄지 지정한다.

LineChart (선 차트)

시간에 따른 추이를 보여줄 때 적합하다.

tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts";

<LineChart data={data}>
  <CartesianGrid strokeDasharray="3 3" />
  <XAxis dataKey="date" />
  <YAxis />
  <Tooltip />
  <Line type="monotone" dataKey="visitors" stroke="#8884d8" strokeWidth={2} />
</LineChart>

type="monotone"은 데이터 포인트 사이를 부드러운 곡선으로 연결한다. "linear"로 바꾸면 직선으로 연결된다. 데이터의 성격에 따라 선택하면 된다 — 연속적인 데이터는 monotone, 불연속적인 데이터는 linear가 자연스럽다.

AreaChart (영역 차트)

LineChart와 비슷하지만 선 아래 영역을 색으로 채운다. 누적 데이터나 볼륨감을 강조할 때 유용하다.

tsx
import { AreaChart, Area, XAxis, YAxis, Tooltip } from "recharts";

<AreaChart data={data}>
  <XAxis dataKey="date" />
  <YAxis />
  <Tooltip />
  <Area
    type="monotone"
    dataKey="value"
    stroke="#8884d8"
    fill="#8884d8"
    fillOpacity={0.3}
  />
</AreaChart>

fillOpacity를 낮추면 반투명하게 되어서 여러 Area를 겹쳐도 아래 영역이 보인다.


ResponsiveContainer

차트에 고정 width/height를 주면 반응형이 안 된다. ResponsiveContainer로 감싸면 부모 요소의 크기에 맞게 자동으로 리사이즈된다.

tsx
import { ResponsiveContainer, BarChart, Bar } from "recharts";

<ResponsiveContainer width="100%" height={300}>
  <BarChart data={data}>
    <Bar dataKey="value" fill="#8884d8" />
  </BarChart>
</ResponsiveContainer>

width="100%"로 부모 너비를 꽉 채우고, height만 고정값으로 주는 패턴이 가장 일반적이다. 주의할 점은 ResponsiveContainer의 부모 요소에 명시적인 크기가 있어야 한다는 것이다. 부모가 크기를 가지지 않으면 차트가 렌더링되지 않거나 0 크기로 나온다.

또 다른 방법은 ResponsiveContainer 없이 CSS로 부모 크기를 제어하고, ResizeObserver를 직접 사용하는 것이다:

tsx
function ChartWrapper({ children }: { children: (width: number) => React.ReactNode }) {
  const [width, setWidth] = useState(0);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        setWidth(entry.contentRect.width);
      }
    });
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return <div ref={ref}>{width > 0 && children(width)}</div>;
}

// 사용
<ChartWrapper>
  {(width) => (
    <BarChart width={width} height={300} data={data}>
      <Bar dataKey="value" fill="#8884d8" />
    </BarChart>
  )}
</ChartWrapper>

이 방식은 차트 너비를 직접 제어할 수 있어서 너비에 따라 라벨을 잘라내는 등의 로직을 추가할 수 있다.


Tooltip 커스터마이징

기본 Tooltip도 쓸 만하지만, 커스텀 Tooltip으로 원하는 형태를 만들 수 있다.

tsx
function CustomTooltip({ active, payload, label }: any) {
  if (!active || !payload?.length) return null;

  return (
    <div className="bg-white p-3 rounded shadow border">
      <p className="font-bold">{label}</p>
      {payload.map((entry: any, index: number) => (
        <p key={index} style={{ color: entry.color }}>
          {entry.name}: {entry.value.toLocaleString()}
        </p>
      ))}
    </div>
  );
}

<Tooltip content={<CustomTooltip />} />

content prop에 커스텀 컴포넌트를 넘기면 된다. Recharts가 active, payload, label props를 자동으로 주입해준다.

  • active: 마우스가 차트 위에 있는지 여부
  • payload: 해당 데이터 포인트의 값들 배열
  • label: X축 값

<ChartTooltipContent> 같은 래퍼 컴포넌트를 만들어서 formatter prop으로 값 포맷팅을 위임하는 패턴도 자주 쓴다:

tsx
<ChartTooltip
  cursor={true}
  content={
    <ChartTooltipContent
      formatter={(value) => (
        <div className="flex justify-between gap-4">
          <span className="text-gray-400">조회수</span>
          <span>{value.toLocaleString()}</span>
        </div>
      )}
    />
  }
/>

XAxis 커스터마이징

X축 라벨이 길면 잘려서 안 보일 수 있다. tickFormatter로 라벨을 가공할 수 있다.

tsx
<XAxis
  dataKey="title"
  tickLine={false}      // 눈금선 숨김
  tickMargin={10}       // 라벨과 축 사이 간격
  axisLine={false}      // 축 선 숨김
  tickFormatter={(value) => {
    const maxLength = 10;
    return value.length > maxLength
      ? `${value.slice(0, maxLength - 3)}...`
      : value;
  }}
/>

tickLine={false}axisLine={false}로 눈금선과 축 선을 숨기면 더 미니멀한 디자인이 된다. 대부분의 대시보드 UI에서 이 옵션을 사용한다.

라벨이 정말 긴 경우 차트 너비에 따라 동적으로 잘라내는 게 더 좋다:

tsx
const truncateText = (text: string, chartWidth: number) => {
  const charWidth = 55; // 글자당 대략적인 픽셀 너비
  const maxChars = Math.floor(chartWidth / charWidth);
  return text.length > maxChars
    ? `${text.slice(0, Math.max(0, maxChars - 3))}...`
    : text;
};

ChartConfig 패턴

여러 차트에서 일관된 색상과 라벨을 사용하려면 설정 객체를 분리하는 게 좋다.

tsx
type ChartConfig = Record<string, {
  label: string;
  color: string;
}>;

const chartConfig: ChartConfig = {
  desktop: {
    label: "데스크톱",
    color: "hsl(200, 70%, 68%)",
  },
  mobile: {
    label: "모바일",
    color: "hsl(120, 70%, 68%)",
  },
};

이 설정 객체를 차트 래퍼 컴포넌트에 넘기면, 차트 내부에서 CSS 변수로 변환해서 사용할 수 있다:

tsx
function ChartContainer({
  config,
  children,
}: {
  config: ChartConfig;
  children: React.ReactNode;
}) {
  const style = Object.entries(config).reduce(
    (acc, [key, value]) => ({
      ...acc,
      [`--color-${key}`]: value.color,
    }),
    {} as React.CSSProperties
  );

  return <div style={style}>{children}</div>;
}

// 사용
<ChartContainer config={chartConfig}>
  <BarChart data={data}>
    <Bar dataKey="sales" fill="var(--color-desktop)" />
  </BarChart>
</ChartContainer>

CSS 변수를 사용하면 테마 변경이나 다크 모드 대응이 쉬워진다. fill 값을 하드코딩하는 대신 var(--color-desktop) 같은 변수를 사용하면 설정 객체만 바꿔서 전체 차트 색상을 일괄 변경할 수 있다.


성능 고려사항

Recharts는 SVG 기반이라 데이터 포인트가 많아지면 DOM 노드 수가 급격히 증가한다. 수천 개 이상의 데이터 포인트를 그려야 한다면 몇 가지를 고려해야 한다.

애니메이션 비활성화: 데이터가 많을 때 입장/퇴장 애니메이션은 성능을 크게 떨어뜨린다.

tsx
<Bar dataKey="value" isAnimationActive={false} />
<Line dataKey="value" isAnimationActive={false} />

데이터 샘플링: 1000개 포인트를 전부 그리는 대신, 화면 해상도에 맞게 간추린다. 600px 너비 차트에 1000개 포인트를 그려봤자 겹쳐서 의미가 없다.

tsx
function sampleData<T>(data: T[], maxPoints: number): T[] {
  if (data.length <= maxPoints) return data;
  const step = Math.ceil(data.length / maxPoints);
  return data.filter((_, i) => i % step === 0);
}

Canvas 대안 검토: 정말 대량의 데이터(수만 포인트 이상)를 실시간으로 그려야 한다면 SVG 기반 라이브러리의 한계다. 이 경우 Canvas 기반 라이브러리(Chart.js, uPlot)가 더 적합하다.


정리

  • D3의 계산 로직 위에 React 컴포넌트 조합 방식으로 차트를 구성하므로 선언적이고 직관적이다
  • ResponsiveContainer로 반응형 처리하고, ChartConfig 패턴으로 색상/라벨을 CSS 변수 기반으로 일관 관리한다
  • SVG 기반이라 대량 데이터에서는 애니메이션 비활성화와 샘플링이 필요하고, 수만 포인트 이상이면 Canvas 라이브러리를 검토한다

관련 문서