메일 서명에 회사 로고를 넣기로 했다. 웹에서 하던 대로 data: URI로 이미지를 본문에 박아 넣으면 될 것 같았다. 외부 요청이 없으니 이미지 차단에도 안 걸릴 테고.
정확히 반대였다.
메일 클라이언트는 브라우저가 아니다
웹에서 data: URI는 요청 한 번을 아끼는 좋은 수단이다. 메일에서는 가장 나쁜 선택지다.
| 클라이언트 | <img src="data:image/png;base64,…"> |
|---|---|
| Gmail (웹·모바일) | 태그를 걷어낸다. 렌더되지 않는다 |
| Outlook 데스크톱 | 지원하지 않는다. 본문을 Word 엔진으로 그린다 |
| Outlook.com | 걷어낸다 |
| Apple Mail · Thunderbird | 표시된다 |
| 네이버 메일 | 공개된 검증 자료 없음 |
| 다음 메일 | 공개된 검증 자료 없음 |
국내 웹메일 두 곳을 "확인되지 않음"으로 비워둔 데는 이유가 있다. 믿을 만한 공개 자료를 찾지 못했다. 이 문제는 뒤에서 따로 다룬다.
업무 메일에서 가장 중요한 두 클라이언트에서 정확히 안 보인다. 그리고 이건 차단당하는 것보다 나쁘다. 차단은 사용자가 "이미지 표시"를 누르면 살아나지만, 걷어내진 태그는 되살릴 방법이 없다. 받는 사람 입장에서는 이미지가 있었다는 사실조차 알 수 없다.
용량도 불리하다. base64는 본문 HTML 안에 들어가므로 Gmail의 본문 길이 제한(약 102KB, 넘으면 "메시지 전체 보기"로 잘린다)을 그대로 잡아먹는다. 20KB 로고가 base64로 27KB가 되어 본문의 4분의 1을 차지하는 셈이다.
그럼 이미지를 어떻게 넣나 — 세 가지 방법
선택지는 사실상 셋이고, 각각이 메일 안에서 다르게 실린다.
| cid 인라인 첨부 | 절대경로 URL | |
|---|---|---|
| 쓰는 법 | 이미지를 함께 실어 src="cid:logo" | src="https://…/logo.png" |
| 이미지 차단 | 대부분 차단하지 않는다 — 외부 요청이 없다 | 거의 모두 기본 차단 |
| 메일 용량 | 메일마다 이미지 크기만큼 (+37%) | 0 |
| 이미지 교체 | 보낸 메일은 옛 이미지 그대로 | 보낸 메일까지 전부 바뀐다 |
| 서버 의존 | 없다. 메일이 자족적이다 | 서버가 죽거나 도메인이 바뀌면 과거 메일이 전부 깨진다 |
| 부작용 | 일부 클라이언트가 첨부 클립 아이콘을 표시 | 열람 추적으로 오해받고 스팸 점수에 불리 |
반드시 보여야 하는 이미지라면 cid
결정적인 기준은 차단이다. "회사 로고가 든 통일된 서명"이 목표인데 절대 URL을 쓰면 수신자 대부분이 로고가 안 보이는 상태로 처음 받는다. 목표를 만족하지 못하는 선택지다.
cid의 단점인 "첨부 클립이 뜬다"는 미관 문제이고, "용량이 는다"는 로고를 20KB 이하로 최적화하면 메일 한 통에 27KB 늘어나는 정도다. 서버 의존이 없다는 점도 크다 — 도메인이 바뀌거나 서버를 옮겨도 이미 나간 수천 통이 그대로 살아 있다.
절대 URL이 나은 경우도 있다
반대로 뉴스레터나 마케팅 메일이라면 절대 URL이 낫다. 발송량이 많아 용량이 곧 비용이고, 어차피 이미지가 안 보여도 본문만으로 읽히게 설계하며, 배너를 나중에 교체하고 싶은 경우도 있다. 열람 추적도 그쪽에서는 기능이지 흠이 아니다.
정리하면 이미지가 안 보이면 실패인가로 갈린다. 서명 로고는 실패고, 뉴스레터 배너는 아쉬움이다.
서명에 로고 넣기
실제로 만든 서명은 이런 모양이다.
<img src="cid:complogo" width="300" alt="A사"
style="width:300px;max-width:100%;height:auto;border:0;vertical-align:middle;">
보내는 쪽에서는 같은 식별자를 가진 첨부를 인라인으로 실어야 한다. API마다 이름은 다르지만 개념은 같다 — 첨부에 Content-ID를 붙이고 "본문에 박히는 것"이라고 표시한다.
attachments: [{
name: 'logo.png',
contentType: 'image/png',
contentId: 'complogo', // 본문의 cid:complogo 와 짝이 된다
isInline: true, // 첨부 목록이 아니라 본문에 박힌다는 표시
contentBytes: '...base64...'
}]
몇 가지 실무적인 디테일이 있다.
축소본을 미리 만들어 둔다
원본이 1500px인 로고를 그대로 실으면 메일 클라이언트가 제멋대로 줄여 계단현상이 난다. 표시 폭이 300px이라면 고해상도 대응으로 600px짜리를 한 번 만들어 두고, HTML에는 width="300"을 명시한다.
여기서 중요한 건 런타임에 매번 줄이지 않는다는 것이다. 서명은 메일마다 붙기 때문에 그 비용이 발송 수만큼 곱해진다.
width는 속성과 스타일에 둘 다 쓴다
width="300" 속성만 있으면 일부 클라이언트가 무시하고, style만 있으면 스타일을 걷어내는 클라이언트에서 원본 크기로 튀어나온다. 둘 다 쓰는 것이 관행이다. height:auto와 border:0도 함께 넣는다 — 링크로 감쌌을 때 파란 테두리가 생기는 것을 막는다.
미리보기는 웹 경로로 바꿔서 보여준다
서명 편집 화면에서 미리보기를 띄울 때 문제가 하나 생긴다. 브라우저는 cid:를 그릴 수 없다. 메일 클라이언트 안에서만 의미가 있는 주소이기 때문이다.
// 미리보기에서만 cid 를 웹 경로로 바꾼다.
// 발송 본문은 항상 cid 다.
$preview = str_replace('cid:' . LOGO_CID, '/images/logo-600.png', $html);
바꾸는 것은 미리보기뿐이라는 점을 코드에 주석으로 남겨두는 편이 좋다. 나중에 누군가 "미리보기랑 실제가 다르네"라며 발송 쪽을 URL로 바꿔버리기 쉽다.
빈 값은 구분자까지 걷어낸다
서명은 여러 사람의 정보를 채워 만든다. 그런데 비어 있는 항목이 반드시 나온다. 부서명이 없거나 휴대폰이 없는 직원이 있다.
PHONE. | EMAIL. hong@example.com ← 빈 값 자리만 비우면 이렇게 나간다
EMAIL. hong@example.com ← 구분자까지 걷어내야 한다
값이 하나도 안 남으면 그 줄 자체를 없앤다. 사소해 보이지만 전 직원에게 매일 나가는 것이라 티가 크게 난다.
서명의 이메일 주소는 계정 주소여야 한다
직원 정보에 있는 이메일과 실제 발송에 쓰는 계정 주소가 다를 수 있다. 서명에 적는 이메일은 "답장을 받을 주소"이므로 발송 계정 쪽을 써야 한다. 직원 정보의 값을 그대로 쓰면 받는 사람이 엉뚱한 주소로 답장한다.
국내 웹메일은 직접 확인해야 한다
메일 클라이언트 호환성을 정리해둔 표준 참고자료로 caniemail.com이 있다. 21개 클라이언트의 기능별 지원 현황을 모아둔 곳인데, 네이버·다음·카카오는 목록에 아예 없다. Apple Mail, Gmail, Outlook, Yahoo, ProtonMail, GMX, WEB.DE, Mail.ru, WP.pl 같은 서구·유럽 서비스 위주다.
한국어로 검색해도 일반적인 base64 설명글만 나오고, 국내 웹메일의 data: URI 처리에 대한 구체적인 자료는 찾지 못했다. 그래서 이 글에서도 두 칸을 비워뒀다. 추측으로 채우면 읽는 사람이 그걸 근거 삼아 결정하게 된다.
한 통이면 확인된다
확인 방법은 간단하다. 같은 이미지를 세 방식으로 넣은 메일을 한 통 만들어 각 계정으로 보내면 된다.
<table border="1">
<tr><td>A. cid 인라인</td>
<td><img src="cid:testlogo" width="32" alt="A-안보임"></td></tr>
<tr><td>B. 절대경로 URL</td>
<td><img src="https://example.com/logo.png" width="32" alt="B-안보임"></td></tr>
<tr><td>C. data URI</td>
<td><img src="data:image/png;base64,iVBORw0K…" width="32" alt="C-안보임"></td></tr>
</table>
웹메일 작성창에 HTML을 붙여넣는 방식으로는 안 된다 — 에디터가 정리해버린다. SMTP로 직접 보내거나(파이썬의 email.message는 add_related()로 cid를 지원한다) 발송 API를 쓴다.
볼 때 세 가지를 구분한다.
- B는 "이미지 표시"를 누르기 전 상태가 핵심이다. 눌러서 보이는 건 아무 의미가 없다. 받는 사람 대부분은 누르지 않는다.
- C는
alt텍스트가 남는지 본다. 깨진 아이콘과 함께 "C-안보임"이 보이면 태그는 남고 이미지만 못 읽은 것이고, alt까지 통째로 사라졌으면 태그가 제거된 것이다. 둘은 의미가 다르다 — 후자는 되살릴 방법이 없다. - 웹과 모바일 앱을 따로 본다. 같은 서비스라도 렌더링 엔진이 다를 수 있다.
표를 몰라도 되는 선택지가 있다
여기서 짚어둘 것이 있다. cid 인라인을 고르면 이 표를 몰라도 된다.
cid는 특별한 기능이 아니라 메일이 원래부터 이미지를 싣던 방식이다. 새 문법을 클라이언트가 지원해주기를 기대하는 쪽이 아니라, 모든 클라이언트가 오래전부터 처리해온 구조를 쓰는 것이다. 검증표에 없는 클라이언트에서도 동작할 가능성이 가장 높은 이유다.
모르는 클라이언트가 있다는 사실 자체가 cid를 고를 근거가 된다.
스타일을 적용하는 방법
HTML 메일의 CSS는 2000년대 초 웹으로 돌아간다고 생각하면 얼추 맞는다.
인라인 style이 기본이다
<style> 블록과 class는 많은 클라이언트가 걷어낸다. Gmail은 예전보다 나아졌지만, Outlook 데스크톱은 여전히 상당 부분을 무시한다. 그래서 실무에서는 모든 스타일을 태그마다 인라인으로 붙인다.
<div style="font-family:-apple-system,BlinkMacSystemFont,'Malgun Gothic','맑은 고딕',sans-serif;
font-size:12px;line-height:1.7;color:#374151;">
<span style="color:#6b7280;">PHONE.</span> 010-0000-0000
</div>
손으로 쓰면 금방 지저분해지므로, 템플릿에서 클래스로 작성하고 발송 직전에 인라인으로 변환하는 도구(premailer 계열)를 쓰는 방법도 있다. 서명처럼 작은 조각이면 그냥 인라인으로 만드는 편이 빠르다.
안 되는 것들
- 웹폰트 — 대부분 로드되지 않는다. 시스템 폰트 스택으로 쓰고, 한글 클라이언트를 고려해
맑은 고딕류를 스택에 넣는다. position,float,flex,grid— Outlook의 Word 엔진이 모른다. 레이아웃은 여전히<table>로 짠다.- 배경 이미지 — 클라이언트별로 편차가 크다. 배경색으로 대체하는 편이 안전하다.
rem,vh같은 단위 —px로 쓴다.- 단축 속성 일부 —
margin: 0 auto처럼 뭉뚱그린 표기가 무시되는 경우가 있다. 풀어 쓴다.
다크 모드는 통제가 어렵다
일부 클라이언트는 색을 자동으로 반전시킨다. 흰 배경에 검은 글씨로 만든 서명이 검은 배경에 흰 글씨가 되는데, 이때 배경이 투명한 로고가 안 보이게 되는 일이 흔하다.
완전한 통제는 포기하는 편이 낫다. 대신 로고에 여백을 포함한 배경색을 넣거나, 반전되어도 읽히는 색 대비를 고르면 대부분의 사고를 피할 수 있다.
정리
data:URI는 메일에서 쓰지 않는다. Gmail·Outlook에서 태그째 사라지고, 차단보다 나쁘다.- 반드시 보여야 하는 이미지는 cid 인라인. 차단되지 않고 서버에 의존하지 않는다.
- 안 보여도 되는 이미지는 절대 URL. 용량이 0이고 나중에 교체할 수 있다.
- 로고는 축소본을 미리 만들고,
width를 속성과 스타일에 둘 다 쓴다. - 미리보기는
cid를 웹 경로로 치환해서만 보여준다. - 스타일은 인라인으로. 레이아웃은
<table>, 단위는px, 폰트는 시스템 스택. - 국내 웹메일은 공개 검증 자료가 없다. 세 방식을 한 표에 넣은 메일 한 통으로 직접 확인한다.
메일 HTML을 만들다 보면 웹에서 당연하던 것들이 하나씩 걸린다. 클라이언트마다 렌더링 엔진이 다르고, 그중 하나는 워드프로세서다. 브라우저에서 잘 보인다는 것이 아무것도 보장하지 않는다는 전제로 시작하면, 어디서부터 확인해야 하는지가 분명해진다. 결국 실제로 보내보는 것 말고 확인할 방법은 없다.