테스트 페이지

이주호's avatar
Jan 24, 2025
테스트 페이지
 

1. 서버 사이드에서 로깅하는 방법!!

 
  1. 페이지 방문 후 서버에 요청이 들어오는 시점에 blog_id, post_id 정보를 담아 빈 log 생성 후 페이지 컴포넌트 return. page 의 prop 으로 blog, post 데이터와 함께 생성한 log_id 값도 같이 전달
    1.  
      ... const { traffic_type } = props.searchParams; const isInternal = traffic_type === "internal"; let logId = null; if (!isInternal) { const { data, error: logError } = await supabase .from("logs") .insert({ blog_id: postProps.blog.id, post_id: postProps.post.id, }) .select("id") .single(); logId = data?.id ?? null; } const serverContent = ( <div className={clsx( "px-5 w-full", postProps.post.content_type === "tiptap" && "tiptap" )} dangerouslySetInnerHTML={{ __html: contentHtml ?? "" }} /> ); return ( <> {isTeamPlan && postProps.post.post_custom_scripts?.json_ld_script && ( <script id="post-json-ld" type="application/ld+json" dangerouslySetInnerHTML={{ __html: postProps.post.post_custom_scripts?.json_ld_script ?? "", }} /> )} {/* body start scripts */} {isTeamPlan && postProps.post.post_custom_scripts?.body_start_script && ( <> {parseScripts(postProps.post.post_custom_scripts.body_start_script)} </> )} <PostPage {...postProps} logId={logId} children={serverContent} /> {/* body end scripts */} {isTeamPlan && postProps.post.post_custom_scripts?.body_end_script && ( <>{parseScripts(postProps.post.post_custom_scripts.body_end_script)}</> )} </> ); ...
 
  1. client side component 가 mount 시점에서 넘겨 받은 log_id 를 통해 client 정보를 update.
    1.  
      ... useEffect(() => { if (!logId) return; const sessionId = getOrCreateSessionId(); let userId = getCookie("_inblog_user"); if (!userId) { userId = generateUserId(); setCookie("_inblog_user", userId); } const updateLog = async () => { const { data, error } = await supabase .from("logs") .update({ device: isMobile() ? "mobile" : isMobileTablet() ? "tablet" : "desktop", referrer: window.document.referrer ? new URL(window.document.referrer).hostname : "direct", full_referrer: window.document.referrer, is_routing_back: false, session_id: sessionId, user_id: userId, }) .eq("id", logId); }; updateLog(); }, [logId]); ...
 
  1. 버튼 클릭시 마찬가지로 넘겨받은 log_id 를 통해 해당 log 의 is_click 을 true 로 업데이트
    1. const { data, error } = await supabase .from("logs") .update({ is_click: true }) .eq("id", logId);
 
💡
  • 서버에 요청이 들어올 때 마다 어떤 블로그 어떤 포스트에 요청이 들어왔는지 먼저 기록하고, 기록한 값을 페이지에 서명해서 클라이언트에 전달해주는 느낌
  • 서버에 해당 페이지에 url 로 들어오는 요청을 큰 변수 없이 catch 해서 기록 할 수 있음
  • logId 값을 서버에서 전달한 상태로 page 를 내려주기 때문에, client-side 렌더링 방식과 달리 뒤로가기 등을 통해 다시 페이지로 돌아가더라도 해당 log 는 특정해서 기록할 수 있음
  • 뒤로가기 케이스는 한 번 렌더링 된 페이지에 대해서 브라우저가 서버에 다시 url를 호출하지 않기 때문에 자동으로 기록되지 않음

중요! 서버 사이드 로깅을 채택하지 못한 이유

🔥
Next js <Link/> 를 사용하기 때문에, 실제로 방문하지 않은 페이지에 대하여 Next js 가 preload 하는 과정에서 log 가 serverside 에서 기록됨
notion image
방문한 위의 post_id 39981 페이지의 경우
  1. 서버에서 post_id 39981 로그 생성 (클라이언트 정보는 빈 상태)
  1. client side 에서 39981 포스트 컴포넌트의 mount 와 동시에 session_id, user_id 등 클라이언트 정보 로그에 업데이트
  1. hydration 이후 view port 내에 있는 author (inblog 팀 프로필), more-articles 의 포스트들 (오른쪽 2개) 에 대해 Next js Link 가 미리 정보를 호출
  1. 서버 사이드에서는 받은 콜에 의해 방문하지도 않은 3 페이지에 대해 빈로그 생성
notion image
session_id, user_id 가 빈 로그는 실제로 방문하지 않은 페이지라고 판단할 수는 있으나, Link 특성상 불필요한 빈 로그가 너무 많이 생성될 수 있음.
⇒ 배포 1주일 앞둔 시점에서 서버 사이드 로깅 채택하기 어려운 가장 큰 사유
 

