데이터를 처리한 앞 이야기는 아래에.

1. 백단부터 프론트까지 데이터 필터링 : 데이터 처리의 기준의 기준의 기준

 

[Service] 관제용 차트를 위한 험난한 여정_① : 데이터 처리의 기준의 기준의 기준

관제솔루션을 만들면서 가장 힘들었던 부분에 대한 리뷰.실무를 하지않으면 절대 느낄수 없는 부분이어서 삽질 히스토리를 남겨보려 한다.두달에 걸쳐 같은 화면을 5번씩 엎어가며 완성한 로직

ala-nueva.tistory.com

 

 


✅ 이슈 & 체크포인트

화면별로 평균 4개의 라인차트를 뿌려야 한다. 

폴링주기는 5~20초로 다양하며, 라인 차트 외에도 동시에 돌아가는 폴링이 약 10개정도 + 배경에 3D 객체와 애니메이션이 존재하므로 UI연출과 화면 부하를 동시에 생각해야 한다.

 

1. 폴링이 잦을수록 생각보다 화면 부하가 크다

차트는 <DOM>기반 차트와 <Canvas> 기반 차트 두가지로 나눠진다. ApexChart는 DOM기반으로, DOM은 점 마다 해당위치를 '새로'그려주므로 다 그리기도 전에 새로운 폴링이 5초만에 와서 '전체'데이터를 밀어넣는다? 차트 내부 스케줄러가 꼬이면서 화면이 깨진다.

  결국 값이 있는데도 선이 사라지거나 (툴팁에는 데이터 표기됨) 갑자기 중간중간 구멍이 뚫린다

 

2. 퍼블리싱의 색상 그라데이션 & Opacity도 부하가 크다

→ 만약 중간에 null이 들어와서 라인차트 중간중간이 끊어진다면, 차트는 끊어진위치 = 라인의 끝, 다시 그리는 위치 = 라인의 시작 이라고 판단하여 색상값을 다시 계산해야한다. 

구멍이 많을수록, 그리고 폴링 주기가 짧을수록 계산이 많이 들어가면서 차트 스케줄러가 꼬이고 화면이 깨진다.

→ 더불어 opacity 0~1 그라데이션도 더해져, 어디부터 정말 값이 없는건지 판단하기 어려웠으므로 이런 형태의 디자인은 지양하자.

 

3. 백그라운드에 3D애니메이션이 있다면 더욱 최적화에 신경쓰자

→ 보통은 관제 차트를 30개씩 돌려도 화면이 안깨지는데 왜 이 화면은 배경에 깔린 3D 애니메이션이 드득드득 끊기는지에 대한 pm의 컴플레인이 있었다... ㅎ.... 아무도 3D안해봤으면서 처음해보는 주니어한테 왜이러세요

  일단 차트 라이브러리를 canvas기반 라이브러리로 선정하지 않은 것, 그리고 3D는 마우스 조작 하나에도 전체 면을 다시 그려야하고, 움직이는 애니메이션은 더더욱 부하가 심하다.

  결론은 3D 쪽  최적화 + 데이터를 덜어내고 sliding처리를 빡세게 해서 '애니메이션이 밀리는 현상이 덜 보이도록' 처리했다. 화면이 화려할수록 부하가 심해지는걸 고려하고 화면을 설계하자. 예쁘고 화려하다고 다가 아니다🥲

 


✨ 트러블슈팅

2. 데이터 특성에 따른 퍼블리싱이 필요해 : 슬라이딩처리와 차트 특성에 의해 차트가 깨진다 

이 부분은 front (React, TypeScript, ApexChart) 에 관한 내용이다.

예쁘게 하겠다고 만들어준 디자인 & 퍼블리싱이 실 화면에 영향을 끼치는 문제가 있었다.

디버깅겸 모니터링은 꼭 필요한게, 폴링 몇주기가 돌아갈 동안은 차트가 잘 나온다. 그러다 값이 값자기 사라지거나 라인이 통채로 사라지는걸 보고 왜그러지..? 하다가 이런 이슈가 존재한다는걸 알게됐다.

 

1) 디자인 & 퍼블리싱 이슈

디자인팀에서는 '바람'에 대한 차트라고 라인차트의 처음과 끝의 투명도를 0~1로 조정 & 예쁨을 위한 그라데이션을 넣어주었는데요.

지피티로 만든 샘플!

 

