์ด์ ๊ธ ์์ sentry๋ฅผ ํ๋ก ํธ์๋ ์๋ฒ์ ์ฐ๋ ์๋ฃํ์๋ค. ํ์ง๋ง ์ด๋ค ์ ์ ๊ฐ ๋ฐ์ํ ์ค๋ฅ์ธ์ง ํ์ธํ๊ธฐ๋ ์ด๋ ค์ ๊ณ ์์ค๋งต ์ฐ๋๋ ์ ๋๋ก ๋์ด์์ง ์์์ ์ด๋ค ์ฝ๋์์ ๋ฐ์ํ ์๋ฌ์ธ์ง ํ ๋์ ์์๋ณผ ์ ์๋ ์ํฉ์ ์ค๋ฅ๋ฅผ ํธ๋ ์ด์คํ๊ธฐ ์ด๋ ค์ด ๋ฌธ์ ์ ์ ๊ฐ์ง๊ณ ์์๋ค.
๋ํ, ํ์ฌ ์๋น์ค๊ฐ ๋ฆด๋ฆฌ์ฆ ๋์ด์๋ ํ์ฌ ์์ ์ ์ฌ์ฉ์๊ฐ ์ด๋ค ํ์ด์ง(๊ธฐ๋ฅ๋จ์์ ํ์ด์ง๋ก ๊ตฌ์ฑ๋์ด ์๋ค.)๋ฅผ ์์ฃผ ๋ค์ด๊ฐ๋์ง ์ ์ ์๋ ๋ฐฉ๋ฒ์ด ์์ผ๋ฉด ์ข๊ฒ ๋ค๋ ๋ด๋ถ์ ์๊ฒฌ๋ ์กด์ฌํ๋ค. ๊ทธ๋์ ๋๋ 3๊ฐ์ง์ ๋ํด์ ์ผ๋จ ๊ณ ๋ํ๋ฅผ ์งํํ์๋ค.
1. ์ด๋ค ์ฌ์ฉ์๋ก๋ถํฐ ๋น๋กฏ๋ ์ค๋ฅ์ธ๊ฐ
2. ์ด๋ค ์์ค์ฝ๋๋ก๋ถํฐ ํธ๋ ์ด์ค ๋ ์ค๋ฅ์ธ๊ฐ
3. ์ด๋ค ํ์ด์ง๋ฅผ ์ฌ์ฉ์๊ฐ ๋ง์ด ์ ๊ทผํ๋๊ฐ
์ด๋ค ์ฌ์ฉ์๋ก๋ถํฐ ๋น๋กฏ๋ ์ค๋ฅ์ธ๊ฐ
Sentry์์๋ ์๋ฌ ์ด๋ฒคํธ์ ์ฌ์ฉ์ ์ ๋ณด(Context) ๋ฅผ ํจ๊ป ๋ถ์ฌ์ ์ ์กํ ์ ์๋ค.
๊ฐ์ฅ ๊ธฐ๋ณธ์ด ๋๋ API๊ฐ ๋ฐ๋ก setUser์ด๋ค.
import * as Sentry from '@sentry/browser'; // ๋๋ @sentry/nextjs, @sentry/react ๋ฑ
// ๋ก๊ทธ์ธ ์งํ ํน์ ์ ์ ์ ๋ณด๊ฐ ์ค๋น๋ ์์
Sentry.setUser({
id: user.id, // ๋ด๋ถ ์ ์ ID
email: user.email, // (์ ํ) ์ด๋ฉ์ผ
username: user.name, // (์ ํ) ์ด๋ฆ/๋๋ค์
});


