Skip to content

Conversation

@yuminnnnni
Copy link
Member

Related issue

#834

Result

🔍 기존 코드의 문제점 분석

1. 전체 DOM 재생성 문제

// 기존 코드 (AuthorBarChart_old.tsx)
useEffect(() => {
  const svg = d3.select(svgRef.current);
  svg.selectAll("*").remove();  // 매번 모든 SVG 요소 삭제
  
  // 축 그룹 새로 생성
  const xAxisGroup = svg.append("g").attr("class", "x-axis");
  const yAxisGroup = svg.append("g").attr("class", "y-axis");
  const barGroup = svg.append("g").attr("class", "bar-container");
  
  // 바 요소들 새로 생성
  barGroup.selectAll("rect")
    .data(data)
    .join("rect");  // 키 함수 없음
}, [data, metric]);

문제점:

  • 전체 DOM 삭제: svg.selectAll("*").remove()로 모든 요소 삭제
  • 완전 재생성: 축, 바, 레이블 등 모든 요소를 매번 새로 생성
  • 비효율적 렌더링: 변경되지 않은 요소도 재생성
  • 성능 병목: DOM 조작 비용이 데이터 크기에 비례하여 급격히 증가

2. 비효율적인 데이터 바인딩

// 기존 코드의 데이터 바인딩
barGroup.selectAll("rect")
  .data(data)
  .join(
    (enter) => enter.append("rect"),  // 키 함수 없음
    (update) => update,
    (exit) => exit.remove()
  );

문제점:

  • 키 함수 부재: 데이터 변경 시 요소 매칭이 불안정
  • 예측 불가능한 DOM 조작: 같은 데이터라도 다른 DOM 요소에 바인딩될 수 있음
  • 애니메이션 끊김: 요소 식별이 불안정하여 부드러운 전환 어려움

3. 개별적 이미지 DOM 조작

// 기존 코드의 이미지 처리
barElements.forEach(async (barElement, i) => {
  const profileImgSrc = await getAuthorProfileImgSrc(data[i].name);
  bar.append("image")  // 각 이미지마다 개별적으로 DOM 조작
    .attr("xlink:href", profileImgSrc);
});

문제점:

  • 개별 DOM 조작: 각 이미지 로딩 완료 시마다 DOM 업데이트
  • 에러 처리 부재: 개별 이미지 로딩 실패 시 처리 로직 없음
  • 일관성 부족: 이미지별로 다른 타이밍에 표시되어 시각적 일관성 저하

⚡ 최적화 구현 내용

1. 선택적 DOM 업데이트 시스템

// 개선된 코드 (AuthorBarChart.tsx)
useEffect(() => {
  const svg = d3.select(svgRef.current);
  // svg.selectAll("*").remove(); // 이 줄 제거!
  
  // 기존 요소 재사용 또는 필요시에만 생성
  const xAxisGroup = svg
    .selectAll(".x-axis")
    .data([null])
    .join("g")
    .attr("class", "author-bar-chart__axis x-axis");
    
  const yAxisGroup = svg
    .selectAll(".y-axis") 
    .data([null])
    .join("g")
    .attr("class", "author-bar-chart__axis y-axis");
    
  const barGroup = svg
    .selectAll(".author-bar-chart__container")
    .data([null])
    .join("g")
    .attr("class", "author-bar-chart__container");
}, [data, metric]);

핵심 개선사항:

  • DOM 재사용: 기존 축 그룹과 컨테이너 요소 재사용
  • 조건부 생성: 없는 요소만 새로 생성 (join 패턴 활용)
  • 점진적 업데이트: 전체 삭제 없이 필요한 부분만 수정
  • 성능 향상: DOM 조작 비용을 O(n)에서 O(k)로 감소 (k는 변경된 요소 수)

2. 안정적인 데이터 바인딩 시스템

// 개선된 데이터 바인딩
const bars = barGroup
  .selectAll(".author-bar-chart__bar")
  .data(data, (d: any) => d?.name || "")  // 키 함수로 안정적 매칭
  .join(
    // Enter: 새로운 데이터에 대한 요소 생성
    (enter) => {
      const barElement = enter.append("g").attr("class", "author-bar-chart__bar");
      barElement.append("rect")
        .attr("width", xScale.bandwidth())
        .attr("height", 0)
        .attr("x", (d: any) => xScale(d?.name) || 0)
        .attr("y", DIMENSIONS.height);
      return barElement;
    },
    // Update: 기존 요소 업데이트
    (update) => {
      update.select("rect")
        .on("mouseover", handleMouseOver)
        .on("click", handleClickBar);
      return update;
    },
    // Exit: 제거될 요소 애니메이션 후 삭제
    (exit) => {
      exit.select("rect")
        .transition()
        .duration(250)
        .attr("height", 0)
        .attr("y", DIMENSIONS.height);
      return exit.transition().duration(250).remove();
    }
  );

