문의 폼에 첫 방문 출처 남기기: 유입 경로 기록하는 법
문의한 사람이 ChatGPT·검색·인스타그램 중 어디서 처음 왔는지 남기는 법. referrer·utm·first-touch 개념부터 쿠키 저장, 폼 첨부, 요청문, 빈칸이 생기는 경우까지 정리했습니다.

문의가 한 건 들어오면 곧 궁금해집니다. 이 사람은 어디서 우리 사이트를 알았을까요? 검색이었는지 ChatGPT 답변이었는지, 인스타그램이나 유튜브 설명란이었는지 모르면 다음에 어디에 힘을 줄지 정하기 어렵습니다. 방문자 수 통계는 있어도 문의한 사람의 출처는 따로 남기지 않으면 알 수 없거든요.
이 글에서는 Claude Code나 Codex로 만든 사이트에 "첫 방문 출처"를 기록하고 문의 폼이 제출될 때 그 출처를 함께 저장하는 방법을 정리합니다. 개념은 짧게 설명하고 AI Creator Lab 사이트에 실제로 들어가 있는 구조를 예로 듭니다. 그다음 내 사이트에 붙일 때 쓸 요청문과 확인 방법, 자주 틀어지는 경우를 이어서 다룹니다.
왜 "첫 방문"을 따로 남기나요
제가 만든 고객사 프로젝트(익명)에 이 기록을 붙여 두었는데요. 2026년 8월 27일에 배포했고 8월 31일에 첫 문의가 들어왔습니다. 8월 31일부터 9월 7일까지 8일 동안 문의 25건이 저장됐습니다. 그중 21건(84%)의 첫 방문 출처가 ChatGPT였습니다. 여기서 첫 방문 출처는 문의를 남긴 사람이 처음 사이트에 들어왔을 때 기록된 경로입니다. 한 프로젝트의 초기 관측값이라 다른 사이트에서도 같을 거라는 뜻은 아닙니다.
이 숫자가 의미 있었던 건 문의 한 건마다 출처가 붙어 있었기 때문입니다. 전체 방문자 통계만 봤다면 ChatGPT에서 온 방문이 몇 번인지는 알아도 그 사람들이 실제로 문의했는지는 연결하기 어려웠을 거예요.
첫 방문을 따로 두는 이유도 있습니다. 사람들은 한 번에 문의하지 않는 경우가 많습니다. ChatGPT 답변에서 링크를 눌러 처음 들어왔다가 창을 닫고 며칠 뒤 주소를 직접 입력해 다시 와서 문의할 수 있어요. 이때 마지막 방문만 보면 "직접 유입"으로 찍히고 처음 알게 된 경로는 사라집니다.
먼저 알아둘 단어 네 가지
referrer: 브라우저가 알려주는 "직전 페이지"
다른 사이트의 링크를 눌러 들어오면 브라우저가 요청에 Referer 헤더를 붙여 보낼 수 있습니다(철자가 원래 이렇습니다). MDN 문서에 따르면 이 값에는 출발지 주소의 origin·경로·쿼리가 담길 수 있습니다. #section 같은 조각은 담기지 않고요.
다만 얼마나 담을지는 출발한 사이트의 Referrer-Policy가 정합니다. MDN의 Referrer-Policy 문서를 보면 정책을 따로 정하지 않았을 때의 기본값은 strict-origin-when-cross-origin입니다. 다른 사이트로 이동할 때는 origin만 보냅니다. HTTPS에서 HTTP로 갈 때는 아예 보내지 않습니다. no-referrer로 설정한 사이트에서 오면 아무 정보도 오지 않고요. 그래서 referrer로는 보통 google.com, chatgpt.com 같은 도메인 정도만 알 수 있다고 생각하는 편이 안전합니다.
utm 파라미터: 링크에 직접 붙이는 꼬리표
referrer는 상대 사이트가 주는 정보라 내가 통제할 수 없습니다. 반대로 utm 파라미터는 내가 링크를 만들 때 직접 붙입니다. ?utm_source=instagram&utm_medium=profile&utm_campaign=autumn 같은 식입니다.
Google 애널리틱스 도움말은 utm_source(유입처), utm_medium(매체), utm_campaign(캠페인)을 항상 쓰라고 안내합니다. 같은 캠페인 안에서 소재나 링크를 나눌 때는 utm_content를 씁니다. 도움말의 예시는 한 이메일 안의 버튼 두 개를 구분하는 경우예요. 유료 키워드에는 utm_term을 씁니다. 값은 대소문자를 구분해서 google과 Google이 다른 값으로 잡히니 소문자로 통일하는 게 좋습니다.
ChatGPT 검색 결과에서 오면 붙는 utm_source=chatgpt.com
ChatGPT 검색 결과에 걸린 링크를 눌러 들어오면 주소에 utm_source=chatgpt.com이 붙어 옵니다. 사이트 운영자가 붙인 게 아니라 ChatGPT가 붙여 보내는 값이에요. OpenAI 게시자 FAQ는 OAI-SearchBot을 허용한 사이트라면 Google 애널리틱스 같은 도구로 ChatGPT에서 온 유입을 볼 수 있다고 안내합니다. ChatGPT 검색 결과에서 나가는 주소에 이 값을 자동으로 넣기 때문입니다.
FAQ가 말하는 범위는 검색 결과에서 나가는 링크입니다. 모든 화면의 모든 링크에 붙는다고 보장한 건 아니에요. 링크를 복사해 다른 곳에 붙여 넣는 식으로 경로가 바뀌면 빠질 수도 있으니 referrer 도메인(chatgpt.com)도 같이 보는 게 좋습니다.
first-touch와 last-touch
- first-touch(첫 방문 기준): 처음 들어온 경로를 한 번 저장하고 이후 방문으로 덮어쓰지 않습니다. "처음 어떻게 알게 됐나"에 답합니다.
- last-touch(마지막 방문 기준): 문의 직전의 경로를 저장합니다. "무엇이 마지막으로 행동하게 했나"에 가깝습니다.
둘 중 무엇이 맞다기보다 질문이 다릅니다. 어떤 채널에 글을 더 써야 할지 정하려면 first-touch가 먼저 필요하다고 봐서 이 글은 first-touch를 중심으로 다룹니다. 여유가 있으면 문의 시점의 경로도 함께 남기면 됩니다.
순서: 저장 → 첨부 → 확인
전체 흐름은 세 단계입니다.
- 첫 방문: 출처 저장. 방문자가 페이지를 열 때 utm 값과 referrer 도메인, 처음 도착한 페이지, 시각을 쿠키에 담습니다. 이미 쿠키가 있으면 건드리지 않습니다.
- 폼 제출: 출처 첨부. 문의 폼이 서버로 전송될 때 서버가 쿠키를 읽어 문의 데이터와 같은 행에 저장합니다.
- 리포트 확인. 문의 목록을 출처별로 묶어 봅니다.
AI Creator Lab은 이렇게 되어 있어요
이 사이트에 실제로 들어가 있는 구조를 간단히 풀어 보겠습니다.
- 모든 페이지 요청은 서버에서 먼저 한 번 거칩니다. 이 단계에서 문서 요청 한 번을 페이지뷰 한 행으로 기록합니다. 미리 불러오기(prefetch)나 화면 조각 요청, 리다이렉트 응답은 세지 않습니다. 그래야 방문 한 번이 여러 행으로 부풀지 않습니다.
- 같은 단계에서 User-Agent를 보고 사람, AI 크롤러, 검색 봇, 링크 미리보기 봇, 기타 봇으로 나눕니다. 예를 들어 GPTBot·OAI-SearchBot·ChatGPT-User는 AI 크롤러, Googlebot과 네이버의 Yeti는 검색 봇, 카카오톡·페이스북의 링크 미리보기는 미리보기 봇입니다. 봇 방문을 사람 방문자로 세지 않기 위해서입니다.
- 사람 방문이고 주소에 utm이 붙어 있으며 아직 첫 유입 쿠키가 없을 때만 쿠키를 만듭니다. 쿠키에는 source·medium·campaign·content·term, 처음 도착한 경로, 시각이 들어가고 90일 동안 유지됩니다. 이미 쿠키가 있으면 새로 쓰지 않습니다. 이것이 first-touch입니다.
- referrer 도메인은 쿠키가 아니라 페이지뷰 기록 쪽에 남깁니다. 채널은 utm이 있으면 utm으로, 없으면 referrer 도메인으로 정해서 검색·AI 답변·소셜·유튜브·메신저 같은 이름을 붙입니다. 이 사이트도 2026년 9월 24일까지는
utm_source=chatgpt.com이 붙은 방문을 "기타 링크"로 세고 있었습니다. 채널 규칙이chatgpt라는 이름만 알고chatgpt.com처럼 도메인 모양으로 오는 값은 몰랐기 때문입니다. 이 글을 쓰면서 발견해 고쳤습니다. 지금은 도메인 모양의 utm_source도 referrer와 같은 규칙으로 나눕니다. - 회원이 로그인하면 쿠키의 첫 유입 값을 프로필에 한 번만 복사합니다. 오픈 알림 신청 폼도 제출할 때 같은 쿠키를 읽어 신청 데이터에 붙입니다.
- 유튜브 영상 설명란에는
/yt/<영상ID>같은 짧은 주소만 넣습니다. 이 주소가 utm(utm_source=youtube, 영상 ID는utm_content)을 붙여 홈으로 넘겨 줍니다. 영상 설명을 다시 고치지 않아도 목적지나 캠페인 이름을 바꿀 수 있어요.
쿠키 부분을 단순화하면 이런 모양입니다. 이 사이트 방식(utm이 있을 때만 저장)을 줄인 코드라 referrer는 들어 있지 않습니다. 내 사이트에 붙일 때는 이 코드 대신 아래 요청문을 쓰세요.
// 첫 방문: utm이 있고 아직 첫 유입 쿠키가 없을 때만 저장
const source = url.searchParams.get("utm_source");
if (source && !request.cookies.get("first_touch")) {
const attr = {
source,
medium: url.searchParams.get("utm_medium") ?? undefined,
campaign: url.searchParams.get("utm_campaign") ?? undefined,
content: url.searchParams.get("utm_content") ?? undefined,
term: url.searchParams.get("utm_term") ?? undefined,
landing_path: url.pathname, // 쿼리는 빼고 경로만
at: new Date().toISOString(),
};
response.cookies.set("first_touch", encodeURIComponent(JSON.stringify(attr)), {
path: "/",
maxAge: 60 * 60 * 24 * 90,
httpOnly: true,
sameSite: "lax",
secure: url.protocol === "https:", // 배포 주소(https)에서만 Secure
});
}
쿠키 옵션 네 가지는 각각 하는 일이 다릅니다. MDN 쿠키 문서에 따르면 HttpOnly 쿠키는 자바스크립트로 읽을 수 없고 서버로만 전달됩니다. SameSite=Lax는 다른 사이트에서 링크를 눌러 들어올 때도 쿠키를 보내 줍니다. Secure는 HTTPS 요청에만 붙습니다. 같은 문서는 http: 주소의 사이트가 Secure 쿠키를 만들 수 없다고도 적고 있어서 이 사이트는 https일 때만 Secure를 켭니다. Max-Age로 기간을 정하면 브라우저를 닫아도 남아 있습니다.
값을 저장하기 전에 한 번 거르는 것도 중요합니다. 이 사이트는 utm 값을 180자로 자르고 이메일처럼 보이거나 http로 시작하는 값은 버립니다. 누군가 링크에 이메일 주소를 utm으로 붙여 공유하면 그 값이 분석 기록에 그대로 쌓일 수 있거든요.
이 구조는 제 사이트의 선택입니다. 여러분 사이트에서는 utm이 없을 때도 referrer 도메인을 첫 방문 쿠키에 함께 담아 두는 편이 편합니다. 검색이나 AI 답변에서 utm 없이 들어온 사람의 문의도 출처가 남기 때문입니다. 반대로 utm도 외부 referrer도 없는 방문에서는 쿠키를 만들지 않는 걸 권합니다. 빈 값이 먼저 굳어 버리면 나중에 인스타그램 링크로 다시 들어와도 남지 않거든요. 엄밀히 말하면 "첫 방문"이 아니라 "출처가 확인된 첫 방문"을 남기는 셈입니다. 아래 요청문은 이 기준으로 쓰여 있습니다.
리포트는 이 정도면 충분합니다
문의 데이터에 출처 칸이 생기면 이런 표를 볼 수 있습니다. 아래는 형식을 보여 주는 예시이며 실제 문의가 아닙니다.
| 문의 ID | 문의 일시 | 첫 방문 출처 | referrer 도메인 | 첫 도착 페이지 | 첫 방문 일시 |
|---|---|---|---|---|---|
| 예시-01 | 9/10 14:02 | chatgpt.com | chatgpt.com | /services/profile | 9/08 21:40 |
| 예시-02 | 9/11 09:15 | (없음) | google.com | /faq | 9/11 09:03 |
| 예시-03 | 9/11 18:30 | instagram / profile | (없음) | / | 9/02 12:11 |
| 예시-04 | 9/12 10:05 | (없음) | (없음) | (없음) | (없음) |
예시-01은 이틀 전 ChatGPT에서 처음 들어왔다가 나중에 문의한 경우입니다. 예시-04처럼 전부 비어 있으면 "직접 입력"이라고 단정하지 말고 미확인으로 두세요. 이유는 아래 실패 사례에서 설명합니다.
처음에는 출처별 문의 건수만 세도 충분합니다. 건수가 쌓이면 "유입은 많은데 문의가 없는 채널"과 "유입은 적어도 문의로 이어지는 채널"을 비교할 수 있습니다. 얼마나 쌓여야 차이가 보이는지는 사이트마다 다릅니다. 문의 폼과 저장 자체를 아직 만들지 않았다면 글 끝 "다음 단계"의 첫 글부터 보세요.
개인정보는 필요한 만큼만
출처 기록에는 개인정보가 없어도 됩니다. 그래서 몇 가지 선을 긋는 게 좋습니다.
- 분석용 기록(페이지뷰·유입)에는 이름·이메일·전화번호를 넣지 않습니다. 연락처는 문의 데이터에만 둡니다.
- 전체 URL 대신 경로와 도메인만 저장합니다. 쿼리에 이메일이나 토큰이 섞여 들어오는 경우가 있습니다.
- 개인정보 처리방침에 쿠키 사용과 거부 방법을 적습니다. 개인정보 보호법 제30조 제1항 제7호는 처리방침에 "인터넷 접속정보파일 등 개인정보를 자동으로 수집하는 장치의 설치·운영 및 그 거부에 관한 사항(해당하는 경우에만 정한다)"을 포함하도록 하고 있습니다. 해외 방문자가 많다면 GDPR 같은 규정의 쿠키 고지 요건도 따로 확인하세요.
내 사이트에 붙이는 요청문
Claude Code나 Codex에 붙여 넣기 전에 대괄호 부분만 내 사이트에 맞게 바꾸면 됩니다.
이 사이트의 문의 폼에 "첫 방문 출처"를 함께 저장하고 싶어.
- 문의 폼 위치: [예: /contact 페이지의 문의 폼]
- 문의가 저장되는 곳: [예: Supabase의 inquiries 표]
- 내 도메인: [예: example.com, www.example.com 포함]
1. 사람이 페이지(문서 요청)를 열 때 서버(미들웨어나 서버 함수)에서 first_touch 쿠키를 만든다.
- 담을 값: utm_source, utm_medium, utm_campaign, utm_content, utm_term,
referrer 호스트(앞의 www.는 떼고 호스트만), 처음 도착한 경로(쿼리 제외), 시각
- 내 도메인에서 온 referrer는 외부 출처로 치지 않는다.
- utm_source가 있거나 외부 referrer가 있을 때만 만든다. 둘 다 없으면 만들지 않는다.
- 쿠키가 이미 있으면 덮어쓰지 않는다(first-touch).
- HttpOnly, SameSite=Lax, 90일. Secure는 https 주소에서만 켠다.
- utm 값만 소문자로 맞춘다(경로는 그대로 둔다). 모든 값은 180자로 자르고,
이메일처럼 보이거나 http로 시작하는 utm 값은 버린다.
- 이미지·스크립트·프리페치 요청과 봇 User-Agent에서는 쿠키를 만들지 않는다.
2. 문의 폼이 제출되면 서버에서 쿠키를 읽어 문의 행의 first_touch 칸(JSON)에 저장한다.
칸이 없으면 추가하는 마이그레이션도 만든다.
쿠키가 없으면 null로 저장하고 폼 제출은 정상적으로 성공시킨다.
3. first_touch를 채널 이름으로 바꾸는 함수를 만든다.
- utm_source를 먼저 보고, 없으면 referrer 호스트를 본다.
- 호스트 비교는 완전 일치가 아니라 끝부분 일치로 한다.
예: www.google.com, search.naver.com은 검색, m.youtube.com, youtu.be는 유튜브.
- utm_source가 chatgpt.com처럼 도메인 모양이면 referrer 호스트와 같은 규칙으로 나눈다.
- chatgpt.com은 "AI 답변", google·naver는 "검색", instagram·threads는 "소셜",
youtube·youtu.be는 "유튜브", 어디에도 안 맞으면 "기타 링크",
utm과 referrer가 모두 없으면 "미확인"으로 표시한다.
4. 관리자만 볼 수 있는 화면에 채널별 문의 수 표를 만든다.
5. 연락처·문의 내용은 분석 기록에 복사하지 않는다.
6. 끝나면 배포된 https 주소에서 확인하는 순서를 알려줘.
마지막 확인이 제일 중요합니다. 로컬 http 주소에서는 Secure 쿠키가 만들어지지 않을 수 있으니 배포된 https 주소에서 해 보세요. 테스트 문의에는 내용 첫머리에 [테스트]라고 적어 두면 나중에 골라내기 쉽습니다.
- utm을 붙여 첫 방문. 시크릿 창에서
https://내도메인/?utm_source=test&utm_medium=manual로 들어가 두세 페이지를 이동한 뒤 문의를 넣습니다. 저장된 행의 first_touch에utm_source가test, 처음 도착한 경로가/로 남아 있으면 됩니다. - 덮어쓰지 않는지. 같은 창에서 utm 없이 주소를 다시 열고 한 번 더 문의합니다. 이번 행에도
test가 그대로 있어야 first-touch가 제대로 동작하는 겁니다. - 아무 정보도 없을 때. 새 시크릿 창을 열고 주소창에 도메인을 직접 입력해 문의합니다. first_touch가 비어 있는(null) 게 정상입니다. 표에서는 "미확인"으로 보여야 하고요.
관리자 화면을 만들기 전이라면 문의가 저장되는 표를 직접 열어 보면 됩니다. Supabase라면 Table Editor에서 방금 넣은 행의 first_touch 칸을 확인할 수 있습니다. 확인이 끝난 [테스트] 행은 지우거나 리포트에서 빼 두세요.
출처가 비거나 틀어지는 경우
기록을 붙여도 모든 문의에 출처가 붙지는 않습니다. 빈칸이 나오는 이유를 알고 있으면 숫자를 덜 과하게 해석하게 됩니다.
- referrer가 지워진 경우. 출발 사이트가
no-referrer정책을 쓰거나 HTTPS에서 HTTP로 넘어가면 referrer가 오지 않습니다. 앱 안의 브라우저나 메신저에서 링크를 열 때도 비어 있을 수 있어요. - 직접 유입. 주소를 직접 입력했거나 북마크로 들어온 경우입니다. 다만 1번 때문에 비어 있는 경우와 겉으로는 구분되지 않아서 빈칸은 "직접"이 아니라 "미확인"으로 두는 게 정확합니다.
- 쿠키가 막히거나 지워진 경우. 사용자가 쿠키를 지우거나 브라우저 설정으로 막으면 첫 방문 기록이 사라지고 다음 방문이 새 첫 방문이 됩니다. 90일이 지나 쿠키가 만료돼도 마찬가지입니다.
- 한 사람이 여러 기기를 쓰는 경우. 휴대폰에서 ChatGPT 링크로 처음 보고 나중에 노트북으로 들어와 문의하면 쿠키는 노트북 것만 남습니다. 쿠키는 기기와 브라우저 단위라 이 연결은 알 수 없습니다.
- utm 값이 제각각인 경우. 같은 인스타그램인데
Instagram,insta,ig로 섞이면 표가 쪼개집니다. 링크를 만들기 전에 source·medium 이름표를 한 번 정해 두세요. - 채널 규칙이 값을 못 알아보는 경우. 이 사이트에서
chatgpt.com이 "기타 링크"로 빠졌던 것처럼 규칙이 예상하지 못한 모양의 값은 엉뚱한 칸으로 갑니다. "기타 링크"가 갑자기 늘면 원래 값을 열어 보고 규칙을 고치세요.
그래서 첫 방문 출처는 정답표라기보다 방향을 보는 기록에 가깝습니다. 빈칸 비율이 얼마인지도 함께 보면서 채널 사이의 차이를 읽으면 됩니다.
다음 단계
- 폼 저장부터: Supabase로 첫 데이터베이스: 로그인 없는 폼 저장하기
- 검색·AI 답변 관측과 문의 기록 나누기: 홈페이지를 검색·AI 답변·문의 흐름에 연결하기
- 홈페이지·앱 만들기 주제 모음: 주제별 안내
- 배포부터 문의 기록까지 직접 만들어 보는 과정과 위 사례 수치: 홈페이지 코스
출처와 확인 (2026-09-24)
- MDN: Referer 헤더
- MDN: Referrer-Policy
- MDN: HTTP 쿠키
- Google 애널리틱스: 맞춤 URL로 캠페인 데이터 수집하기
- OpenAI: Publishers and Developers FAQ
- 국가법령정보센터: 개인정보 보호법 제30조(시행 2026-09-11 현행 조문 기준)
- 사례 수치: 홈페이지 코스 소개(https://aicreatorlab.com/courses/website, 2026-09-07 문의 저장·유입 출처 기록 기준, 고객사 익명, 한 프로젝트의 초기 관측값)