체크포인트에서 언급했듯이, null값으로 인해 중간중간 라인이 끊기게 되면, 차트는 시작값과 끝값에 대한 설정을 일일히 다시 계산해야 한다. 5초마다 폴링중이라서 prev데이터가 아직 렌더링 중인데 뒷 데이터가 들어온다? → 차트 터짐의 원인이 되었다.

 

또한 시각적으로 보기에는 예쁘지만, opaity가 0~1로 중간중간 들어갈 경우, 정확하게 어떤 데이터부터 유실되었는지 알 수가 없었다. 그라데이션때문에 끊김 구간이 더 넓어보였기 때문!

 

▶ 결국 퍼블리싱에서 해당 설정을 다 덜어내고 단일색 및 opaicity도 1로 처리했다.

 

ApexChart에서는 이 구간을 죽여주면 된다 ▼

더보기

이것 또한 차트에 부하가 걸린다는걸 알고 나서 얼마나....허무했는지...

UI 개발엔 예쁨이 아니라 효율이 중요하다는걸 알게 된 사례였다.

// 차트 선 스타일
        stroke: {
            width: 2, // 선 두께
            curve: "smooth" as const, // 곡선 스타일
            lineCap: "round",
            // colors: ["url(#area-stroke-filter2)"], // stroke 그라데이션 방법2 : SVG 필터 추가 (현재는 사용X)
            // stroke에 그라데이션 방법1
            // fill: {
            //   type: "gradient",
            //   gradient: {
            //     type: "horizontal",
            //     colorStops: [
            //       [
            //         {
            //           offset: 0,
            //           color: "#FF8EB4",
            //           opacity: 1
            //         },
            //         {
            //           offset: 100,
            //           color: "#FDA993",
            //           opacity: 1
            //         },
            //       ]
            //     ]
            //   }
            // }
        },

 

2) DOM 기반 차트 라이브러리

솔루션에 무료 차트 라이브러리를 사용해야함 + 팀원들이 사용해봐서 잘 아는 라이브러리로 채택된게 이렇게 크리티컬한 문제가 될줄 몰랐다. 심지어 이 부분을 알게되어 얘기를 했음에도 '기존에 다른 프로젝트를 할때는 문제가 없었는데 왜 여기서만 문제냐' 라는 답변을 들어서, 내 후임에겐 이렇게 대답하지 말아야겠다고도 생각했다 😇

 

각설하고, 왜 이슈가 되었는지, 차트 특성을 정리해보자.

구분 DOM 기반 차트 (ex. ApexCharts) Canvas 기반 차트 (Chart.js 등)
렌더링 방식 데이터 변경 시 DOM 엘리먼트(svg path, text 등)를 새로 생성/갱신 → 브라우저의 레이아웃/리플로우 발생 데이터 변경 시 캔버스 픽셀 단위로 다시 그림 (DOM 영향 거의 없음)
업데이트 비용 각 데이터 포인트마다 DOM 노드 갱신 필요 → 노드 수가 많을수록 부하 증가 캔버스 한 장에 픽셀만 갱신 → 데이터 양이 많아도 상대적으로 일정
스케줄링 충돌 5초마다 들어오는 폴링 데이터가 누적되면 브라우저의 렌더링 큐와 JS 이벤트 루프가 경쟁 → 끊김/지연 발생 가능 그리기 자체가 빠르고 단일 draw call 중심 → 이벤트 루프와 충돌 적음
메모리 사용 많은 DOM 객체 유지 필요 → GC(가비지 컬렉션) 부담 증가 캔버스는 프레임버퍼만 사용 → 메모리 효율적
확장성 시각적 꾸밈, 툴팁, 인터랙션은 편리하나 대용량 시 급격히 느려짐 대규모 데이터 스트리밍 처리에 상대적으로 유리

👉 DOM 기반은 매번 새로 노드를 그려야 하고, 폴링 타이밍이 브라우저 스케줄링과 겹치면 부하가 눈에 띄게 생긴다.

👉 반면 Canvas는 한 장에만 그림을 덮어씌우므로 오버헤드가 훨씬 적다.

 

한 화면에 애니메이션, 3D, 각종 차트와 상태값 등 부하도 많은데 차트 스케줄까지 꼬이면 힘들어지는것은 당연지사.

이 부분은 '데이터 개수 축소 & 데이터 슬라이딩 처리' 로 해결을 보았다.▼

더보기

1. 데이터 축소

고객사의 처음 요청은 5초 버킷 30분 데이터였다. 그렇다면 5초마다 dot 360개 * 차트 5개 이렇게 되는데, 공간도 협소할 뿐더러 너무 촘촘하여 실질적으로 유효데이터를 보기가 어려울것 같고 부하 문제가 컸다.

