블로그 구축기의 후속 이야기다. 1편의 12단계 표에서 6단계가 "애드센스 준비"였다. 그때 글 상세에 광고 자리 세 곳을 만들고, ads.txt와 소개·개인정보처리방침 페이지를 붙였다. 광고 자리는 만들어 두었지만, 어느 자리에 어떤 광고 단위를 넣을지는 설정 파일에 숫자를 적는 방식이었다.
이번 글은 그 광고 위치 설정이 관리자 화면으로 옮겨 가고, 결국 위치마다 토글 하나만 남게 된 과정이다. 화면을 네 번 바꿨는데, 매번 바로 앞 단계에서 불편했던 점을 하나씩 걷어 낸 결과였다.
| 단계 | 광고 단위를 정하는 방법 | 바꾼 이유 |
|---|---|---|
| 0 | 설정 파일에 slot ID 입력 | (처음 구조) |
| 1 | 관리자 화면에서 위치별 on/off + slot ID 입력 | slot 하나 바꾸는 데 배포가 필요했다 |
| 2 | 애드센스 광고 단위 목록을 불러와 드롭다운으로 선택 | 숫자를 복사해 붙여 넣는 일이 번거롭고 틀리기 쉬웠다 |
| 3 | 이름이 접두어_위치인 단위를 자동 선택 |
위치 여섯 개를 매번 목록에서 찾아 골라야 했다 |
| 4 | 드롭다운을 없애고 연결 상태 + 토글만 표시 | 이름 규칙이 정해지자 고를 일이 없어졌다 |
0단계: 설정 파일의 slot ID
처음 구조는 단순했다. 광고 위치는 본문 상단(제목 아래), 본문 중간, 본문 하단 세 곳이었고, 위치마다 애드센스 광고 단위의 slot ID를 설정 파일에 적었다. ASP.NET Core로 옮긴 뒤에는 appsettings.json의 이 부분이었다.
"AdSense": {
"ClientId": "",
"HeadScriptEnabled": true,
"AdUnitsEnabled": false,
"Slots": {
"PostTop": "",
"PostMiddle": "",
"PostBottom": ""
}
}
광고를 그리는 부분 뷰 _AdUnit은 위치 이름을 받아 slot을 찾고, 값이 없으면 아무것도 출력하지 않는다. 광고가 늦게 떠도 본문이 밀리지 않도록 자리 높이를 미리 잡아 둔 것도 이때 정했다.
@{
var slot = AdSense.GetSlot(Model); // 꺼져 있거나 slot 이 비어 있으면 null
}
@if (slot != null)
{
<ins class="adsbygoogle" style="display:block"
data-ad-client="@AdSense.ClientId"
data-ad-slot="@slot"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
}
본문 중간 광고는 본문 HTML을 가운데 <h2> 바로 앞에서 둘로 나눠 그 사이에 넣는다. 소제목이 없는 글이나, 고른 <h2>가 본문 맨 앞이라 상단 광고와 붙게 되는 글에서는 중간 광고를 생략한다.
문제는 운영이었다. 광고 단위 하나를 바꾸거나 위치 하나를 끄려면 설정 파일을 고치고, 커밋하고, 배포해야 했다. slot ID는 비밀값이 아니어서 저장소에 그대로 두었는데, 그러다 보니 광고 설정 변경이 곧 코드 배포가 됐다.
1단계: 관리자 화면으로 옮기기
광고 설정을 운영 중에 바꿀 수 있도록 /admin/ads 화면을 만들었다. 왼쪽에는 블로그 화면을 단순하게 그린 미리보기 도식이 있고, 오른쪽에는 위치마다 켜기/끄기 스위치와 slot ID 입력칸이 있다. 스위치를 켜면 도식의 해당 자리가 강조된다.
위치도 여섯 곳으로 늘렸다.
| 키 | 이름 | 자리 |
|---|---|---|
PostTop |
본문 상단 | 글 제목·요약 아래 |
PostMiddle |
본문 중간 | 본문 가운데 소제목(h2) 앞 |
PostBottom |
본문 하단 | 본문 끝, 태그 위 |
Sidebar |
사이드바 | 카테고리·최근 글 아래 (모바일은 본문 아래) |
ListTop |
목록 상단 | 홈·카테고리·태그 글 목록 위 |
ListBottom |
목록 하단 | 글 목록 아래, 페이지 이동 위 |
값은 새 테이블 SiteSetting(Key, Value, UpdatedAt)에 키-값으로 저장했다. 키는 AdSense:Position:PostTop:Slot, AdSense:Position:PostTop:Enabled 같은 모양이다. 광고 설정 하나 때문에 테이블을 따로 설계하기보다, 앞으로 관리자 화면에서 바꿀 설정을 같은 테이블에 쌓을 수 있게 했다.
기존 동작을 깨지 않는 것이 중요했다. DB에 값이 없는 항목은 appsettings의 값을 기본값으로 쓴다.
private AdPositionSetting GetPosition(IReadOnlyDictionary<string, string> stored, string key)
{
var enabled = stored.TryGetValue(EnabledKey(key), out var enabledValue)
? enabledValue == Bool(true)
: O.AdUnitsEnabled; // DB 에 없으면 appsettings
string slot;
if (!stored.TryGetValue(SlotKey(key), out slot))
{
O.Slots?.TryGetValue(key, out slot);
}
return new AdPositionSetting { Key = key, Enabled = enabled, Slot = slot?.Trim() ?? string.Empty };
}
광고 설정은 모든 페이지에서 읽는다. 요청마다 DB를 조회하지 않도록 AdSense:* 키 전체를 한 번 읽어 메모리에 두고, 저장할 때만 비워서 다음 조회에서 다시 읽게 했다. 저장하자마자 블로그에 반영되는 이유다. DB 읽기에 실패하면 1분 동안은 appsettings 값만 쓰고 다시 시도하지 않는다. 광고 설정 때문에 페이지가 오류를 내면 안 되기 때문이다.
입력 검증은 두 가지였다. slot ID는 숫자만 받고(^\d{5,20}$), 켠 위치는 slot ID가 반드시 있어야 한다.
이것으로 배포 없이 광고를 켜고 끌 수 있게 됐다. 하지만 slot ID 자체는 여전히 애드센스 화면에서 광고 코드를 열고, data-ad-slot 값을 찾아 복사해 와야 했다. 열 자리 숫자를 여섯 번 옮겨 적는 일은 번거롭고, 잘못 붙여 넣어도 숫자 형식만 맞으면 그대로 저장된다.
2단계: 애드센스에서 광고 단위 목록 불러오기
그 무렵 관리자 화면에 애드센스 수익 대시보드를 먼저 만들어 두었다. 구글 계정을 OAuth로 연결하고 AdSense Management API로 보고서를 받아 오는 화면이다. 이미 읽기 권한(adsense.readonly)으로 연결되어 있으니, 같은 연결로 광고 단위 목록도 받아 올 수 있었다.
애드센스 API에서 광고 단위는 계정 → 광고 클라이언트 → 광고 단위 순서로 내려간다. 블로그에 붙이는 광고 단위는 콘텐츠 광고(AFC) 클라이언트에 있다.
var clients = await _api.ListAdClientsAsync(account, accessToken);
foreach (var client in clients.Where(c => c.ProductCode == "AFC"))
{
foreach (var unit in await _api.ListAdUnitsAsync(client.Name, accessToken))
{
var type = unit.ContentAdsSettings?.Type; // DISPLAY, FEED, ARTICLE ...
units.Add(new AdUnitOption
{
Slot = unit.Slot,
Name = string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.Slot : unit.DisplayName.Trim(),
Archived = unit.State == "ARCHIVED",
Type = type == null || type == "DISPLAY" ? null : AdUnitTypeName(type)
});
}
}
광고 단위의 name은 accounts/pub-0000000000000000/adclients/ca-pub-0000000000000000/adunits/1234567890 같은 경로라서, 마지막 / 뒤가 곧 광고 코드의 data-ad-slot 값이다.
public string Slot => Name?.Substring(Name.LastIndexOf('/') + 1);
목록 API는 페이지 단위로 내려오므로 nextPageToken이 없을 때까지 모두 읽는다. 받은 목록은 30분 동안 캐시하고, 애드센스에서 단위를 새로 만들었을 때를 위해 "목록 새로고침" 버튼을 두었다.
화면에서는 위치별 slot 입력칸이 드롭다운으로 바뀌었다. 신경 쓴 부분은 기존 값을 잃지 않는 것이었다.
- 보관(archive)된 단위는 광고가 나오지 않으므로 목록에서 뺐다. 다만 지금 그 위치에 연결된 단위라면 보여 준다. 그래야 "보관됨" 표시를 보고 바꿔야 한다는 것을 알 수 있다.
- 1단계에서 입력해 둔 slot이 애드센스 목록에 없으면 "(애드센스 목록에 없음)"이라는 항목으로 남겨 둔다. 드롭다운에 없다고 값이 빈칸으로 저장되면 안 된다.
- 애드센스 연결이 안 되어 있거나 목록 조회에 실패하면 1단계의 slot ID 입력칸으로 돌아간다. 외부 API 상태 때문에 광고 설정을 못 바꾸는 일은 없게 했다.
위치마다 서로 다른 광고 단위를 연결해 두면 쓸모가 하나 더 생긴다. 수익 대시보드의 광고 단위별 표에서, 저장된 slot과 광고 단위를 맞춰 "본문 상단" 같은 위치 이름을 함께 보여 준다. 어느 자리가 수익을 내는지 바로 보인다.
3단계: 이름 규칙으로 자동 선택
드롭다운이 생기자 slot을 복사할 일은 없어졌다. 대신 다른 불편이 보였다. 광고 단위가 여러 개 쌓이면 위치 여섯 곳마다 긴 목록에서 맞는 단위를 찾아 골라야 했다. 그리고 같은 코드로 돌아가는 경제 블로그(economy.addsoft.co.kr)까지 생각하면, 한 애드센스 계정 안에 여러 사이트의 광고 단위가 섞일 수 있었다.
그래서 광고 단위 이름에 규칙을 정했다. 사이트접두어_위치이름이다. 이 블로그는 블로그_본문상단, 블로그_사이드바처럼 짓고, 경제 블로그는 경제_본문상단처럼 짓는다. 접두어는 사이트 설정에 넣었다.
// appsettings.json (기술블로그)
"AdUnitPrefix": "블로그"
// sites/economy.json (경제 블로그)
"AdUnitPrefix": "경제"
컨트롤러는 slot이 비어 있는 위치에 대해 이름이 맞는 단위를 찾아 미리 골라 둔다. 사람이 애드센스 화면에서 이름을 지을 때 띄어쓰기나 구분자를 조금씩 다르게 쓸 수 있어서, 비교할 때는 공백·_·-를 지우고 소문자로 맞췄다.
private static string NormalizeUnitName(string name)
{
return new string(name.Where(c => !char.IsWhiteSpace(c) && c != '_' && c != '-').ToArray()).ToLowerInvariant();
}
후보는 보관되지 않은 디스플레이 단위만으로 좁혔다. 블로그의 _AdUnit이 출력하는 광고 코드는 디스플레이 형식이라, 인피드나 콘텐츠 내 광고 단위를 위치에 연결하면 형식이 맞지 않는다. 드롭다운에서도 이때부터 디스플레이가 아닌 단위는 이미 연결된 경우만 보이게 했다.
이 단계에서는 자동 선택을 화면에만 채웠다. 해당 위치 아래에 "이름이 맞는 '블로그_본문상단' 단위를 자동으로 골랐습니다. 저장해야 반영됩니다."라는 안내를 띄우고, 저장은 관리자가 확인한 뒤 하게 했다. 이미 slot이 있는 위치는 건드리지 않았다.
4단계: 드롭다운을 없애다
자동 선택을 써 보니 드롭다운이 남아 있을 이유가 없었다. 이름 규칙대로 단위를 만들면 항상 그 단위가 골라지고, 다른 단위를 고를 일이 없다. 오히려 드롭다운이 있으면 규칙과 다른 단위를 손으로 골라 저장할 수 있다. 그러면 수익 대시보드의 위치 표시와 애드센스의 단위 이름이 서로 다른 이야기를 하게 된다.
그래서 기준을 바꿨다. 이름 규칙이 곧 매핑이다. 화면은 위치마다 두 가지만 보여 준다.
- 이름 규칙으로 찾은 단위가 있으면: 초록색 체크와 함께 단위 이름, slot 번호
- 없으면: 노란색 경고와 함께 "애드센스에
블로그_본문상단디스플레이 광고 단위가 없습니다. 만든 뒤 목록을 새로고침하세요." 그리고 그 위치의 토글은 꺼진 채로 잠긴다
자동 선택 메서드 AutoSelect는 MapByName으로 바뀌었다. slot이 빈 위치만이 아니라 모든 위치에 대해, 기대하는 단위 이름과 찾은 단위를 행에 담는다.
foreach (var row in model.Rows)
{
row.ExpectedUnitName = model.AdUnitPrefix + "_" + row.Info.Name.Replace(" ", string.Empty);
var wanted = NormalizeUnitName(row.ExpectedUnitName);
row.Unit = candidates.FirstOrDefault(u => NormalizeUnitName(u.Name) == wanted);
}
가장 중요한 변화는 저장 쪽이다. 화면에 hidden 필드로 slot을 실어 보내기는 하지만, 서버는 그 값을 믿지 않는다. 저장할 때 이름 규칙으로 다시 찾은 단위의 slot으로 덮어쓴다.
if (model.NameMapping)
{
// 이름 규칙이면 화면 값 대신 찾은 단위의 slot 을 저장한다
row.Setting.Slot = row.Unit?.Slot ?? string.Empty;
if (row.Setting.Enabled && row.Unit == null)
{
row.Error = $"애드센스에 '{row.ExpectedUnitName}' 디스플레이 광고 단위를 만든 뒤 목록을 새로고침하세요.";
}
}
화면에서 토글을 잠가 두었으니 단위가 없는 위치를 켜서 보낼 일은 보통 없다. 그래도 서버에서 한 번 더 막았다. 잠긴 토글은 disabled 체크박스라 폼에 전송되지 않고, 결과적으로 꺼진 것으로 저장된다.
이름 규칙을 쓸지는 조건 하나로 정한다.
public bool NameMapping => AdUnits.HasUnits && !string.IsNullOrEmpty(AdUnitPrefix);
애드센스가 연결되어 목록을 받았고 접두어가 설정되어 있을 때만 이름 규칙을 쓴다. 연결이 안 됐거나, 조회에 실패했거나, 접두어가 비어 있으면 1단계의 slot ID 직접 입력으로 돌아간다. 화면 상단에는 지금 어느 방식인지와 그 이유를 한 줄로 보여 준다.
드롭다운과 그 스타일, 단위 이름을 조합하던 코드가 빠지면서 이 커밋은 추가보다 삭제가 많았다. 기능을 덜어 내고 화면이 더 분명해진 경우였다.
지금의 운영 흐름
광고 위치 하나를 새로 쓰려면 이렇게 한다.
- 애드센스에서 디스플레이 광고 단위를
블로그_목록상단같은 이름으로 만든다. - 블로그 관리자 광고 설정에서 "목록 새로고침"을 누른다.
- 해당 위치에 초록색 체크와 단위 이름이 뜨면 토글을 켜고 저장한다.
slot 번호는 한 번도 직접 보지 않는다. 화면에 표시는 되지만 확인용일 뿐이다.
정리하며
- 설정을 화면으로 옮길 때는 기존 값을 기본값으로 남긴다. DB에 값이 없으면
appsettings를 읽게 해서, 화면을 배포한 순간에도 광고 동작이 바뀌지 않았다. - 외부 API에 기대는 화면에는 돌아갈 길을 둔다. 애드센스 연결이 끊기거나 조회가 실패해도 slot ID 직접 입력으로 설정은 계속 바꿀 수 있다.
- 규칙을 정했다면 선택지를 없앤다. 자동 선택까지는 편의 기능이었지만, 드롭다운을 없애고 서버가 이름 규칙으로 slot을 정하게 하면서 규칙이 실제로 지켜지는 구조가 됐다.
- 화면이 아니라 서버가 최종 판단을 한다. 토글을 잠그는 것과 별개로, 저장할 때 서버가 다시 찾아 정한다.
입력칸 여섯 개와 숫자 검증으로 시작한 화면이 결국 토글 여섯 개로 줄었다. 광고 단위 이름을 짓는 순간 연결이 끝나니, 광고 설정에서 실수할 여지도 그만큼 줄었다.