안녕하세요! 프론트엔드 개발자 코디(Kody)입니다. 👋🏻
최근에… 정말 뼈아픈 경험을 했습니다.
광고 메인배너의 데이터가 뒤섞이는 문제가 발생해, 결국 회사가 광고비를 환불해야 하는 상황까지 갔어요. 😨
광고라는 것은 단순한 UI 요소가 아니라 실제 금전적 가치가 연결된 기능이기 때문에, 이런 실수가 반복되면 신뢰 문제까지 발생할 수 있죠.
그래서 “이런 일이 다시는 발생하지 않도록 해야 한다!”는 생각으로
🔹 문제의 원인을 분석하고,
🔹 어떤 해결책이 가장 효과적인지 고민하고,
🔹 결과적으로 테스트 코드를 도입하여 안정성을 높이는 과정을 거쳤습니다.
이번 글에서는
✔️ 문제가 발생했던 상황과 그 심각성
✔️ 버그의 원인이 무엇이었는지
✔️ 이 문제를 해결하기 위해 어떤 방식으로 테스트를 도입했는지
✔️ 결과적으로 테스트가 어떻게 안정성을 높여줬는지
에 대해 이야기해 보려고 합니다.
🚨 문제 상황: 광고 배너 데이터가 뒤섞였다!
📌 배경: 광고 컴포넌트를 디자인 시스템으로 전환 중
최근 디자인 시스템을 구축하면서 광고 컴포넌트도 리팩토링하는 작업을 진행했습니다.
광고 배너도 이제 디자인 시스템의 컴포넌트로 통합해야 했고,
이 과정에서 기존의 코드들을 정리하면서 리팩토링을 병행했어요.
보통 새로운 디자인 시스템을 적용할 때,
기존 컴포넌트를 하나씩 교체하는 과정에서 자연스럽게 리팩토링도 함께 진행됩니다.
당연히 광고 컴포넌트도 이런 흐름 속에서 교체되었죠.
그러던 어느 날…
🚨 광고 배너에서 이상한 현상이 발생한 겁니다.
📌 증상: 배너의 이미지와 URL이 뒤섞이는 현상 발생
광고 배너는 단순한 UI 요소가 아니라,
기업이 비용을 지불하고 노출하는 콘텐츠이기 때문에, 철저한 관리가 필요합니다.
하지만 이번 버그로 인해 배너가 잘못된 형태로 노출되는 문제가 발생했어요.
🛑 정상적으로 동작해야 하는 방식
1️⃣ API에서 광고 데이터를 받아옴
2️⃣ 배너 순서를 랜덤하게 섞음
3️⃣ 각 배너는 두 개의 광고 소재(A/B)를 가짐
- 예) id=1 배너: 이미지 A, URL AA / id=2 배너: 이미지 B, URL BB
4️⃣ 각 배너의 두 개의 소재 중 하나를 랜덤으로 선택
5️⃣ 배너 데이터가 정리된 후 화면에 렌더링
🚨 실제로 발생한 문제
하지만 버그로 인해, 배너 데이터가 정상적으로 매칭되지 않았어요.
- id=1 배너의 이미지가 A여야 하는데, 이미지 B가 노출됨
- 그런데 URL은 AA로 매칭되어 있음 (즉, 이미지와 URL이 따로 놀고 있음)
- 어떤 배너는 첫 번째 소재(A)의 이미지가 사용되는데, 두 번째 소재(B)의 URL이 매칭됨
즉, 광고 소재와 URL이 뒤섞이는 심각한 버그가 발생한 것입니다.
😨 이 문제로 인해 잘못된 광고가 노출되었고, 결국 광고주에게 하루치 광고비를 환불하는 사태가 발생했습니다.
이건 단순한 UI 버그가 아니라, 비즈니스적인 손실로 이어진 심각한 문제였어요.
🔎 원인 분석: 어디서 문제가 발생했을까?
이번 버그의 핵심 문제는 배너 데이터를 랜덤하게 섞는 과정에서 ID와 이미지/URL 데이터가 올바르게 매칭되지 않은 것이었습니다.
결과적으로 배너의 이미지와 URL이 뒤섞여서 잘못된 광고가 노출되는 심각한 문제가 발생했어요.
📌 문제의 핵심 원인
버그의 원인을 파악하기 위해 디버깅을 진행하면서 몇 가지 중요한 패턴을 발견했습니다.
1️⃣ 배너 데이터를 랜덤하게 섞는 과정에서 데이터 매칭 오류 발생
광고 배너는 매번 순서가 랜덤하게 변경되는 구조였습니다.
즉, 각 배너의 ID와 광고 소재(A/B)의 연결이 유지된 상태에서 섞여야 했는데,
배너를 섞는 과정에서 ID와 연결된 이미지/URL 정보가 따로 놀기 시작한 것이 문제였습니다.
const shuffledBanners = getShuffledMainBanners(mainBanner);
✔ 문제 발생 원인:
- getShuffledMainBanners() 함수에서 배너 순서를 무작위로 섞는 과정에서 배너 ID와 배너의 소재(A/B) 정보가 올바르게 유지되지 않음
- 즉, 원래 id=1 배너가 이미지 A / URL AA여야 하는데, → 이미지 B / URL AA와 같이 잘못된 조합이 생성됨.
✔ 디버깅 과정에서 확인된 현상:
- 특정 경우에 배너가 정상적으로 매칭되기도 했지만, 렌더링 순서에 따라 ID와 광고 소재가 어긋나는 현상이 간헐적으로 발생
- 특히, 배너 배열이 변경되었을 때, 기존 데이터가 그대로 남아있어 URL과 이미지가 엇갈릴 가능성이 있었음
2️⃣ useMemo 의존성 문제로 인해 배너 데이터가 렌더링 사이클에서 어긋남
이 문제를 더 깊이 파고들다 보니,
MainBannerSection 컴포넌트에서 bannerOptions를 만드는 useMemo 의존성 문제도 연관이 있었습니다.
📌 기존 코드 구조
const bannerOptions = useMemo(() => useMainBanner(enabledBanners, isMobile), [isMobile]);
const bannerSlides = bannerOptions.map((item, idx) => {
return (
<ExternalLink key={idx} href={item.url}>
<MainBannerImage filename={item.filename} />
</ExternalLink>
);
});
✔ 문제 발생 원인:
- bannerOptions는 useMemo를 통해 메모이제이션이 이루어지고 있었음
- 하지만, 배너 슬라이드(bannerSlides)와 렌더링 사이클이 서로 다르게 동작
- 즉, bannerOptions가 한 번 계산된 이후 배너 리스트(bannerSlides)와 동기화되지 않는 경우가 발생
✔ 디버깅 과정에서 확인된 현상:
- 광고 데이터가 바뀔 때마다 useMemo의 의존성이 불완전하여, 이전 상태의 데이터를 참조하는 경우가 있었음
- 이전 렌더링에서 사용된 배너의 소재(A/B)와 현재 배너 ID가 일치하지 않는 경우가 발생
- 즉, 배너 순서를 섞는 과정에서 랜덤성이 유지되지만, 그와 연결된 데이터가 동기화되지 않는 문제
🚨 즉, useMemo가 enabledBanners를 의존성 배열에서 제외하면서, 배너 데이터가 최신 상태로 업데이트되지 않는 문제 발생!
📌 일단 급한 불 끄기: bannerOptions와 bannerSlides의 동기화
이 문제를 해결하기 위해 배너 데이터를 렌더링하는 과정에서 useMemo 의존성을 보다 명확하게 관리했습니다.
✅ 수정된 코드 구조
bannerOptions를 bannerSlides 내부에서 생성하여 같은 의존성을 유지
const bannerSlides = useMemo(() => {
const bannerOptions = useMainBanner(enabledBanners, isMobile);
return bannerOptions.map((item, idx) => {
return (
<ExternalLink key={idx} href={item.url}>
<MainBannerImage filename={item.filename} />
</ExternalLink>
);
});
}, [enabledBanners, isMobile]);
✔ 배너 데이터와 슬라이드 렌더링을 같은 의존성을 기반으로 동기화
✔ 배너 순서를 섞을 때, ID와 광고 소재(A/B)가 정확하게 유지됨
✔ 배너 데이터가 변경될 때마다 useMemo가 최신 값을 참조하도록 수정
🛠 해결 방법: 테스트 코드 도입 & 자동화 구축!
이번 문제를 겪으며 가장 큰 깨달음을 얻었습니다.
“광고처럼 중요한 영역에서는 테스트 코드가 필수적이다.”
QA 단계에서 이런 문제를 발견하는 것도 중요하지만,
그 이전에 개발 단계에서 오류를 잡을 수 있도록 테스트 코드가 있어야 한다는 것을 절실히 깨달았어요.
이후 단순한 테스트 코드 작성에서 더 나아가,
테스트를 자동으로 실행하고 코드 안정성을 지속적으로 유지하기 위해 Husky를 활용한 테스트 자동화까지 도입하게 되었습니다.
그 과정에서 어떤 테스트를 도입했고, 어떻게 자동화를 적용했는지 자세히 공유해보겠습니다.
✅ 1. 유닛 테스트 (useMainBanner.test.ts)
👉 배너 데이터가 올바르게 변환되는지 확인
import { renderHook } from "@testing-library/react";
import { useMainBanner } from "@/hook/helper/mainBanner";
import { mockAllAds } from "@/__test__/mock/getCampaignCamps";
import { getShuffledMainBanners, groupAdProductByAdProduct } from "@/utils/ads";
const mainBanner = groupAdProductByAdProduct(mockAllAds).mainBanner;
const mockMainBanners = getShuffledMainBanners(mainBanner);
describe("useMainBanner Hook", () => {
it("배너 데이터가 올바르게 변환되는지 확인", () => {
const { result } = renderHook(() => useMainBanner(mockMainBanners, false));
result.current.forEach((transformedBanner) => {
const originalMock = mockMainBanners.find((banner) => banner.adId === transformedBanner.adId);
expect(originalMock).toBeDefined();
expect(transformedBanner.bannerId).toBe(originalMock?.selectedBanner.bannerId);
expect(transformedBanner.url).toBe(originalMock?.selectedBanner.pcUrl);
});
});
it("isMobile이 true일 때 올바른 모바일 URL과 파일명을 반환하는지 확인", () => {
const { result } = renderHook(() => useMainBanner(mockMainBanners, true));
result.current.forEach((transformedBanner) => {
const originalMock = mockMainBanners.find((banner) => banner.adId === transformedBanner.adId);
expect(originalMock).toBeDefined();
expect(transformedBanner.filename).toBe(originalMock?.selectedBanner.mobileFilename);
expect(transformedBanner.url).toBe(originalMock?.selectedBanner.mobileUrl);
});
});
});
✔️ 랜덤으로 섞인 후에도 데이터가 정상적으로 매칭되는지 검증
✔️ 배너 ID, 이미지, URL이 일치하는지 확인
✔️ 모바일 환경에서도 올바르게 동작하는지 테스트 추가
✅ 2. 컴포넌트 테스트 (MainBannerSection.test.tsx)
👉 배너 데이터가 정상적으로 화면에 렌더링되는지 확인
import { render, screen } from "@testing-library/react";
import MainBannerSection from "@/components/ads/MainBannerSection";
describe("MainBannerSection 컴포넌트", () => {
it("배너 데이터가 정상적으로 렌더링되는지 확인", () => {
render(<MainBannerSection enabledBanners={mockBanners} isLoading={false} />);
const bannerLinks = screen.getAllByRole("link");
expect(bannerLinks).toHaveLength(mockBanners.length);
});
it("배너 링크 클릭 시 새 탭에서 열려야 한다", () => {
render(<MainBannerSection enabledBanners={mockBanners} isLoading={false} />);
const bannerLink = screen.getAllByRole("link")[0];
expect(bannerLink).toHaveAttribute("target", "_blank");
});
});
✔️ 렌더링된 배너 수와 데이터 개수가 일치하는지 확인
✔️ 배너 링크가 올바르게 설정되었는지 검증
✔️ 배너 클릭 시 정상적으로 새 창이 열리는지 테스트 추가
🚀 Husky를 활용한 테스트 자동화 도입
테스트 코드를 추가한 이후에도 고민이 있었습니다.
“이 테스트를 항상 실행해서 코드 안정성을 유지하려면 어떻게 해야 할까?”
개발 과정에서 테스트를 수동으로 실행하는 것은 번거롭고,
테스트 실행을 잊어버리는 경우도 많았기 때문에 Husky를 도입해 Git Hooks 기반으로 테스트 자동 실행 환경을 구축했습니다.
✔️ pre-commit: 코드 스타일 검사 & 자동 수정
✔️ pre-push: Git 푸시 전에 자동으로 yarn test 실행하여 테스트 검증
✨ 테스트 자동화 도입 후 변화
✅ 개발자가 실수로 테스트를 실행하지 않고 코드를 푸시하는 경우를 방지!
✅ 테스트 코드가 코드 리뷰 전 필수 단계가 되어 코드 안정성 향상
✅ 팀 전체적으로 코드 품질 유지 및 테스트 문화 정착
광고 컴포넌트의 안정성이 필수적인 만큼, Husky를 활용한 자동화는 큰 도움이 되었습니다.
특히, 작은 실수로 인한 광고 배너 문제는 다시 발생하지 않도록 철저하게 예방할 수 있었어요.
📌 결론: 테스트 도입 & 자동화는 선택이 아니라 필수!
이번 경험을 통해 크게 두 가지를 배웠습니다.
📌 첫 번째, 광고처럼 중요한 기능에서는 테스트 코드가 필수적이다.
광고 배너 하나만 잘못 노출되어도, 비즈니스적으로 큰 손실이 발생할 수 있습니다.
이제는 광고 관련 코드에는 반드시 테스트 코드를 추가하고 있습니다.
📌 두 번째, 테스트 자동화는 코드 품질 유지의 핵심이다.
테스트를 작성해도, 실행하지 않으면 의미가 없습니다.
Husky를 활용한 자동화된 테스트 실행 환경을 구축한 덕분에,
이제는 개발 과정에서 실수로 테스트를 빼먹는 일이 사라졌어요.
🎯 앞으로의 목표
1️⃣ 테스트 커버리지 확대
현재는 광고 데이터 변환 & 랜더링 테스트 위주로 진행했지만,
광고 클릭 추적, A/B 테스트 결과 검증 등 더 다양한 시나리오의 테스트도 추가할 예정입니다.
2️⃣ CI/CD 파이프라인에 테스트 자동 실행 연동
현재는 로컬 개발 환경에서 Husky를 통해 실행하고 있지만,
추후에는 GitHub Actions CI/CD를 활용해 배포 전에 자동으로 테스트를 실행하도록 개선할 계획입니다.
이번 문제를 통해 다시 한번 테스트의 중요성을 체감했습니다.
작은 실수 하나가 광고주에게 신뢰 문제를 만들고, 금전적 손실로 이어질 수 있기 때문이죠.
이제는 광고 관련 기능을 개발할 때마다 테스트 코드가 기본적으로 포함되도록 팀의 개발 문화도 바뀌었습니다.
“이 정도는 괜찮겠지”라는 생각이 얼마나 위험한지를 다시 한번 깨닫게 된 경험이었어요.
오늘도 긴 글 읽어주셔서 감사합니다! 🙆🏻♂️
'개발 일기' 카테고리의 다른 글
플럭스 패턴(Flux Pattern)을 중심으로 본 Zustand의 동작방식 (0) | 2025.03.03 |
---|---|
Next.js 정리 - React와 Next.js, 서버컴포넌트와 클라컴포넌트, CSR과 SSR (0) | 2025.03.03 |
팀 내 개발 프로세스 개선. 스타트업다운 해결 방법? (0) | 2025.02.08 |
이벤트루프와 캐싱을 활용한 데이터 필터링&정렬 성능 최적화 (0) | 2025.01.27 |
스타트업 프론트엔드 개발자의 사내 디자인시스템 구축기 (1) | 2025.01.26 |