diff --git a/client/index.html b/client/index.html index 9b7b8a5a..c942e856 100644 --- a/client/index.html +++ b/client/index.html @@ -11,6 +11,21 @@ + + + + + + + <%= process.env.META_PIXEL_CODE %> +
diff --git a/client/package-lock.json b/client/package-lock.json index c7177704..be3aad1a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -32,6 +32,7 @@ "@storybook/react-webpack5": "^7.4.5", "@storybook/testing-library": "0.2.1", "@types/google.maps": "^3.54.0", + "@types/gtag.js": "^0.0.16", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -6738,6 +6739,12 @@ "@types/node": "*" } }, + "node_modules/@types/gtag.js": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.16.tgz", + "integrity": "sha512-TefTCQWiQYc1F7zF7KKLOd5E645me+9CjfPwJLNxWxaTK3ITKmCxtHRVH7EhSo1dl+sVGmu2h2htCOZrjWQRrQ==", + "dev": true + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", diff --git a/client/package.json b/client/package.json index 940ad104..1bf752dc 100644 --- a/client/package.json +++ b/client/package.json @@ -42,6 +42,7 @@ "@storybook/react-webpack5": "^7.4.5", "@storybook/testing-library": "0.2.1", "@types/google.maps": "^3.54.0", + "@types/gtag.js": "^0.0.16", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/client/src/components/CafeCard.tsx b/client/src/components/CafeCard.tsx index 4993c613..c40e6fc3 100644 --- a/client/src/components/CafeCard.tsx +++ b/client/src/components/CafeCard.tsx @@ -9,6 +9,7 @@ import useCafeLikes from '../hooks/useCafeLikes'; import useClipboard from '../hooks/useClipboard'; import useUser from '../hooks/useUser'; import type { Cafe } from '../types'; +import { withGAEvent } from '../utils/GoogleAnalytics'; import Resource from '../utils/Resource'; import CafeDetailBottomSheet from './CafeDetailBottomSheet'; import CafeMenuBottomSheet from './CafeMenuBottomSheet'; @@ -42,34 +43,34 @@ const CafeCard = (props: CardProps) => { setCurrentImageIndex(index); }, []); - const handleShare = async () => { + const handleShare = withGAEvent('share', { cafeName: cafe.name }, async () => { try { await clipboard.copyToClipboard(`https://yozm.cafe/cafes/${cafe.id}`); showToast('success', 'URL이 복사되었습니다!'); } catch (error) { showToast('error', `URL 복사 실패: ${error}`); } - }; + }); - const handleLikeCountIncrease = () => { + const handleLikeCountIncrease = withGAEvent('click_like_button', { cafeName: cafe.name }, () => { if (!user) { showToast('error', '로그인이 필요합니다!'); return; } setLiked({ isLiked: !isLiked }); - }; + }); - const handleDetailOpen = () => { + const handleDetailOpen = withGAEvent('click_detail_button', { cafeName: cafe.name }, () => { setIsShowDetail(true); - }; + }); const handleDetailClose = () => { setIsShowDetail(false); }; - const handleMenuOpen = () => { + const handleMenuOpen = withGAEvent('click_menu_button', { cafeName: cafe.name }, () => { setIsMenuOpened(true); - }; + }); const handleMenuClose = () => { setIsMenuOpened(false); diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index 38c3f510..24daab61 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import CafeCard from '../components/CafeCard'; import ScrollSnap from '../components/ScrollSnap'; import useCafes from '../hooks/useCafes'; import type { Cafe } from '../types'; +import { withGAEvent } from '../utils/GoogleAnalytics'; import { easeOutExpo } from '../utils/timingFunctions'; const PREFETCH_OFFSET = 2; @@ -24,6 +25,8 @@ const HomePage = () => { const itemRenderer = (cafe: Cafe) => ; + useEffect(withGAEvent('cafe_view', { cafeName: cafes[activeIndex].name }), [activeIndex]); + return ( { + const { identity } = useAuth(); useSilentLink(); + useEffect(() => { + if (identity) { + setGAConfig({ + user_id: identity.sub, + }); + } + }, [identity]); + return ( <> diff --git a/client/src/utils/GoogleAnalytics.ts b/client/src/utils/GoogleAnalytics.ts new file mode 100644 index 00000000..b051362d --- /dev/null +++ b/client/src/utils/GoogleAnalytics.ts @@ -0,0 +1,62 @@ +/// + +declare global { + interface Window { + dataLayer: unknown[]; + } +} + +const ID = process.env.GOOGLE_ANALYTICS_TRACKING_ID ?? null; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +type GAEvent = + | { name: 'share'; params: { cafeName: string } } + | { name: 'click_like_button'; params: { cafeName: string } } + | { name: 'click_menu_button'; params: { cafeName: string } } + | { name: 'click_detail_button'; params: { cafeName: string } } + | { name: 'cafe_view'; params: { cafeName: string } }; + +type GAConfig = { + user_id: string; +}; + +export function withGAEvent( + ...args: Extract extends { params: infer Params extends Record } + ? [event: EventName, params: Params] + : [event: EventName] +): () => void; + +export function withGAEvent( + ...args: Extract extends { params: infer Params extends Record } + ? [event: EventName, params: Params, fn: (...args: Args) => ReturnType] + : [event: EventName, fn: (...args: Args) => ReturnType] +): (...args: Args) => ReturnType; + +/** + * Google Analytics 이벤트를 발생시킬 때 사용하는 함수 + * + * 원본 함수를 마지막 인자로 넣어 이벤트 함수와 결합하여 사용 가능합니다 + */ +export function withGAEvent( + ...args: Extract extends { params: infer Params extends Record } + ? [event: EventName, params: Params] | [event: EventName, params: Params, fn: (...args: Args) => ReturnType] + : [event: EventName] | [event: EventName, fn: (...args: Args) => ReturnType] +) { + const [arg1, arg2, arg3] = args; + const event = arg1; + const params = typeof arg2 === 'object' ? arg2 : {}; + const fn = arg3 ?? noop; + + return (...args: Args) => { + window.gtag('event', event, params); + + return fn(...args); + }; +} + +export function setGAConfig(config: Partial) { + if (!ID) return; + window.gtag('config', ID, config); +}