ํด๋น Context๋ก ์ด๋ค ์ ์ ์๊ฒ์ ๋ฐ์ํ๊ฑด์ง,
ํน์ ์ฌ์ฉ์์๊ฒ์๋ง ๋ฐ์ํ๋ ๊ฑด์ง ๋ฑ ์ค๋ฅ ์ญ์ถ์ ์ ๋์์ ์ค ์ ์๊ฒ ๋์๋ค.
์ด๋ค ์์ค์ฝ๋๋ก๋ถํฐ ํธ๋ ์ด์ค ๋ ์ค๋ฅ์ธ๊ฐ
1. React Component Name ์ถ์
์ด ํ์ด์ง์์ ์ฌ์ฉ๋ ์ด๋ค React ์ปดํฌ๋ํธ์์ ๋ฌธ์ ๊ฐ ํฐ์ง ๊ฑด์ง, ๊ทธ ์ปดํฌ๋ํธ๊ฐ ์ด๋ค ๋ถ๋ชจ ์ปดํฌ๋ํธ ํธ๋ฆฌ ์์ ์์๋์ง ์ถ์ ํ๋ ๊ฒ๋ sentry์์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
React Component Annotation ์ ์ ๊ณตํ๊ณ ์๊ณ , Next.js์ฉ Sentry SDK์์๋ reactComponentAnnotation ์ต์ ์ผ๋ก ํ์ฑํํ ์ ์๋ค.
https://docs.sentry.io/platforms/javascript/guides/react/features/component-names/
์ ์ฉ ๋ฐฉ๋ฒ
Next.js์ withSentryConfig ํธ์ถ๋ถ์์ ์ต์ ๋ง ์ผ์ฃผ๋ฉด ๋๋ค.
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
const nextConfig = {
// ... ๊ธฐ์กด ์ค์
};
module.exports = withSentryConfig(
nextConfig,
{
// Sentry Webpack Plugin ์ต์
reactComponentAnnotation: {
enabled: true,
},
// ... ๊ทธ ๋ฐ์ ์ต์
},
);
2. Source Map ์ง์
ํ๋ก ํธ์๋ ์ฝ๋๋ ๋น๋ ๊ณผ์ ์์ ๋ฒ๋ค๋ง/์์ถ์ด ์ด๋ฃจ์ด์ง๊ธฐ ๋๋ฌธ์, Sentry์ ๋ฐ๋ก ์ฐ๋ํ๋ฉด ์คํ ํธ๋ ์ด์ค์ ๋๋ต ๋ค์๊ณผ ๊ฐ์ด ํ์๋๋ค.
- app-1234abcd.js:1:12345
- vendor-9876efgh.js:1:67890
์ด ์ ๋ณด๋ง์ผ๋ก๋ ์ด๋ค ํ์ผ์ ๋ช ๋ฒ์งธ ์ค์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋์ง ์๊ธฐ ์ด๋ ต๋ค. ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Source Map ์ง์์ ์ถ๊ฐํ๋ค.
Source Map ์ง์์ ํ๊ฒ ๋๋ฉด ๋น๋๋ JS ์ฝ๋๊ฐ ์๋ ์๋ณธ ์ฝ๋ ๊ธฐ์ค์ ์๋ฌ ์์น ์ถ์ ๊ฐ๋ฅํ๋ค.

์ด๊ธฐ์ ์ค๋ฅ๋ฅผ ๋ฐ์ ์ํค๋ฉด ๋ค์๊ณผ ๊ฐ์ด path๊ฐ ํธ๋ ์ด์ค ๋์ด ์ด๋์์ ์ค๋ฅ๊ฐ ๋ ๊ฒ์ธ์ง ์ ์ ์๋ค. ์ ํํ ์์น๋ฅผ ์๊ธฐ๋ ์ด๋ ต๋ค.
์ ์ฉ ๋ฐฉ๋ฒ - ๋๋ถ๋ถ ๊ทผ๋ณธ์ ์ผ๋ก ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
nextConfig,
{
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
},
);
๋์ ์ ์ฉ ๋ฐฉ๋ฒ
์์ฝ๊ฒ๋.. ๋์ ๊ฒฝ์ฐ ํด๋น config ์ต์ ์ ์ผ๋ ๊ฒ์ผ๋ก ์ฐ๋์ด ๋์ง ์์๋ค...
Saas ๋ฒ์ ์ด ์๋๋ผ์ ๊ทธ๋ฐ์ง๋ ๋ชจ๋ฅด๊ฒ ์ผ๋,,, ๋ค๋ฅธ ๋ฐฉ์๋ค์ ์ฐพ์์ ์ ์ฉํ์๋ค.