2. 클라이언트 사이드에서 로깅하는 방법

 
  1. layout.tsx 상단에 <Script/> logging.js 스크립트 추가 (/public/logging.js)
    1. (function () { const LOG_EVENT_NAME = "inblog-log-event"; let currentPath = null; let isRoutingBack = false; let isLogging = false; let isInitialized = false; window.addEventListener("inblog-log-event", async (e) => { if (!isInitialized) return; const searchParams = new URLSearchParams(window.location.search); const trafficType = searchParams.get("traffic_type"); if (trafficType === "internal") return; if (isLogging) return; isLogging = true; const isForClick = e.detail.is_click; if (isForClick) { fetch("/api/log-click", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ session_id: e.detail.session_id, post_id: e.detail.post_id, }), }); } else { // Add a 50ms delay await new Promise((resolve) => setTimeout(resolve, 50)); // Fetch the log view API fetch("/api/log-view", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ ...e.detail, is_routing_back: isRoutingBack, }), }); } isLogging = false; }); // Listen for browser back/forward button usage window.addEventListener("popstate", () => { isRoutingBack = true; }); // Handle route changes (pushState / replaceState) function handleRouteChange() { const newPath = window.location.pathname; // Store the updated current path currentPath = newPath; isLogging = false; isRoutingBack = false; logId = null; } // Wrap native history methods to detect route changes function rewriteHistoryMethod(methodName) { const original = history[methodName]; return function (...args) { original.apply(this, args); handleRouteChange(); }; } history.pushState = rewriteHistoryMethod("pushState"); history.replaceState = rewriteHistoryMethod("replaceState"); // Run once on initial load handleRouteChange(); window.dispatchEvent(new Event("inblog-initial-log-event")); isInitialized = true; })();
       
      스크립트의 역할
      • window.addEventListener “popstate” 를 통해 뒤로가기 event 를 구분
      • post, blog 페이지의 post_id, blog_id 값이 서버에서 받아진 시점을 “inblog-log-event” listen 하고 “popstate” 상태와 결합하여 로깅할지 여부를 결정
      • “/api/log-click” , "/api/log-view" api 를 호출하여 supabase 로깅 로직 실행
      • 플로우
        • routing 시 isLogging, isRoutingBack(뒤로가기 여부) 초기화
        • "inblog-log-event" 이벤트를 listen 하여 post, blog 페이지가 서버에서 받은 post_id, blog_id 값을 event detail로 받음
        • "inblog-log-event" 이벤트 catch 이후 50ms 버퍼 타임을 두고, “popstate” 이벤트가 정상적으로 받아졌음을 가정.
        • “popstate” 여부와 event로부터 받은 post, blog id 값, 추가적인 client,user,session 정보를 /api/log-view" api 를 호출하여 supabase 로깅 로직 실행
        • 같은 페이지 상에서 유저가 cta 클릭시, is_click = true 인 "inblog-log-event" 이벤트 catch, “/api/log-click” 을 통해 클릭 이벤트 기록
       
  1. 클라이언트 컴포넌트에 사용되는 useLogView hook
    1. logIdRef“inblog-initial-log-event” listener 가 있는 이유
      <Script src={`${INBLOG_URL}/logging.js`} strategy="afterInteractive" />
      layout.tsx 의 Script code 가 strategy="afterInteractive" (beforeInteractive 는 hydration 에러 발생) 인데, 가장 첫 페이지 렌딩 시 client component 에서 dispatch 하는 window.dispatchEvent( new CustomEvent("inblog-log-event",
      시점에 script 의 listener 가 init 되지 않음. 스크립트가 실행되고 1회만 window.dispatchEvent(new Event("inblog-initial-log-event"));
      하여 client component 에서 로깅하도록 처리
      import { useEffect, useRef, useState } from "react"; import supabase from "@/lib/supabase"; import { useInterval } from "react-use"; import { v4 as uuidv4 } from "uuid"; export type LogType = "home" | "post" | "author" | "category"; export const LOGGING_EVENT_NAME = "inblog-log-event"; export function useLogView({ blogId, postId, authorId, tagId, logType, }: { blogId: number; postId?: number; authorId?: string; tagId?: number; logType: LogType; }) { const [isLogging, setIsLogging] = useState(false); const logIdRef = useRef<number | null>(null); const logPostCTAClickEvent = async () => { if (logType !== "post" || !postId) return; const userId = getCookie("_inblog_user"); const sessionId = sessionStorage.getItem("inblog-session-id"); if (!userId || !sessionId) return; if (logIdRef.current) { const { data, error } = await supabase .from("logs") .update({ is_click: true }) .eq("id", logIdRef.current); if (error) console.error("Error logging post CTA click:", error); } else { window.dispatchEvent( new CustomEvent("inblog-log-event", { detail: { is_click: true, session_id: sessionId, post_id: postId, }, }) ); } }; useEffect(() => { const handleLogEvent = async () => { if ( !TEST_BLOG_IDS.includes(blogId) && process.env.NEXT_PUBLIC_VERCEL_ENV !== "production" ) { console.log("Skipping logging because we're not in production"); return; } if (typeof window === "undefined") return; if (isLogging) return; setIsLogging(true); const sessionId = getOrCreateSessionId(); let userId = getCookie("_inblog_user"); if (!userId) { userId = generateUserId(); setCookie("_inblog_user", userId); } // Insert log into Supabase const { data, error } = await supabase .from("logs") .insert({ blog_id: blogId, post_id: postId ?? null, session_id: sessionId, user_id: userId, created_at: new Date().toISOString(), is_click: false, device: isMobile() ? "mobile" : isMobileTablet() ? "tablet" : "desktop", referrer: window.document.referrer ? new URL(window.document.referrer).hostname : "direct", full_referrer: window.document.referrer, log_type: logType, author_uuid: authorId ?? null, tag_id: tagId ?? null, is_routing_back: false, }) .select() .single(); if (error) { console.error("Error inserting log:", error); return; } logIdRef.current = data.id; }; // Add event listener window.addEventListener("inblog-initial-log-event", handleLogEvent); return () => { window.removeEventListener("inblog-initial-log-event", handleLogEvent); }; }, []); useEffect(() => { if (typeof window === "undefined") return; if (isLogging) return; console.log("dispatching event"); setIsLogging(true); let userId = getCookie("_inblog_user"); if (!userId) { userId = generateUserId(); setCookie("_inblog_user", userId); } window.dispatchEvent( new CustomEvent("inblog-log-event", { detail: { blogId, postId, sessionId: getOrCreateSessionId(), userId, created_at: new Date().toISOString(), is_click: false, device: isMobile() ? "mobile" : isMobileTablet() ? "tablet" : "desktop", referrer: window.document.referrer ? new URL(window.document.referrer).hostname : "direct", full_referrer: window.document.referrer, log_type: logType, author_uuid: authorId ?? null, tag_id: tagId ?? null, }, }) ); }, [postId, blogId]); return { logPostCTAClickEvent, }; } function getCookie(name: string): string | null { const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`)); return match ? decodeURIComponent(match[2]) : null; } function setCookie(name: string, value: string, days = 365) { // Default 365 days const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); const expires = `; expires=${date.toUTCString()}`; document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`; } // -- Generate a basic random user ID, e.g. for first-time visitors -- function generateUserId() { return `_inblog_user_${Math.random().toString(36).substring(2)}`; } const isMobile = () => { let check = false; (function (a) { if ( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( a ) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( a.substr(0, 4) ) ) check = true; })(window.navigator.userAgent); return check; }; const isMobileTablet = () => { let check = false; (function (a) { if ( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( a ) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( a.substr(0, 4) ) ) check = true; })(window.navigator.userAgent); return check; }; function generateSessionId(): string { return uuidv4(); } function getOrCreateSessionId() { let sessionId = sessionStorage.getItem("inblog-session-id"); if (!sessionId) { sessionId = generateSessionId(); sessionStorage.setItem("inblog-session-id", sessionId); } return sessionId; }
  1. 뒤로 가기 기록과 버튼 클릭 처리 방법
      • 일반적인 정상 routing 과 뒤로가기/앞으로가기 버튼 routing 모두 log 에 기록
      • 뒤로가기/앞으로가기 routing 은 is_routing_back 은 true 로 구분 (page view 기반 pricing 용 count 에서 제외)
      • A→B 이동 후, 뒤로가기 버튼으로 A 페이지로 돌아간 경우
        • post_id: a, is_routing_back: false, is_click: false
        • post_id: b, is_routing_back: false, is_click: false
        • post_id: a, is_routing_back: true, is_click: false
      • CTA 버튼 클릭 처리 방법
        • CTA 클릭 시 event 로 post_id 와 session_id 전달
          • DECLARE current_log RECORD; prev_log RECORD; BEGIN -- 1. Fetch the latest log row (by highest id) for the given session_id and post_id, -- but only select the columns we need. SELECT id, is_routing_back, is_click INTO current_log FROM logs WHERE session_id = p_session_id AND post_id = p_post_id ORDER BY id DESC LIMIT 1; IF NOT FOUND THEN RAISE EXCEPTION 'No log record found for session_id: %, post_id: %', p_session_id, p_post_id; END IF; -- 2. If the current_log is already clicked, return early. IF current_log.is_click THEN RETURN; END IF; -- 3. If is_routing_back is false, mark the current log as clicked and return. IF current_log.is_routing_back = false THEN UPDATE logs SET is_click = true WHERE id = current_log.id; RETURN; END IF; -- 4. If is_routing_back is true: -- (a) Mark the current log as clicked. UPDATE logs SET is_click = true WHERE id = current_log.id; -- (b) Find the most recent previous log (by id < current_log.id) where: -- - is_routing_back = false -- - is_click = false or is_click IS NULL -- (and has the same session_id, post_id). -- Only select the 'id' of that log, as that’s all we need for the update. SELECT id INTO prev_log FROM logs WHERE session_id = p_session_id AND post_id = p_post_id AND is_routing_back = false AND (is_click = false OR is_click IS NULL) AND id < current_log.id ORDER BY id DESC LIMIT 1; -- 5. If no matching previous log is found, just return. IF NOT FOUND THEN RETURN; END IF; -- 6. Otherwise, mark that previous log as clicked and set click_by_back to the current log's id. UPDATE logs SET is_click = true, click_by_back = current_log.id WHERE id = prev_log.id; END;
        • session_id 와 post_id 기반으로 현재 버튼을 클릭한 log 의 row 구함
        • 해당 log 의 is_routing_back 이 false 인 경우, 정상적인 방문이므로 is_click 을 true 로 update
        • 해당 log 의 is_routing_back 이 true 인 경우, 뒤로가기 방문에서 버튼을 누르는 경우로 페이지 뷰 수 대비 중복 클릭이 발생할 수 잇음
          • notion image
          • 예시 케이스: 홈 → 39981 포스트 → 39773 포스트 → 뒤로가기 버튼 클릭으로 39981 포스트 복귀 → CTA 클릭
          • 홈 화면 방문 로그 생성 (4546145)
          • 39981 포스트 방문 로그 생성 (4546151), is_click false / is_routing_back false
          • 39773 포스트 방문 로그 생성 (4546152)
          • 39981 포스트 방문 로그 생성 (4546157), is_click false / is_routing_back true
          • CTA 버튼 클릭시, 로그의 투명성을 최대화 하기 위해 우선 뒤로가기로 생성된 4546157 로그의 is_click true 로 수정
          • 같은 세션의 같은 post_id 방문한 log 중 is_click false / is_routing_back false 인 가장 최신 로그를 찾고,
            • 해당 log 의 is_click true 로 수정, 동시에 click_by_back 의 id 값에 4546157 값을 넣어 출처를 기록
              ⇒ 뒤로 가기 로그에서의 클릭을 기록함과 동시에, 일반적인 방문을 통한 로그의 클릭을 수정해주며 출처를 기록
             
            💡
            위의 방법을 사용한 이유
            1. CTR 계산을 위해 click 이벤트의 수가 is_routing_back 이 true 가 아닌 (정상적인 페이지 방문) 페이지 뷰 수를 넘게 할 수 없음
            1. 뒤로가기를 통해 click 을 하여 이벤트가 무분별하게 기록될 시 ctr 100% 를 넘어갈 수 있기에, 별도의 처리가 필요하나 함부로 생략/가공 시 black box 이슈가 생김으로 로그를 최대한 있는 그대로 남기면서 ctr 100% 넘지 않는 처리가 필요함
            1. 따라서 뒤로 가기 클릭을 통해 click 한 경우 우선은 해당 뒤로가기 방문 로그에 is_click 을 true 로 하여 기록은 남김
            1. 뒤로가기를 통해 버튼을 클릭했다면, 이는 원래 이전에 정상 방문한 페이지 중 하나에서 클릭을 했다는 의미
            1. 따라서 정상 방문한 페이지 (is_routing_back 이 true 가 아닌) 중 CTA 를 클릭하지 않은 (is_click 이 true 가 아닌) 로그를 찾아서 그 페이지에서 클릭을 했을 것이다로 처리.
            1. 대신 기록을 정확하게 남기기 위해 click_by_back 에 뒤로가기를 통해 방문한 로그의 id를 기록하여 향후 유기적으로 판단 가능할 수 있도록 처리
            💡
            위 방법의 장점
            1. User journey 를 보여줘야할 경우 페이지 방문과 뒤로가기/앞으로가기를 통해 방문한 케이스를 다 보여줄 수 있음
            1. 뒤로가기/앞으로가기 하여 버튼을 클릭했다는 세부 정보도 알려줄 수 있음
            1. CTR 계산 시 추가로 복잡한 처리나 가공 없이 기존 방식 그대로 is_routing_back 이 true 가 아닌 페이지뷰와 is_click 값을 찾아서 계산해줄 수 있음
Share article

juhoblog