사용자 피드백 기반의 모달 최적화와 성능 개선: 응답 없는 버튼 문제 해결하기

사용자 피드백 기반의 모달 최적화와 성능 개선: 응답 없는 버튼 문제 해결하기

개요

발매 정보 페이지에서는 각 발매 아이템의 '응모' 또는 '선착순' 버튼을 클릭해 상세 정보를 모달로 확인할 수 있습니다. 그러나 인기 발매일 경우 트래픽이 급증하여 버튼이 반응하지 않거나 여러 번 눌러야 모달이 뜨는 현상이 발생했습니다. 이는 사용자 불편을 초래하며 개선이 필요했습니다.

아래는 사용자 경험과 성능 최적화를 위해 코드와 아키텍처를 개선한 작업 내용을 다룹니다.


문제 해결을 위한 최적화 작업

1. 데이터 로딩 중 모달 즉시 표시 및 스켈레톤 추가

기존 코드에서는 데이터가 완전히 로딩된 후에야 모달이 표시되었습니다. 이를 개선하기 위해 데이터 로딩 중에도 모달을 먼저 표시하고, 로딩 중임을 사용자에게 알리는 스켈레톤 컴포넌트를 추가했습니다.

  • 변경 전 코드:
if (!release) return; // 데이터가 로드되지 않았을 경우 모달을 표시하지 않음
  • 변경 후 코드:
return ( <Portal> <div id="modalSaleInfo" className="modal-wrap open"> <div className="layer-modal modal-bottom slide_type" data-ui-type="Modal_Sale_Info"> {/* 로딩 중일 때 스켈레톤 컴포넌트 표시 */} {isLoading ? <ReleaseLayerSkeleton onClose={closeModal} /> : {/* 나머지 상세 모달 내용 */} } </div> )} </div> </Portal> );

이렇게 구현하여 사용자는 모달이 곧 뜬다는 피드백을 즉시 받을 수 있으며, 데이터가 완전히 로딩되기 전에 발생하는 불편을 줄일 수 있었습니다.

2. 중복 클릭 방지와 API 요청 최적화

모달이 늦게 뜨는 상황에서 사용자가 여러 번 버튼을 누르면서, 불필요한 API 요청이 서버에 과부하를 주는 문제가 발생했습니다. 이를 해결하기 위해 **쓰로틀(throttle)**을 적용하여 반복 클릭을 방지했습니다.

  • 쓰로틀을 적용한 코드:
import { throttle } from 'lodash'; const throttledOpenReleaseLayer = throttle((tabIndex: number) => { OpenReleaseLayer(){/* 상세 모달을 여는 함수*/} } }, 2000);

쓰로틀을 2초로 설정하여, 특정 버튼을 클릭한 후 2초 동안은 추가 클릭을 무시하도록 했습니다. 이 방식으로 동일한 API 요청이 반복 발생하지 않도록 하여, 서버 부하를 줄이고 사용자 경험을 개선했습니다.

3. 전역 상태 관리로 로딩 상태 공유

버튼과 모달은 DOM 구조상 서로 다른 위치에 렌더링됩니다. 특히, 모달은 **포탈(Portal)**을 사용하여 다른 DOM 트리에 존재하므로, 서로의 상태를 직접 공유하기 어려웠습니다. 이를 해결하기 위해 Zustand를 이용해 전역 상태를 관리하여, 버튼을 클릭할 때 로딩 상태를 전역으로 관리하고 모달에서도 동일한 상태를 참조하도록 했습니다.

  • Zustand 상태 관리 스토어 (loadingStore.ts):
import { create } from 'zustand'; interface LoadingState { loadingStates: { [key: string]: boolean }; setLoading: (uuid: string, isLoading: boolean) => void; } export const useLoadingStore = create<LoadingState>((set) => ({ loadingStates: {}, setLoading: (uuid, isLoading) => { set((state) => ({ loadingStates: { ...state.loadingStates, [uuid]: isLoading, }, })); }, })); export const useLoadingState = (uuid: string) => useLoadingStore((state) => state.loadingStates[uuid] || false); export const useLoadingActions = () => useLoadingStore((state) => state.setLoading);

이렇게 구현하여, 버튼 클릭 시의 로딩 상태가 전역으로 관리되면서 버튼과 모달 간의 상태 공유가 원활하게 이루어졌습니다.

4. 로딩 스피너 추가로 버튼 상태 표시