์ผ๋จ ์์ ์์ค๋งต์ด ์ฐ๋๋์ง ์์ ์ฌ์ง์์ ๋ณด์ด๋ unminify code ๋ฒํผ์ ํด๋ฆญํ์ ๋ ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ง์ฃผํ๋ค.
Missing source file with a matching Debug ID
No Artifacts With Debug IDs Uploaded
๋ฌธ์ ์์ธ
๋ฒ๋ค์๋ Debug ID๊ฐ ์๋๋ฐ, ๊ทธ Debug ID์ ๋งค์นญ๋๋ ์์ค๋งต์ด sentry์ ์ ๋ก๋๊ฐ ๋์ง ์์ ๋ฐ์ํ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
ํด๊ฒฐ ํฌ์ธํธ
๋น๋ ์์ ์ Sentry Webpack ํ๋ฌ๊ทธ์ธ์ด ์คํ๋์ด Debug ID๋ฅผ ์ฃผ์ ํ๊ณ ์ ๋ก๋ ์ํจ๋ค.

Debug ID ๋งค์นญ๊ณผ ํ์ผ ์ ๋ก๋์ ๋ํด ๋ค์ํ ์๋๋ค์ ํ์๋ค...
ํ ๊ธ์ ํผ์ณ์ ์ ๋ด์ฉ๋ค์ ๋ค ์ ๋ฆฌํ๊ธฐ์.. ๋๋ฌด๋ ๋ฐฉ๋ํด์.... ๊ฒฐ๋ก ์ ์ธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋ํด์๋ง ์ ๋ฆฌํด๋ณด๋ ค ํ๋ค.
on-Premise ํ๊ฒฝ์ sentry์์ sourcemap ์ฐ๋ํ ๋ฐฉ๋ฒ
1. sentryWebpackPlugin ์ค์น
https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/webpack/#manual-setup
Webpack | Sentry for JavaScript
Upload your source maps with our webpack plugin.
docs.sentry.io
# npm
npm install @sentry/webpack-plugin --save-dev
# yarn
yarn add @sentry/webpack-plugin --dev
# pnpm
pnpm add @sentry/webpack-plugin --save-dev
sentryWebpackPlugin์ ์ค์นํด์ค๋ค.
2. next.config.js ์ค์ ํ๊ธฐ
์ฌ๊ธฐ์ ํฌ์ธํธ๋ productionBrowserSourceMaps ์ต์ ์ true๋ก ์ค์ ํ, sentryWebpackPlugin์ ํตํด ์์ค๋งต์ ์์ฑํ์ฌ ์ ๋ก๋ ํ๋ ๊ฒ์ด๋ค.
productionBrowserSourceMaps๋ ๋ฌด์์ธ๊ฐ?
- next.js ๊ณต์ ๋ฌธ์
Next.js๋ ํ๋ก๋์ ๋น๋ ์ค์ ๋ธ๋ผ์ฐ์ ์์ค ๋งต ์์ฑ์ ํ์ฑํํ ์ ์๋ ๊ตฌ์ฑ ํ๋๊ทธ๋ฅผ ์ ๊ณตํ๋ค.
productionBrowserSourceMaps ์ต์ ์ด ํ์ฑํ๋๋ฉด ์์ค ๋งต์ JavaScript ํ์ผ๊ณผ ๋์ผํ ๋๋ ํ ๋ฆฌ์ ์ถ๋ ฅ๋๋ค.
์์ค๋งต์ ๋ํ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๋ฐ๋ฉด์ ๋ค์๊ณผ ๊ฐ์ trade off๊ฐ ๋ฐ์ํ๋ ์ฐธ๊ณ ํ ๊ฒ!
- ์์ค ๋งต์ ์ถ๊ฐํ๋ฉด next build ์๊ฐ์ด ์ฆ๊ฐํ ์ ์๋ค.
- next build ์ค ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ฆ๊ฐํ๋ค.
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
const isProd = process.env.NODE_ENV === 'production';
const nextConfig: NextConfig = {
... next ์ค์
// SOURCE MAP ์์ฑ์ ์ํ ์์ค๋งต ์์ฑ
productionBrowserSourceMaps: true,
webpack(config, { isServer }) {
if (!isServer) {
config.devtool = 'source-map';
}
if (isProd) {
config.plugins.push(
sentryWebpackPlugin({
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
url: process.env.NEXT_PUBLIC_SENTRY_URL,
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
assets: [
'.next/static/chunks/**/*.{js,map}',
'.next/static/css/**/*.{js,map}',
'.next/server/**/*.{js,map}',
],
ignore: [
'**/node_modules/**',
'**/*.br',
'**/*.gz',
'**/*.txt',
'**/*.html',
'**/*.wasm',
'**/*.ts',
'**/*.tsx',
'**/*.json',
'**/*.png',
'**/*.jpg',
'**/*.svg',
'**/*.webp',
'**/*.css.map',
],
},
}),
);
}
return config;
},
};
// Production ํ๊ฒฝ์์๋ง Sentry ์ค์ ์ ์ฉ
const config =
process.env.NODE_ENV === 'production'
? withSentryConfig(nextConfig, {
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
sentryUrl: process.env.NEXT_PUBLIC_SENTRY_URL,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI,
disableLogger: true,
automaticVercelMonitors: true,
reactComponentAnnotation: {
enabled: true,
},
})
: nextConfig;
export default config;
โฐ๏ธ ์์ค๋งต ์ ๋ก๋ ์ค ๋ฐ์ํ ์ ์๋ ๋ฌธ์ (feat. status 413 - Content Too Large)
HTTP 413 Content Too Large ์๋ต ์ํ ์ฝ๋๋ ์์ฒญ ์ํฐํฐ๊ฐ ์๋ฒ์ ์ํด ์ ์๋ ์ ํ๋ณด๋ค ํฌ๋ค๋ ๊ฒ์ ๋ํ๋ธ๋ค. ์๋ฒ๋ ์ฐ๊ฒฐ์ ๋ซ๊ฑฐ๋ Retry-After ํค๋ ํ๋๋ฅผ ๋ฐํํ ์ ์๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ์ bodySize์ time out์ ๋๋ฆฌ๋ ๋ฐฉ๋ฒ์ด๋ค.
nginx์๋ฒ ๊ธฐ์ค์ผ๋ก client_max_body_size์ proxy_read_timeout, proxy_send_timeout์ ์ง์ ํ์ฌ ํด๊ฒฐํ๋ค.
์ฐ๋ฆฌ ์๋ฒ์ ๊ฒฝ์ฐ nginx ๊ธฐ๋ฐ์ด์๋๋ฐ ํธ์คํธ ์๋ฒ์ nginx.conf์ sentry server์ docker compose์ ์์ฑ๋ nginx.conf ์ค์ ์ ๋ค์๊ณผ ๊ฐ์ด ๋ฐ๊พธ์ด ์ด๋ฅผ ํด๊ฒฐํ์๋ค.
# host server์ nginx.conf
# sentry
server {
listen 9000 ssl;
server_name localhost;
# 413 ์๋ฌ๋ฅผ ์์ ํ๊ธฐ ์ํด ์ถ๊ฐ ํ์
client_max_body_size 200m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
... ๋ค๋ฅธ ์ค์ ๋ค
}
///self-hosted sentry docker compose nginx.conf
upstream relay {
server relay:3000;
keepalive 2;
}
upstream sentry {
server web:9000;
keepalive 2;
}
server {
listen 80;
# 413 ์๋ฌ๋ฅผ ์์ ํ๊ธฐ ์ํด ์ถ๊ฐ ํ์
client_max_body_size 200m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
location /api/store/ {
proxy_pass http://relay;
}
location ~ ^/api/[1-9]\d*/ {
proxy_pass http://relay;
}
location ^~ /api/0/relays/ {
proxy_pass http://relay;
}
location ^~ /js-sdk/ {
root /var/www/;
# This value is set to mimic the behavior of the upstream Sentry CDN. For security reasons,
# it is recommended to change this to your Sentry URL (in most cases same as system.url-prefix).
add_header Access-Control-Allow-Origin *;
}
location / {
proxy_pass http://sentry;
}
location /_assets/ {
proxy_pass http://sentry/_static/dist/sentry/;
proxy_hide_header Content-Disposition;
}
location /_static/ {
proxy_pass http://sentry;
proxy_hide_header Content-Disposition;
}
}
์ ์ฉ ๊ฒฐ๊ณผ