핵심 개선사항:

  • 키 함수 활용: d?.name을 키로 사용하여 요소 식별
  • 예측 가능한 매칭: 동일한 저자는 항상 동일한 DOM 요소에 바인딩
  • 부드러운 애니메이션: 안정적인 요소 매칭으로 자연스러운 전환
  • 최소 DOM 조작: 실제로 변경된 데이터에 대해서만 DOM 업데이트

3. 일괄 이미지 처리 및 안정성 강화 시스템

// 개선된 이미지 로딩
// 기존 이미지 제거
bars.selectAll("image.author-bar-chart__profile-image").remove();

// 모든 이미지를 병렬로 로딩 (기존과 동일하지만 에러 처리 강화)
const imagePromises = data.map(async (d: AuthorDataType) => {
  if (!d?.name) return null;
  
  try {
    const profileImgSrc = await getAuthorProfileImgSrc(d.name);
    return { name: d.name, src: profileImgSrc };
  } catch (error) {
    console.warn(`Failed to load profile image for ${d.name}:`, error);
    return null;
  }
});

// 모든 이미지 로딩 완료 후 한번에 DOM에 추가
Promise.all(imagePromises).then((imageResults) => {
  const validImages = imageResults.filter(result => result !== null);
  
  bars.selectAll("image.author-bar-chart__profile-image")
    .data(validImages, (d: any) => d?.name || "")
    .enter()
    .append("image")
    .attr("class", "author-bar-chart__profile-image")
    .attr("xlink:href", (d: any) => d?.src ?? "")
    .style("opacity", 0)
    .transition()
    .duration(300)
    .style("opacity", 1);
});

핵심 개선사항:

  • 일괄 DOM 조작: 모든 이미지 로딩 완료 후 한번에 DOM 업데이트
  • 강화된 에러 처리: 개별 이미지 로딩 실패 시에도 전체 프로세스 중단 없음
  • 시각적 일관성: 모든 이미지가 동시에 페이드인 애니메이션으로 표시
  • 안정성 향상: null 체크 및 예외 상황 처리 강화

📊 성능 테스트 결과

실제 브라우저 환경 측정 결과

데이터 크기별 성능 개선 현황

데이터 크기 기존 코드 (ms) 개선 코드 (ms) 개선율 시간 단축
10개 7.90 7.46 +5.6% 0.44ms
50개 10.49 6.72 +35.9% 3.77ms
100개 10.11 8.76 +13.4% 1.35ms
500개 18.93 11.49 +39.3% 7.44ms
1000개 38.27 24.63 +35.6% 13.64ms

핵심 성과 요약

전체 성능 개선: 평균 25.96%

  • 모든 데이터 크기에서 일관된 성능 향상 확인
  • 대용량 데이터에서 특히 뛰어난 성능 (35-39% 향상)

📈 성능 개선 패턴 분석

성능 개선 곡선:
10개:   5.6%  ▁
50개:  35.9%  ████████
100개: 13.4%  ███
500개: 39.3%  █████████ ⭐ (최고점)
1000개: 35.6% ████████

🎯 데이터 크기별 상세 분석

1. 소규모 데이터 (10개)

  • 평균 시간: 7.90ms → 7.46ms (+5.6%)
  • 최소 시간: 4.10ms → 2.00ms (51% 향상)
  • 최대 시간: 16.20ms → 16.10ms
  • 특징: 작은 개선이지만 최소 시간에서 큰 향상

2. 중간 규모 데이터 (50개)

  • 평균 시간: 10.49ms → 6.72ms (+35.9%)
  • 최소 시간: 6.10ms → 3.50ms
  • 최대 시간: 16.60ms → 12.80ms
  • 특징: 가장 안정적이고 큰 성능 향상

3. 중간 규모 데이터 (100개)

  • 평균 시간: 10.11ms → 8.76ms (+13.4%)
  • 최소 시간: 6.50ms → 4.60ms
  • 최대 시간: 14.30ms → 15.00ms
  • 특징: 안정적인 개선, 일부 최대값에서 변동

