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라서 개별 요소 스타일링 어려움 |
| Victory | React 네이티브, 선언적 | 번들 크기 큼, 복잡한 차트에서 성능 이슈 |
| Nivo | D3 기반, 다양한 차트 타입 | 커스터마이징 자유도 낮음 |
| Recharts | D3 기반 SVG, React 컴포넌트 조합 | 매우 복잡한 시각화에는 한계 |
Recharts의 핵심 장점은 조합(Composition) 이다. 차트를 하나의 설정 객체로 정의하는 게 아니라, <BarChart>, <XAxis>, <Tooltip> 같은 컴포넌트를 조합해서 만든다. React 개발자에게 가장 직관적인 방식이다.
기본 구조
Recharts의 차트는 항상 같은 패턴을 따른다: 컨테이너 → 차트 → 구성 요소.
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 (막대 차트)
카테고리별 값을 비교할 때 사용한다. 가장 기본적인 차트 타입이다.
<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>를 여러 개 넣으면 된다:
<BarChart data={data}>
<XAxis dataKey="month" />
<Bar dataKey="revenue" fill="#8884d8" />
<Bar dataKey="cost" fill="#82ca9d" />
</BarChart>
PieChart (파이 차트)
전체에서 각 항목이 차지하는 비율을 보여줄 때 사용한다.
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 (선 차트)
시간에 따른 추이를 보여줄 때 적합하다.
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와 비슷하지만 선 아래 영역을 색으로 채운다. 누적 데이터나 볼륨감을 강조할 때 유용하다.
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로 감싸면 부모 요소의 크기에 맞게 자동으로 리사이즈된다.
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를 직접 사용하는 것이다:
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으로 원하는 형태를 만들 수 있다.
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으로 값 포맷팅을 위임하는 패턴도 자주 쓴다:
<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로 라벨을 가공할 수 있다.
<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에서 이 옵션을 사용한다.
라벨이 정말 긴 경우 차트 너비에 따라 동적으로 잘라내는 게 더 좋다:
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 패턴
여러 차트에서 일관된 색상과 라벨을 사용하려면 설정 객체를 분리하는 게 좋다.
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 변수로 변환해서 사용할 수 있다:
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 노드 수가 급격히 증가한다. 수천 개 이상의 데이터 포인트를 그려야 한다면 몇 가지를 고려해야 한다.
애니메이션 비활성화: 데이터가 많을 때 입장/퇴장 애니메이션은 성능을 크게 떨어뜨린다.
<Bar dataKey="value" isAnimationActive={false} />
<Line dataKey="value" isAnimationActive={false} />
데이터 샘플링: 1000개 포인트를 전부 그리는 대신, 화면 해상도에 맞게 간추린다. 600px 너비 차트에 1000개 포인트를 그려봤자 겹쳐서 의미가 없다.
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 라이브러리를 검토한다
관련 문서
- IntersectionObserver - 뷰포트 감지 (차트 lazy loading에 활용 가능)
- Context API - ChartConfig 패턴에서 Provider로 설정 공유
- 코드 스플리팅 - 차트 컴포넌트 지연 로딩