์์ค๋งต ์ฐ๋์ ํตํด ์ด๋ค ์ฝ๋์์ ์๋ฌ๋ฅผ ๋์ง๊ฑด์ง ํ๋์ ํ์ธ์ด ๊ฐ๋ฅํด์ก๋ค. ๋ฒ๋ค ํ์ผ ์ด๋ฆ ๋์
src/components/ReferenceList.tsx:45, src/pages/reference/[id].tsx:120 ํํ๋ก ์๋ณธ ํ์ผ + ๋ผ์ธ ๋ฒํธ๊ฐ ๋์จ๋ค.
IDE์์ ํด๋น ํ์ผ/๋ผ์ธ์ ๋ฐ๋ก ์ด์ด๋ณผ ์ ์๊ธฐ ๋๋ฌธ์, ์๋ฌ ๋ถ์ ์๋๊ฐ ๋์ ๋๊ฒ ๋นจ๋ผ์ก๋ค.
์ด๋ค ํ์ด์ง๋ฅผ ์ฌ์ฉ์๊ฐ ๋ง์ด ์ ๊ทผํ๋๊ฐ
* metrics ๋ฐ์ดํฐ ์ปค์คํ
Saas์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด๋, ํ์ฌ ์ค์น ๋ on-premise ๋ฒ์ ์์๋ metrics๋ฅผ ์ง์ํ์ง ์์๋ค.
์ด๋ฒคํธ message ๊ธฐ๋ฐ์ผ๋ก ๋ก๊ทธ ๋ฐ์ดํฐ ํํฐ๋ง์ ํตํด metrics ํ์ธํ์ฌ ๋ชจ๋ํฐ๋ง ๋ก๊ทธ ๊ณ ๋ํ ์์ ์ด๋ค.
์๋ Sentry๋ “์๋ฌ ๋ชจ๋ํฐ๋ง”์ ์ด์ ์ด ๋ง์ถฐ์ง ๋๊ตฌ๋ค.
๋ค๋ง, ์ฐ๋ฆฌ ์๋น์ค ํน์ฑ์ ์ ์
๋์ด ํฌ์ง ์๊ณ ๊ธฐ๋ฅ ๋จ์ ํ์ด์ง ๊ตฌ์กฐ๋ผ์, ๊ฐ๋ฒผ์ด ์์ค์ “ํ์ด์ง ์ ๊ทผ ํต๊ณ”๋ Sentry๋ง์ผ๋ก๋ ์ถฉ๋ถํ ํ์ธํ ์ ์๋ค๊ณ ํ๋จํ๋ค. ๋๋ ๋ ๊ฐ์ง ์ ๋ณด์ ์ง์คํ๋ค.
- ์ด๋ค ํ์ด์ง์์ ์๋ฌ๊ฐ ๋ง์ด ๋ฐ์ํ๋๊ฐ?
- ์ฌ์ฉ์๋ค์ด ํ์ด์ง๋ฅผ ์ผ๋ง๋ ์์ฃผ ๋ฐฉ๋ฌธํ๋๊ฐ?
์ด๋ฅผ ์ํด ํ์ด์ง ์ด๋ ์์ ์ Sentry ํ๊ทธ์ ๋ฉ์์ง๋ฅผ ํจ๊ป ๋จ๊ธฐ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค. (ํ์์ ์ผ๋ก๋ง ์ด์ ํ ์ ๊ฑฐ ์์ ์ด๋ค!)
์ฝ๋ ์ ์ฉ
“์ง๊ธ ์ค์ ์๋น์ค์์ ์ด๋ค ํ์ด์ง๋ฅผ ๋ง์ด ๋ณด๊ณ , ์ด๋ค ๋ชจ๋ฌ/๊ธฐ๋ฅ์ด ์์ฃผ ์ด๋ฆฌ๋์ง ๋๋ต์ด๋ผ๋ ์๊ณ ์ถ๋ค.”
ํ์ค์ผ์ผ Analytics ๋๊ตฌ(GA, Amplitude ๋ฑ)๋ฅผ ๋ถ์ด๊ธฐ๋ณด๋ค๋,์ฐ๋ฆฌ๊ฐ ์ด๋ฏธ ์ฌ์ฉ ์ค์ธ Sentry ์์ ๊ฐ๋จํ ๋ฐฉ๋ฌธ ์งํ๋ง ์น์ด๋ ์ถฉ๋ถํ์ง ์์๊น? ๋ผ๋ ํ๋จ์ ํ๊ณ , ๋ค์ ๋ ๊ฐ์ง๋ฅผ ๊ธฐ์ค์ผ๋ก info ๋ ๋ฒจ ์ด๋ฒคํธ๋ฅผ ์ง์ ์๋ ๋ฐฉ์์ ํํ๋ค.
- ํ์ด์ง ๋ฐฉ๋ฌธ(๋ผ์ฐํฐ ์ด๋)
- ๋ชจ๋ฌ ์คํ
1. ํ์ด์ง ๋ฐฉ๋ฌธ ์ด๋ฒคํธ ๋ก๊น
ํ์ด์ง ๋ฐฉ๋ฌธ์ Next.js์ ๋ผ์ฐํฐ ์ด๋ฒคํธ๋ฅผ ํ์ฉํด ๊ฐ์งํ๋ค. ํต์ฌ ์์ด๋์ด๋ ๊ฐ๋จํ๋ค.
- routeChangeComplete ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค Sentry์ level: 'info' ์ธ ์ด๋ฒคํธ๋ฅผ ๋จ๊ธด๋ค.
- ๋ฉ์์ง์๋ “ํ์ด์ง ๋ฐฉ๋ฌธ: [๋ผ์ฐํธ ํจํด]” ์ ๊ธฐ๋กํ๊ณ ,
- ํ๊ทธ์๋ ๋ผ์ฐํธ ํจํด(route) + ์ค์ path(path) ๋ฅผ ํจ๊ป ๋จ๊ธด๋ค.
// app.tsx (๋๋ _app.tsx)
useEffect(() => {
const sendPageView = (url?: string) => {
try {
const routePattern =
(router as any)?.router?.pathname ??
(router as any)?.router?.route ??
'';
const path =
typeof window !== 'undefined'
? (url ?? window.location.pathname + (window.location.search || ''))
: '';
Sentry.captureEvent({
message: `ํ์ด์ง ๋ฐฉ๋ฌธ: ${routePattern}`,
level: 'info',
tags: {
route: routePattern || '(unknown)', // Next.js ๋ผ์ฐํธ ํจํด (/projects/[id] ๋ฑ)
path, // ์ค์ path (/projects/123?tab=info ๋ฑ)
},
});
} catch (err) {
Sentry.captureException(err);
}
};
const handleRouteDone = (url: string) => sendPageView(url);
router.events.on('routeChangeComplete', handleRouteDone);
// ์ฒซ ๋ก๋
sendPageView();
return () => {
router.events.off('routeChangeComplete', handleRouteDone);
};
}, []);
์ฌ๊ธฐ์ route vs path ๋ฅผ ๋๋ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- route : /rulepacks/[id], /references/[refId]/detail ๊ฐ์ ํจํด ๋จ์ ํต๊ณ๋ฅผ ๋ณด๊ธฐ ์ํจ
- path : /rulepacks/123?tab=settings ๊ฐ์ด ์ค์ ์ ๊ทผ URL์ ๋ณด๊ณ ์ถ์ ๋ ์ฌ์ฉ
์ด๋ ๊ฒ ํด๋๋ฉด Sentry์์:
- message:"ํ์ด์ง ๋ฐฉ๋ฌธ" ์ด๋ผ๋ ๋ฉ์์ง๋ฅผ ๊ธฐ์ค์ผ๋ก ํํฐ๋ง
- route ํ๊ทธ ๊ธฐ์ค์ผ๋ก ๊ทธ๋ฃนํํด์ “์ด๋ค ๊ธฐ๋ฅ ๋จ์ ํ์ด์ง๊ฐ ๋ง์ด ๋ฐฉ๋ฌธ๋์๋์ง”๋ฅผ ๋น ๋ฅด๊ฒ ํ์ธํ ์ ์๋ค.
4-2. ๋ชจ๋ฌ ๋ฐฉ๋ฌธ ์ด๋ฒคํธ ๋ก๊น (Custom Hook)
ํ์ด์ง ๋ฐฉ๋ฌธ์ ๋ผ์ฐํฐ ์ด๋ฒคํธ๋ก ๊ฐ์งํ๋ฉด ๋์ง๋ง, ๋ชจ๋ฌ์ URL์ด ๋ณํ์ง ์๋ ๊ฒฝ์ฐ๊ฐ ๋๋ถ๋ถ์ด๊ธฐ ๋๋ฌธ์ ๋ผ์ฐํฐ๋ก๋ ๊ฐ์งํ ์ ์๋ค.
๊ทธ๋์ ๋ชจ๋ฌ์ ๋ํด์๋ ๋ค์ ํํ์ ํจํด์ ๋ง๋ค์๋ค.
“๋ชจ๋ฌ์ด ์ด๋ฆด ๋(= isOpen์ด true๊ฐ ๋๋ ์์ ์) ๋ฑ ํ ๋ฒ Sentry info ์ด๋ฒคํธ๋ฅผ ๋จ๊ธฐ๋ ํ ”
๊ทธ ๊ฒฐ๊ณผ๋ฌผ์ด useTrackModalOpen ์ด๋ค.
// hooks/useTrackModalOpen.ts
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import * as Sentry from '@sentry/nextjs';
export interface TrackModalOpenOptions {
message: string;
route?: string;
includePath?: boolean;
tags?: Record<string, string | number | boolean | null | undefined>;
}
/**
* ๋ชจ๋ฌ์ด open ์ํ๊ฐ ๋๋ ์์ ์ Sentry ์ด๋ฒคํธ๋ฅผ ํ ๋ฒ ๊ธฐ๋กํ๋ ํ
.
* ๊ธฐ๋ณธ์ ์ผ๋ก route(ํจํด) ํ๊ทธ๋ฅผ ๋จ๊ธฐ๊ณ , ์ต์
์ ๋ฐ๋ผ path ๋ฐ ์ถ๊ฐ ํ๊ทธ๋ ๊ธฐ๋กํ๋ค.
*/
export function useTrackModalOpen(
isOpen: boolean,
{ message, route, includePath = true, tags }: TrackModalOpenOptions,
): void {
const router = useRouter();
useEffect(() => {
if (!isOpen) return;
try {
const routeTag =
route ??
(router as any)?.pathname ??
(router as any)?.route ??
'(unknown)';
let pathTag = '';
if (includePath && typeof window !== 'undefined') {
const pathname = window.location.pathname;
const search = window.location.search || '';
pathTag = `${pathname}${search}`;
}
const finalTags: Record<string, string> = {
route: String(routeTag),
};
if (includePath && pathTag) {
finalTags.path = pathTag;
}
if (tags) {
for (const [key, value] of Object.entries(tags)) {
if (value !== undefined && value !== null) {
finalTags[key] = String(value);
}
}
}
Sentry.captureEvent({
message,
level: 'info',
tags: finalTags,
});
} catch (error) {
Sentry.captureException(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
}
์ฌ์ฉ์๋ ๋ชจ๋ฌ ์ปดํฌ๋ํธ์์ ๋ค์์ฒ๋ผ ๊ฐ๋จํ ํธ์ถ๋ง ํ๋ฉด ๋๋ค.
import { useTrackModalOpen } from '@/hooks/useTrackModalOpen';
function RulepackDeleteModal({ isOpen }: { isOpen: boolean }) {
useTrackModalOpen(isOpen, {
message: '๋ชจ๋ฌ ๋ฐฉ๋ฌธ: ๋ฃฐํฉ ์ญ์ ',
includePath: true,
tags: { modal: 'rulepack_delete' },
});
// ... ๋๋จธ์ง ๋ชจ๋ฌ UI
}
๊ฒฐ๊ณผ
(message:"ํ์ด์ง ๋ฐฉ๋ฌธ:*" OR message:"๋ชจ๋ฌ ๋ฐฉ๋ฌธ:*") event.type:default(message:"ํ์ด์ง ๋ฐฉ๋ฌธ:*" OR message:"๋ชจ๋ฌ ๋ฐฉ๋ฌธ:*") event.type:default

๋จผ์ sentry๋ฅผ ์ ์ฉํด๋ณด๊ฒ ๋ค๊ณ ๋์ ์ดํ ๊ตฌ์ถ๋ถํฐ ์ ์ฉ๊น์ง ๋ด ์์ผ๋ก ์ผ๊ถ๋๋ฐ, ์ค์ ๋ก ํด๋น ๋ฐ์ดํฐ๋ฅผ ํตํด ์์ธ์ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์์ด์ ์ ์ฉํ๊ฒ ์ฐ์ด๊ณ ์๋ค. ๊ตฌ์ถ๋ ์ด๋ ค์ ์ง๋ง ์์ค๋งต ์ฐ๋ํ ๋๊ฐ ์ ์ผ ํ๋ค์๋๋ฐ ๊ทธ๋๋ ์ ์ฉ์ ์๋ฃํด์ ์ ์ฌ์ฉํ๊ณ ์์ด์ ๋งค์ฐ ๋ฟ๋ฏํ ๊ฒฝํ์ด์๋ค.
๋๊ธ