▶ 5초 버킷 10분데이터로 조정하여 5초마다의 데이터 변화가 라인차트로 충분히 인지되도록 하였고 차트에 붓는 개수도 120개로 줄어들어 부하를 줄일 수 있었다.

 

2. 슬라이딩 처리

데이터 개수가 줄어도 슬라이딩 처리는 꼭 필요하다. 새로 DOM을 120개 그리는것과 기존의 118개를 활용 + 새로생긴 2개를 붙이면, 118개는 기존의 주소값을 활용해서 생성되기 때문에 차트 부하가 훨!씬! 줄어든다.

  이때, 1개 추가됐으니 1개 삭제, 이렇게 개수로만 하게되면 완벽히 10분만 반영된다는 설계가 깨질 수 있기 때문에 이전 글에서 말한것과 같이 시간테이블생성 & 시간 기준으로 데이터를 필터링했다. 아래 소스를 보면 120개 통채로 차트에 반영하는건 첫호출때 뿐이고, 그 이후로는 잘라붙이기 위해 데이터를 필터링한다.

 

if (response?.data) {
        const result = response?.data[0];

        const newDev = (result.devActivePower ?? []) as TypeClctBasic[];

        // 첫 호출
        if (!clctDateRef.current) {
            unstable_runWithPriority(LowPriority, () => {
                setDevActivePower(newDev);
            });
            clctDateRef.current = newRack[newRack.length - 1].lastClctDate;
        } else {
            // 이후 호출 (슬라이딩 + 중복 제거)
            if (newDev.length > 0) {
                const latestTime = newDev[newDev.length - 1]?.lastClctDate;
                const holdData = newDev[0].clctDate;
                clctDateRef.current = latestTime;

                unstable_runWithPriority(LowPriority, () => {
                    setDevActivePower(prev => {
                        const prevLastClctDate = prev[prev.length - 1].lastClctDate;
                        const filtered = prev.filter(item => holdData <= item.clctDate && item.clctDate < prevLastClctDate);
                        const existing = new Set(filtered.map(item => item.clctDate));
                        const uniqueNew = newDev.filter((item) => !existing.has(item.clctDate) && prevLastClctDate <= item.clctDate);
                        return [...filtered, ...uniqueNew];
                    });
                });
            }
        }
    }

 

 

3) React우선순위 스케줄러 조정

'차트 30개씩 띄워도 문제가 없는데 애니메이션있다고 이렇게나 버벅거리는게 맞냐? '라는 피드백을 듣고 1), 2) 도입 전에 최초로 시도했던 세팅이다. 그때는 원인을 전혀 몰랐기에 스케줄러로 애니메이션이 영향을 받는걸 줄여보고자 세팅했다.

 

JS는 싱글스레드다 ▼

더보기

1. JS는 싱글스레드 → 한 번에 한 작업만 실행

  • 자바스크립트는 기본적으로 하나의 메인 스레드에서 실행된다.
  • 이 스레드는 **이벤트 루프(Event Loop)**를 돌면서
    • 콜백 큐 (setTimeout, setInterval, fetch 응답 등)
    • 렌더링 작업 (DOM 업데이트, CSS 애니메이션)
    • React 상태 업데이트
      전부 같은 줄에서 처리해야 함.

2. 충돌/꼬임이 생기는 이유

  • 5초마다 오는 폴링(setInterval → setState) 도 같은 메인 스레드에서 실행돼야 함.
  • 동시에 ApexCharts 같은 DOM 기반 애니메이션도 메인 스레드에서 돌고 있음.
    • (Three.js 등 3D캔버스는 로직만 JS로 처리하고 렌더링은 GPU처리되므로 반쯤 걸쳐져있는 상태)
    • ApexCharts 애니메이션: SVG path나 DOM 요소를 부드럽게 갱신 → 이것도 메인 스레드 점유.→ 둘 다 렌더링 관련 무거운 작업이라 충돌 확률이 높음.
  • 그러면 이벤트 루프에 두 작업이 한꺼번에 줄을 서게 되고, 누가 먼저 처리될지 스케줄러가 조율해야 함.
    → 이 과정에서 "배경에 있는 3D 애니메이션이 끊기는 느낌"이 나올 수 있음.

3. unstable_runWithPriority의 역할

  • JS가 싱글스레드라서 “병렬”로 돌릴 수는 없음.
  • 대신 React가 “우선순위 큐”를 만들어서 중요한 작업(클릭, 애니메이션 등) 먼저 처리하고, 덜 중요한 작업(폴링 데이터 갱신)은 뒤로 미룸.
  • 즉, 싱글스레드 제약 때문에 꼬이지 않게 하기 위한 우선순위 제어 장치가 바로 unstable_runWithPriority.
