| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- typescript
- React
- useSWRImmutable
- npm module
- typescript-axios
- openapitools.json
- es module
- useState
- auth.js
- window.scrollY
- next auth
- npm publish
- refreshaccesstokenerror
- trustHost
- next.js
- flutter
- openapi-generator-cli
- Tanstack Query
- npm library
- openapi-generator
- python
- 웹 디자인
- svgr/cli
- next/script
- NEXT
- 폰트 최적화
- .vscode
- refetchInterval
- refresh token race condition
- nextauth
- Today
- Total
김재욱의 이모저모
카카오 애드핏 반응형 광고가 안 떠서 삽질한 이야기 본문
문제 발견
블로그에 카카오 애드핏 광고를 달았다. PC용 배너(728x90)랑 모바일용 배너(320x50)를 각각 만들어서 반응형으로 보여주려고 했다.
근데 이상한 일이 벌어졌다:
- PC 화면으로 페이지 로드 → PC 광고 잘 뜸 ✅
- 개발자 도구로 모바일 화면 전환 → 광고 안 뜸 ❌
- 새로고침하면 → 그제서야 모바일 광고 뜸 ✅
뭐지? 왜 화면 크기 바꾸면 안 되는 거지?
초기 구현 (잘못된 방법)
1단계: layout.tsx에서 전역 스크립트 로드
// layout.tsx
export default async function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="//t1.daumcdn.net/kas/static/ba.min.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}
공식 문서대로 했다. lazyOnload로 스크립트를 전역으로 불러오면 알아서 .kakao_ad_area 클래스를 찾아서 광고를 띄워준다고 했다.
2단계: Tailwind hidden으로 반응형 처리
// ad-banner-pc.tsx
const AdBannerPC = () => {
return (
<div className="hidden md:block"> {/* 768px 이상에서만 보임 */}
<AdBanner adUnit="DAN-xxx" width={728} height={90} />
</div>
);
};
// ad-banner-mobile.tsx
const AdBannerMobile = () => {
return (
<div className="md:hidden"> {/* 768px 미만에서만 보임 */}
<AdBanner adUnit="DAN-yyy" width={320} height={50} />
</div>
);
};
// books/page.tsx
const BooksPage = () => {
return (
<main>
<AdBannerPC />
<AdBannerMobile />
</main>
);
};
Tailwind의 hidden과 md:block으로 화면 크기에 따라 보여주려고 했다.
문제:
- PC 배너와 Mobile 배너가 둘 다 DOM에 존재함
- 하나는
display: none으로 숨겨져 있을 뿐
왜 안 되는가: 카카오 애드핏 스크립트 동작 원리
1. 페이지 로드
↓
2. ba.min.js 스크립트 실행 (lazyOnload)
↓
3. DOM 전체를 스캔해서 .kakao_ad_area 찾음
↓
4. 찾은 광고 영역 초기화 (display: none → block으로 바꾸고 광고 삽입)
↓
5. 완료! (이후론 아무것도 안 함)문제점:
스크립트는 한 번만 실행됨
PC 화면으로 로드했을 때:
- PC 배너:
hidden md:block→ 768px 이상이라 보임 → 초기화됨 ✅ - Mobile 배너:
md:hidden→ 768px 이상이라 숨겨짐 → 스크립트가 건너뜀 ❌
- PC 배너:
화면을 모바일로 전환했을 때:
- Mobile 배너가 CSS 덕분에 보이긴 함
- 근데 스크립트는 이미 실행 완료라서 재초기화 안 됨 ❌
핵심: hidden은 CSS만 바꿀 뿐이지, 스크립트를 다시 실행시키지 않는다!
해결 시도 1: 조건부 렌더링으로 바꿔보자
"아, DOM에 둘 다 있어서 그런가? 그럼 하나만 렌더링하면 되겠네!"
// ad-banners-wrapper.tsx
'use client';
const AdBannersWrapper = () => {
const [isMobile, setIsMobile] = useState<boolean | null>(null);
useEffect(() => {
const mediaQuery = window.matchMedia('(max-width: 767px)');
const handleChange = (e) => {
setIsMobile(e.matches);
};
handleChange(mediaQuery);
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
if (isMobile === null) return null; // SSR 깜빡임 방지
return isMobile ? <AdBannerMobile /> : <AdBannerPC />;
};
로직:
matchMedia로 화면 크기 감지- 모바일이면 Mobile 배너만, PC면 PC 배너만 렌더링
- 화면 전환하면 컴포넌트가 언마운트/마운트됨
예상:
- 컴포넌트가 새로 마운트되면 광고도 새로 뜨겠지?
결과:
- 여전히 안 됨 ❌
왜?
- 새로운
<ins class="kakao_ad_area">를 DOM에 추가해도 - 스크립트는 이미 실행 완료라서 새 엘리먼트를 감지 못 함
- 스크립트를 다시 실행시켜야 함!
해결 시도 2: 스크립트를 재실행하면 되는 거 아니야?
맞다! 문제는 layout.tsx에서 스크립트를 한 번만 로드하는 거였다.
해결책: 각 배너가 자체적으로 스크립트 로드하기
// ad-banner.tsx (수정 전)
const AdBanner = ({ adUnit, width, height }) => {
return (
<div>
<ins
className="kakao_ad_area"
data-ad-unit={adUnit}
data-ad-width={width}
data-ad-height={height}
/>
</div>
);
};
이건 그냥 HTML만 렌더링하는 Passive한 컴포넌트였다.
// ad-banner.tsx (수정 후)
'use client';
import { useEffect, useRef } from 'react';
declare global {
interface Window {
adfit?: {
destroy?: (unit: string) => void;
};
}
}
const AdBanner = ({ adUnit, width, height }) => {
const scriptElementRef = useRef<HTMLScriptElement | null>(null);
useEffect(() => {
// 1. 스크립트 동적 생성
const script = document.createElement('script');
script.src = '//t1.daumcdn.net/kas/static/ba.min.js';
script.async = true;
scriptElementRef.current = script;
document.body.appendChild(script);
// 2. cleanup: 컴포넌트 언마운트 시
return () => {
// 스크립트 제거
if (scriptElementRef.current) {
document.body.removeChild(scriptElementRef.current);
}
// 광고 정리 (중요!)
const globalAdfit = 'adfit' in window ? window.adfit : null;
if (globalAdfit && globalAdfit.destroy) {
globalAdfit.destroy(adUnit);
}
};
}, [adUnit]);
return (
<div>
<ins
className="kakao_ad_area"
style={{ display: 'none' }}
data-ad-unit={adUnit}
data-ad-width={width}
data-ad-height={height}
/>
</div>
);
};
핵심 변화:
- Client 컴포넌트로 변경 (
'use client') - useEffect에서 스크립트를 동적으로 생성
- 컴포넌트 마운트 시 → 스크립트 추가
- 컴포넌트 언마운트 시 → 스크립트 제거 +
adfit.destroy()호출
layout.tsx에서 전역 스크립트 제거
// layout.tsx (수정 후)
export default async function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
{/* Script 태그 제거! */}
</body>
</html>
);
}
전역 스크립트는 이제 필요 없다. 각 배너가 자기 스크립트를 관리한다.
동작 원리
Before (안 되던 방식)
1. layout.tsx에서 전역 스크립트 로드
↓
2. 스크립트 실행 → DOM의 모든 .kakao_ad_area 초기화
↓
3. PC 배너만 보여서 PC 광고만 초기화됨
↓
4. 화면 전환 → Mobile 배너가 보이긴 하지만
↓
5. 스크립트는 이미 실행 완료라 광고 안 뜸 ❌After (되는 방식)
1. PC 화면으로 로드
↓
2. AdBannersWrapper → AdBannerPC 렌더링
↓
3. AdBannerPC 마운트 → useEffect 실행
↓
4. script 태그 생성 → ba.min.js 로드
↓
5. 스크립트가 PC 광고 초기화 ✅
↓
6. 화면을 모바일로 전환
↓
7. AdBannerPC 언마운트 → cleanup 실행
- script 제거
- adfit.destroy('PC광고유닛') 호출
↓
8. AdBannerMobile 마운트 → useEffect 실행
↓
9. 새 script 태그 생성 → ba.min.js 다시 로드
↓
10. 스크립트가 Mobile 광고 초기화 ✅핵심: 컴포넌트가 바뀔 때마다 스크립트가 재실행된다!
adfit.destroy()가 뭐야?
카카오 애드핏은 window.adfit 전역 객체를 만든다.
이 객체에 destroy(unit) 메서드가 있는데, 특정 광고 유닛을 정리해준다.
// TypeScript 타입 선언
declare global {
interface Window {
adfit?: {
destroy?: (unit: string) => void;
};
}
}
// 안전하게 호출하기
const globalAdfit = 'adfit' in window ? window.adfit : null;
if (globalAdfit && globalAdfit.destroy) {
globalAdfit.destroy('DAN-xxx'); // 광고 유닛 정리
}
왜 필요한가:
- 같은 광고 유닛이 여러 번 초기화되면 문제가 생김
- 컴포넌트 언마운트 시 깔끔하게 정리해야 함
- 메모리 누수 방지
결과
✅ PC ↔ Mobile 화면 전환 시 광고 정상 표시
✅ 새로고침 불필요
✅ 각 배너가 독립적으로 스크립트 관리
✅ 메모리 누수 없이 깔끔하게 정리
배운 점
1. CSS만으로는 동적 콘텐츠를 제어할 수 없다
display: none으로 숨기는 건 시각적으로만 숨기는 거다.
외부 스크립트가 DOM을 스캔하는 타이밍에는 영향을 못 준다.
2. 외부 스크립트는 실행 타이밍이 중요하다
카카오 애드핏처럼 DOM을 스캔하는 스크립트는:
- 언제 실행되는가?
- 어떤 엘리먼트를 스캔하는가?
- 재실행이 가능한가?
이걸 이해해야 한다.
3. Next.js Script 컴포넌트의 한계
<Script strategy="lazyOnload"> 는 편리하지만:
- 한 번만 실행됨
- 동적 컴포넌트에는 적합하지 않음
- 반응형 광고는 각 컴포넌트에서 직접 로드하는 게 나음
4. useEffect + cleanup 패턴의 중요성
useEffect(() => {
// 마운트 시 리소스 생성
const resource = createResource();
return () => {
// 언마운트 시 리소스 정리
cleanupResource(resource);
};
}, [deps]);
이 패턴만 제대로 써도 대부분의 메모리 누수를 방지할 수 있다.
5. 조건부 렌더링이 항상 해결책은 아니다
"DOM에서 제거하면 되겠지?"라고 생각했지만,
진짜 문제는 스크립트 재실행이었다.
문제의 본질을 파악하는 게 중요하다.
응용: 다른 광고 서비스에도 적용 가능
이 패턴은 카카오 애드핏뿐만 아니라:
- Google AdSense
- 네이버 애드포스트
- 기타 외부 스크립트 기반 위젯
모두 동일하게 적용할 수 있다.
useEffect(() => {
const script = document.createElement('script');
script.src = '광고스크립트URL';
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
// 각 서비스의 cleanup API 호출
};
}, [dependencies]);
마무리
처음엔 "왜 hidden이 안 돼?"라고 생각했는데,
알고 보니 외부 스크립트의 실행 타이밍 문제였다.
문제를 제대로 이해하니까 해결책도 명확해졌다:
- 전역 스크립트 로드 (X)
- 각 컴포넌트가 자체 스크립트 로드 (O)
비슷한 문제로 삽질하는 사람들한테 도움이 되면 좋겠다! 🚀
'Front-end' 카테고리의 다른 글
| 3MB 폰트와 1MB 이미지가 사이트를 잡아먹고 있었다 (0) | 2025.11.02 |
|---|---|
| 이미지 Preload했는데 왜 느리지? - 브라우저 캐시의 비밀 (0) | 2025.10.29 |
| Naver Maps Api를 활용하기 (+ next.js 에서 cors 오류 해결하기!) (0) | 2025.09.20 |
| Openapi-generator-cli 활용하여 api 코드 생성하기 (2) | 2025.07.29 |
| yarn berry 설치하기 && vscode의 .vscode setting.json 파일 사용시 주의점 (2) | 2025.07.27 |