4. 대규모 데이터 (500개)

  • 평균 시간: 18.93ms → 11.49ms (+39.3%) ⭐
  • 최소 시간: 17.00ms → 10.70ms
  • 최대 시간: 22.20ms → 12.30ms
  • 특징: 최고 성능 개선율 달성

5. 대용량 데이터 (1000개)

  • 평균 시간: 38.27ms → 24.63ms (+35.6%)
  • 최소 시간: 34.20ms → 21.20ms
  • 최대 시간: 43.40ms → 32.70ms
  • 특징: 대용량에서도 안정적인 고성능 유지

실제 측정값 상세 데이터

50개 데이터 측정값

기존 코드: [6.3, 8.2, 8.7, 13.9, 16.6, 13.2, 16.4, 6.1, 8.2, 7.3]
개선 코드: [3.5, 5.0, 6.8, 6.3, 12.8, 6.9, 6.5, 6.8, 7.1, 5.5]
평균 개선: 10.49ms → 6.72ms (+35.9%)

1000개 데이터 측정값

기존 코드: [42.2, 39.2, 43.4, 38.9, 39.5, 35.2, 38.0, 35.0, 37.1, 34.2]
개선 코드: [21.2, 22.5, 30.7, 32.7, 23.6, 22.2, 21.7, 26.3, 21.6, 23.8]
평균 개선: 38.27ms → 24.63ms (+35.6%)

성능 개선 시각화

데이터 크기별 렌더링 시간 비교:

10개:   7.90ms ████████ → 7.46ms ███████▌ (+5.6%)
50개:  10.49ms ██████████▌ → 6.72ms ██████▌ (+35.9%) ⭐
100개: 10.11ms ██████████ → 8.76ms ████████▌ (+13.4%)
500개: 18.93ms ███████████████████ → 11.49ms ███████████▌ (+39.3%) ⭐
1000개: 38.27ms ████████████████████████████████████████ → 24.63ms ████████████████████████▌ (+35.6%)

범례: ▌ = 1ms

🎯 성능 개선 효과 분석

1. 사용자 경험 개선

  • 체감 성능 향상: 대용량 데이터에서 13.64ms 단축으로 더 빠른 반응성
  • 일관된 성능: 모든 데이터 크기에서 안정적인 성능 향상 확인
  • 부드러운 애니메이션: 최적화된 DOM 조작으로 더 나은 사용자 경험

2. 시스템 리소스 효율성

  • DOM 조작 최적화: 불필요한 요소 재생성 제거로 CPU 부하 경감
  • 렌더링 효율성: 선택적 업데이트로 브라우저 렌더링 엔진 부담 감소

3. 확장성 및 안정성

  • 대용량 데이터 지원: 1000개 데이터에서도 25ms 이내 렌더링
  • 확장성 확보: 데이터 크기 증가에 따른 안정적인 성능 패턴
  • 코드 안정성: 키 함수 기반 데이터 바인딩으로 예측 가능한 DOM 조작

📝 결론

AuthorBarChart 컴포넌트의 성능 최적화 작업을 통해 실제 브라우저 환경에서 평균 25.96%의 렌더링 성능 향상을 달성했습니다.

🎯 주요 성과

✅ 성능 개선 검증

  • 전체 평균 개선: 25.96% 성능 향상
  • 최대 개선율: 39.3% (500개 데이터)
  • 대용량 데이터 특화: 35-39% 향상으로 확장성 확보
  • 실제 환경 검증: 브라우저 DOM 렌더링 환경에서 측정

📊 데이터 크기별 성능 향상

소규모 (10개):    +5.6%  - 기본 성능 개선
중간규모 (50개):  +35.9% - 최적 성능 구간
중간규모 (100개): +13.4% - 안정적 개선
대규모 (500개):   +39.3% - 최고 성능 달성 ⭐
대용량 (1000개):  +35.6% - 확장성 확보

🔧 핵심 기술적 개선 사항

1. DOM 조작 최적화

// Before: svg.selectAll("*").remove() - 전체 재생성
// After:  선택적 업데이트 - 필요한 부분만 수정
  • 시간 복잡도: O(2n) → O(k) (k는 변경된 요소 수)
  • 실제 효과: 대용량 데이터에서 13.64ms 단축

2. 안정적인 데이터 바인딩

// Before: .data(data) - 키 함수 없음
// After:  .data(data, d => d.name) - 안정적 매칭
  • 예측 가능한 DOM 조작: 동일 데이터 → 동일 요소
  • 부드러운 애니메이션: 요소 식별 안정화

3. 일괄 이미지 처리 및 안정성 강화