unstable_runWithPriority(LowPriority, () => {
    setDeviceActivePower(filteredData);
    });
});

1. unstable_runWithPriority

  • React 내부에서 제공하는 실험적 API (scheduler 패키지에 있음).
  • 어떤 작업을 “이건 중요하다 / 덜 중요하다”라고 우선순위를 지정해 실행할 수 있음.
  • 브라우저 이벤트 루프에 스케줄러 레이어를 두는 개념.

2. LowPriority

  • React 스케줄러의 우선순위 값 중 하나. (다른 것들: ImmediatePriority, UserBlockingPriority, NormalPriority, LowPriority, IdlePriority …)
  • 낮은 우선순위 작업으로 밀어놓는 것.
    즉, “UI 애니메이션이나 사용자 인터랙션을 먼저 처리하고, 이 작업은 그 다음에 해도 된다”는 의미.

3. setDeviceActivePower(filteredData);

  • 상태 갱신 자체는 React가 해야 하는 일이니까 반드시 렌더링 사이클을 거쳐야 함.
  • 하지만 LowPriority 안에서 실행되면:
    • React는 우선순위가 높은 작업(UI 인터랙션, 애니메이션 프레임 등)을 먼저 처리.
    • 남는 시간에 이 상태 업데이트를 적용.

4. 왜 쓰였을까?

  • 차트가 5초마다 폴링 → setState 발생 → DOM 기반 차트(ApexCharts)가 리렌더링.
  • 동시에 ApexCharts 내부 애니메이션도 동작 중이라면 React의 상태 업데이트 vs Chart 애니메이션이 스케줄링 레벨에서 충돌.
  • 이때, LowPriority로 감싸면 React가 먼저 애니메이션·유저 인터랙션을 소화하고, 뒤늦게 setState 적용 → 렌더링 끊김 최소화.

 

다만 이 방법은 버벅거림이 '확'줄어들어들을 정도로 엄청난 효과를 보진 못했다.

1), 2) 에서 해결한 것들이 오히려 UI부하는 훨씬 줄어들었다.

▶  스케줄러 조정은 최적화의 한 방법이라고 알아두면 좋을것 같다.

 


💡 이 작업을 하면서 얻은 것

기술적 측면

  • 개발을 처음 배울때에는 마냥 '데이터 쿼리에서 UI에 표현'정도로만 생각했는데, 생각보다 세심하게 처리해야 할 부분이 많다는 것을 알게 되었다. 하물며 그 부분이 디자인이나 퍼블리싱 영역이라 할지라도.
  • 차트타입이 Dom/Canvas로 달라진다는것도 이번에 알게되었다. 도구를 사용할때 '제대로 알고 골라야 한다'는걸 알게되었다. 결과물의 퀄리티에도 많은 영향을 미치는것 같다. (물론 처음부터 Canvas타입 차트로 했다면 이런 부분들을 digging해보지 못했을것이긴 하다만...혼자 해결하기가 너무 힘들었다)
  • 데이터 필터링 및 슬라이딩 처리가 중요함은 알고 있었지만, 화면에 부하를 좌우할정도로 크게 문제를 일으킬줄 몰랐다. 5초마다 차트에 360개를 다시 그리는게 문제가 있겠어? 라고 안이하게 생각했는데 개발하며 UI 상태로 직접 보고 나니 최적화의 중요성을 알게되었다.

커뮤니케이션 측면

  • 주니어 입장에서 '예전 프로젝트에선 문제가 없는데 이 case는 왜그래? 네가 잘못했나보지.', '나는 잘 되는데?' 라는 윽박섞인 피드백은 아무 쓸모가 없다는걸 알게되었다. 내가 주니어를 대하게 된다면, (모든 기술을 알지는 못하겠지만) 내가 모르는 이유가 있을 수 있다는걸 생각하고 피드백을 줄 수 있어야겠다고 생각했다. 어떤 주니어도 '이유를 알고 있으면 그렇게 개발하지 않을 것'이다.
  •  이의를 제기해도 받아들일 생각이 없는 상대를 설득하는건 굉장히 어려워서, 실제로 협상론에 관한 책을 읽기 시작했다. 또한 기술토론이나 코드리뷰가 왜 중요한 문화인지도 새삼 깨닫게 됐다.

+ Recent posts