사용자가 버튼을 클릭했을 때 응답이 없으면, 여러 번 눌러야 하는 상황이 발생할 수 있습니다. 이를 방지하고자 로딩 중일 때 스피너를 표시하여 버튼이 반응 중임을 사용자에게 시각적으로 전달했습니다.

  • 스피너가 포함된 버튼 컴포넌트:
const Button = ({ children, disabled, color, size, isLoading = false, ...props }: IButton) => { const classes = [ 'c-button', disabled ? 'c-button__dimmed' : `c-button__${color || 'primary'}`, // disabled이면 'dimmed' 아니면 color 또는 기본값 'primary' size || '' // size가 있으면 추가, 없으면 빈 문자열 ].join(' '); // 배열을 문자열로 조인 return ( <button type="button" disabled={disabled || isLoading} className={classes} {...props} > {isLoading ? <div className="c-spinner"></div> : children} </button> ); };

5.백엔드 협업을 통한 캐시 설정 및 실시간 데이터 최적화

특히, 많은 사용자가 동시에 접속해 동일한 발매 정보를 보려 할 때 서버에 과부하가 발생할 수 있습니다. 이를 해결하기 위해 백엔드에서 캐시를 설정하여 요청이 몰리는 상황에서도 안정적으로 데이터를 제공할 수 있도록 했습니다. 백엔드에서는 동일한 요청에 대해 캐시된 데이터를 반환함으로써 실시간성이 필요한 발매 정보를 빠르게 제공하면서도 서버 부하를 효과적으로 줄일 수 있었습니다. 프론트엔드에서는 실시간성을 보장하기 위해 캐시를 사용하지 않았습니다. 프론트엔드 캐싱을 사용할 경우 발매 정보가 수정되었을 때 이를 즉시 반영하기 어렵기 때문에, stale-time을 설정하지 않고 매번 백엔드에 데이터를 요청하여 최신 정보를 가져오도록 했습니다.

또한, 대부분의 발매 상세 정보는 한번 등록된 뒤 자주 변경되지 않고, 모든 사용자가 동일한 정보를 확인해도 되는 경우가 많기 때문에, 정적 JSON 데이터를 반환하도록 최적화했습니다. 서버에서 상세 정보를 정적 파일로 캐싱해 두고, 클라이언트가 이 정적 데이터를 조회할 수 있도록 변경한 것입니다. 이렇게 함으로써 클라이언트는 발매 정보를 빠르게 로딩할 수 있게 되었으며, API 응답 속도도 크게 개선되었습니다.

  • 변경 전:
export const getProductRelease = async (uuid: string) => { return await get<ProductRelease>(`/api/v2/product_release/${uuid}/`); };
  • 변경 후:
export async function getProductRelease(uuid: string) { const baseUrl = process.env.NEXT_PUBLIC_API_STATIC_URL; const endpoint = `/api/v2/product-release/${uuid}.json`; return await get<ProductRelease>({ baseUrl, endpoint }); }

최종 결과

이와 같은 최적화 작업을 통해 다음과 같은 성과를 얻었습니다.

사용자 경험 개선: 데이터가 로드되지 않았을 때에도 모달이 즉시 뜨면서 스켈레톤 컴포넌트를 통해 로딩 중임을 명확히 전달했습니다. 서버 성능 향상: 쓰로틀 적용 및 백엔드 캐시 설정으로 중복 API 요청을 줄이고 서버 과부하를 방지했습니다. 클라이언트-서버 협업 강화: 백엔드 캐시와 정적 데이터를 활용한 성능 최적화를 통해 트래픽이 몰리는 상황에서도 안정적인 사용자 경험을 제공할 수 있었습니다.


결론

사용자 피드백과 트래픽 분석을 통해 도출된 문제를 해결하며, 백엔드와의 협업을 통해 클라이언트-서버 간의 최적화를 성공적으로 구현했습니다. 이와 같은 작업은 사용자 경험을 개선하면서도 시스템의 안정성을 높이는 데 큰 역할을 했습니다. 앞으로도 사용자와 시스템 양쪽의 요구를 충족하는 개선을 지속적으로 진행해 나갈 계획입니다.

JP
이중표Frontend Engineer

3년차 프론트엔드 개발자. Next.js, React, TypeScript 기반 웹 애플리케이션 개발 전문. 대규모 트래픽 환경에서 SSR·ISR 렌더링 전략 설계 경험.

이력서 보기