HOC로 렌더링을 차단하고 성능 향상

Category
Engineering Deep Dive
Status
Published
Tags
Kiosk
Stack Navigation
Stackflow
TTS
Trouble Shooting
Description
Published
Slug

Stackflow 라이브러리, HOC로 렌더링 차단하고 성능 향상

→ 라이브러리 내부 동작 분석해 TTS 버그 해결
 

1. 스택이 쌓일수록 시스템이 멈추는 문제

모바일 앱처럼 부드러운 화면 전환 UX를 위해 오픈소스 라이브러리 stackflow를 도입했습니다.
하지만 사용자가 여러 페이지를 오가며 화면 스택(Stack)이 10개 이상 쌓이자, 성능 문제가 발생했습니다.
  • UI 반응성 저하: 화면 전환이 눈에 띄게 버벅이고 UI 전체가 느려졌습니다.
  • 저사양 하드웨어 한계: 특히 키오스크 하드웨어에서 이 문제는 더욱 심각했습니다.
  • 버그 발생: 가장 심각한 문제로, 사용자가 보지 않는 비활성 화면 2~3개의 음성 안내(TTS)가 동시에 중복으로 재생되는 버그가 발생했습니다. 사용자가 뒤로가기를 누르면, 현재 화면의 TTS와 이전 화면의 TTS가 겹쳐서 들리는 혼란스러운 상황이 연출되었습니다.
 

2. 원인 분석: "Unmount"가 아니라 "display: none"

원인을 찾기 위해, Stackflow 라이브러리의 내부 동작 방식을 분석했습니다. 문제의 핵심은 Stackflow의 화면 관리 전략에 있었습니다.
Stackflow는 화면 전환 시 이전 화면(Activity)을 메모리에서 제거(unmount)하는 것이 아니라, 단순히 DOM에서 숨기고(display: none) 컴포넌트의 상태(State)는 그대로 유지했습니다.
이 방식은 사용자가 뒤로가기 시 이전에 보던 화면 상태를 그대로 복원하여 빠른 재전환을 가능하게 하는 장점이 있지만, 저희 시스템에서는 치명적인 부작용을 낳았습니다.
비활성 상태인 여러 화면이 백그라운드에서 useEffect, setInterval, setTimeout 같은 로직을 계속 실행하고 있었던 것입니다. 저희 시스템의 TTS API 호출, 데이터 폴링 로직 등이 멈추지 않고 중복 실행되며 리소스를 점유하고 버그를 일으켰습니다.
 

3. 해결 전략: HOC를 이용한 '렌더링 가드' 설계

문제의 근본 원인은 '비활성 컴포넌트가 렌더링되고 로직을 실행하는 것'이었습니다. 따라서 화면이 비활성(isActive: false) 상태일 때는 렌더링 자체를 중단시켜(return null) 불필요한 리소스 점유를 원천적으로 차단하는 것이 가장 효율적인 해결책이라 판단했습니다.
 
물론, 각 화면 컴포넌트 내부에서 useActivity 훅을 사용해 수동으로 isActive 상태를 체크하고 null을 반환할 수도 있었습니다. 하지만 이 방식은 모든 화면에 중복 코드를 작성해야 하고, 향후 유지보수 시 실수를 유발할 가능성이 컸습니다.
저는 이 로직을 프로젝트 내 모든 화면에 코드 중복 없이, 일관되게 적용하기 위해 고차 컴포넌트(Higher-Order Component, HOC)인 withActiveGuard를 설계했습니다.
 

4. 핵심 구현

withActiveGuard HOC는 Stackflow가 제공하는 useActivity 훅을 사용하여 현재 화면의 활성 상태(isActive)를 실시간으로 추적합니다.

4-1. 첫 번째 시도 (Naive Implementation)

가장 단순한 HOC 구현은 다음과 같았습니다.
// 첫 번째 시도: 단순한 HOC function withActiveGuard<T extends object>(WrappedActivity: ActivityComponentType<T>): ActivityComponentType<T> { const HOC: ActivityComponentType<T> = (props) => { const activity = useActivity(); // (1) 비활성 상태면 렌더링 중단 if (!activity.isActive) { return null; } // (2) 활성 상태면 렌더링 return <WrappedActivity {...props} />; }; return HOC; }
이 코드는 백그라운드 TTS 중복 재생 버그와 리소스 점유 문제를 즉시 해결했습니다.
비활성 화면이 null을 반환하면서 useEffect 등이 모두 중단되었기 때문입니다.
 

4-2. 새로운 문제 발견: 깨지는 애니메이션

하지만 새로운 문제가 발생했습니다. 화면을 뒤로가기 할 때, 사라지는 애니메이션이 작동하지 않고 화면이 즉시 바뀌었습니다.
원인은 !activity.isActive 조건에 있었습니다. 뒤로가기 애니메이션이 시작되는 순간, 해당 화면은 isActive: false 상태가 됩니다. 저희가 만든 HOC는 이 상태를 감지하고 즉시 null을 반환했고, React는 애니메이션을 재생할 컴포넌트를 DOM에서 즉시 제거해버린 것입니다.
 

4-3. transitionState로 엣지 케이스 제어

단순히 null을 반환한 것이 아니라, 애니메이션이 깨지는 엣지 케이스까지 고려해야 했습니다.
Stackflow의 useActivity 훅이 isActive 외에 transitionState라는 값을 제공하는 것을 발견했습니다. 이 상태는 enter-active, exit-active 등 화면 전환 애니메이션의 현재 상태를 알려주었습니다.
저는 이 transitionState를 활용해 HOC의 조건을 보강했습니다.
"화면이 비활성(isActive: false) 상태이더라도, 만약 '사라지는 애니메이션이 진행 중'(transitionState === 'exit-active')이라면, 그 애니메이션이 끝날 때까지는 null을 반환하지 말고 컴포넌트를 렌더링"
이 로직을 적용한 최종 코드는 다음과 같습니다.
withActiveGuard.tsx (최종 HOC 전체 코드)
import React from 'react'; import { ActivityComponentType, useActivity } from '@stackflow/react'; /** * withActiveGuard * - 전달된 Activity 컴포넌트를 감싸서, * - Stackflow 상에서 isActive=false(가려진 상태)면 null을 리턴(렌더링X)하여 * - 중복 음성 안내/로직 실행을 막음 */ // 제네릭 T extends object: 일반적으로 Activity 컴포넌트에 전달될 props 타입 export function withActiveGuard<T extends object>(WrappedActivity: ActivityComponentType<T>): ActivityComponentType<T> { // HOC const HOC: ActivityComponentType<T> = (props) => { // (1) Activity 정보 획득 const activity = useActivity(); // (2) '비활성'이면서 '사라지는 애니메이션 중'이 아닐 때만 렌더링 중단 if (!activity.isActive && activity.transitionState !== 'exit-active') { // 렌더링을 중단(unmount)하여 useEffect 등의 로직 실행을 방지 return null; } // (3) 활성 상태이거나, 사라지는 애니메이션 중일 때는 원본 컴포넌트 렌더 return <WrappedActivity {...props} />; }; // (4) React 개발자 도구에서 디버깅 용이성을 위한 displayName 설정 // HOC로 감싸진 컴포넌트가 'Anonymous'가 아닌 'withActiveGuard(MyComponent)'로 표시됨 HOC.displayName = `withActiveGuard(${WrappedActivity.displayName || WrappedActivity.name || 'AnonymousActivity'})`; return HOC; }
이후 stackflow.ts 설정 파일에서 모든 activitieswithActiveGuard로 감싸, 프로젝트 전체에 이 최적화 로직을 일괄 적용했습니다.
 
 

5. 결과

  • 성능 확보: 비활성 화면의 불필요한 렌더링과 로직 실행을 원천 차단하여, 화면 스택이 10개 이상 쌓여도 일정한 UI 반응 속도를 확보했습니다.
  • 버그 해결: 가장 치명적이었던 백그라운드 음성 안내(TTS) 중복 재생 버그를 근본적으로 해결했습니다.
  • UX 개선: transitionState를 활용한 엣지 케이스 처리로, 성능 최적화와 동시에 부드러운 화면 전환 애니메이션(UX)까지 보존할 수 있었습니다.
 
  • 이를 통해 라이브러리를 적용할 때 단순히 API를 사용하는 것을 넘어,
    • 해당 라이브러리의 성능 특성(특히 메모리 및 렌더링 관리 방식)을 면밀히 검토하고, 프로젝트의 특수한 환경(e.g., 키오스크, TTS)에 맞게 최적화하는 아키텍처 설계의 중요성을 깨달을 수 있었습니다.