// Before: forEach + async - 개별 DOM 조작
// After:  Promise.all - 일괄 DOM 조작
  • 시각적 일관성: 모든 이미지 동시 표시로 사용자 경험 개선
  • 에러 처리 강화: 개별 이미지 실패 시에도 안정적 동작

기존 버전

authorbarchart_.2.mov

개선된 버전

authorbarchart_.2.mov

Discussion

any 타입 사용

  • 현재는 D3.js의 복잡한 타입 시스템으로 인해 any 타입을 사용하고 있습니다.
  • 런타임 안전성은 optional chaining(d?.name)과 기본값 처리로 어느정도 보장하였으나, 만약 이 부분에서 개선이 필요하다면 추후 새로운 이슈로 발행하여 해결하겠습니다!

Copy link
Contributor

@hyemimi hyemimi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM👍👍 너무 멋진 PR인 것 같아요! PR 상세가 엄청 자세하고 전 후 비교가 잘 되어 있어서 이해하기 수월했습니다. d3 활용할 때 참고해보겠습니다ㅎㅎ

Copy link
Contributor

@chae-dahee chae-dahee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터가 많아질 수록 확실히 성능 향상이 눈에 보이긴 하네요 👍 성능 측정 부분 그래프 이미지로 추출해서 보고서에 활용하면 좋을 것 같습니다!
PR 문서도 꼼꼼히 작성해주셔서 좋았습니다~ 핵심 기술적 개선 사항으로 요약해주셔서 감사합니다~
LGTM!!! 고생하셨습니다!!

.data(data)
const bars = barGroup
.selectAll(".author-bar-chart__bar")
.data(data, (d: any) => d?.name || "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명시적인 데이터 키를 이름으로 사용한 것 좋습니다 👍
다만 문서에 말씀하신대로 타입을 any 가 아니고 명확히 정의하면 좋을 것 같습니다. any 가 여러곳에서 반복되는 만큼 제네릭 타입 정의 활용해도 좋고요~!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

키값 넣으신거 좋네요!!!

Comment on lines +237 to +239
bars
.selectAll("image.author-bar-chart__profile-image")
.data(validImages, (d: any) => d?.name || "")
Copy link
Contributor

@chae-dahee chae-dahee Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지도 join() 활용하면 좋을 것 같아요!
이 부분 이슈 생성해서 다음 작업으로 진행하시는거 어떠신가요?ㅎㅎ

(질문) Promise.all으로 일괄 DOM 조작으로 개선하셔서 join 사용안하신건가 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 처음에는 이미지 쪽에도 join을 적용해봤었는데, 이미지 로딩의 비동기적 완료 시점이랑 join의 데이터 바인딩 시점이 서로 잘 맞지 않아서 화면상 이미지 레이아웃이 좀 이상하게 나오더라구요... 😭 그래서 이미지 부분은 기존 방식을 유지하고 바 차트쪽만 join을 적용했습니다!

Copy link
Contributor

@ytaek ytaek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋진 PR 입니당!!!! LGGGGGGTM!

// Axis
const xAxis = d3.axisBottom(xScale).ticks(0).tickSizeInner(0).tickSizeOuter(0);
xAxisGroup.call(xAxis);
xAxisGroup.call(xAxis as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 type을 axis쪽에서 먹여서 다음 PR에서 진행하시면 되겠습니다!!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 알겠습니다!!

const xAxisGroup = svg
.append("g")
.selectAll(".x-axis")
.data([null])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(궁금) null 값이 하나만 들어간 배열을 바인딩 하신 이유가 따로 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d3는 데이터 기반으로 동작하기 때문에 .data() 메서드는 항상 배열을 요구합니다.
여기서는 x축, y축, 컨테이너 그룹처럼 단일 요소만 필요한 경우이기 때문에 길이가 1인 배열 [null]을 전달했는데, 사실 null 값 자체는 큰 의미가 없었습니다 😅

null을 넣는 게 코드 가독성이 안 좋아보인다면 다른 요소로 바꿔보겠습니다..!

.data(data)
const bars = barGroup
.selectAll(".author-bar-chart__bar")
.data(data, (d: any) => d?.name || "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

키값 넣으신거 좋네요!!!

Comment on lines +191 to +197
(update) => {
update
.select("rect")
.on("mouseover", handleMouseOver)
.on("mousemove", handleMouseMove)
.on("mouseout", handleMouseOut)
.on("click", handleClickBar);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d3 진성 유저이십니다 👍👍👍👍👍

@yuminnnnni yuminnnnni merged commit 1df2a07 into githru:main Oct 12, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants