๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
  • What would life be If we had no courage to attemp anything?
๐“๐จ๐๐š๐ฒ ๐ˆ ๐‹๐ž๐š๐ซ๐ง

์‚ฌ๋‚ด ๋ณด์•ˆ ๋ฆฌ์†Œ์Šค ํ†ตํ•ฉ ํ”Œ๋žซํผ ๊ตฌ์ถ•๊ธฐ : 6. on-premise sentry ๊ณ ๋„ํ™”

by DevIseo 2025. 11. 29.

์ด์ „๊ธ€ ์—์„œ 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๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํžˆ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค. ๋‚˜๋Š” ๋‘ ๊ฐ€์ง€ ์ •๋ณด์— ์ง‘์ค‘ํ–ˆ๋‹ค.

  1. ์–ด๋–ค ํŽ˜์ด์ง€์—์„œ ์—๋Ÿฌ๊ฐ€ ๋งŽ์ด ๋ฐœ์ƒํ•˜๋Š”๊ฐ€?
  2. ์‚ฌ์šฉ์ž๋“ค์ด ํŽ˜์ด์ง€๋ฅผ ์–ผ๋งˆ๋‚˜ ์ž์ฃผ ๋ฐฉ๋ฌธํ•˜๋Š”๊ฐ€?

์ด๋ฅผ ์œ„ํ•ด ํŽ˜์ด์ง€ ์ด๋™ ์‹œ์ ์— Sentry ํƒœ๊ทธ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ํ•จ๊ป˜ ๋‚จ๊ธฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. (ํ•œ์‹œ์ ์œผ๋กœ๋งŒ ์šด์˜ ํ›„ ์ œ๊ฑฐ ์˜ˆ์ •์ด๋‹ค!)

 

์ฝ”๋“œ ์ ์šฉ

“์ง€๊ธˆ ์‹ค์ œ ์„œ๋น„์Šค์—์„œ ์–ด๋–ค ํŽ˜์ด์ง€๋ฅผ ๋งŽ์ด ๋ณด๊ณ , ์–ด๋–ค ๋ชจ๋‹ฌ/๊ธฐ๋Šฅ์ด ์ž์ฃผ ์—ด๋ฆฌ๋Š”์ง€ ๋Œ€๋žต์ด๋ผ๋„ ์•Œ๊ณ  ์‹ถ๋‹ค.”

ํ’€์Šค์ผ€์ผ Analytics ๋„๊ตฌ(GA, Amplitude ๋“ฑ)๋ฅผ ๋ถ™์ด๊ธฐ๋ณด๋‹ค๋Š”,์šฐ๋ฆฌ๊ฐ€ ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ Sentry ์œ„์— ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฌธ ์ง€ํ‘œ๋งŒ ์–น์–ด๋„ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์„๊นŒ? ๋ผ๋Š” ํŒ๋‹จ์„ ํ–ˆ๊ณ , ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ๊ธฐ์ค€์œผ๋กœ info ๋ ˆ๋ฒจ ์ด๋ฒคํŠธ๋ฅผ ์ง์ ‘ ์Œ“๋Š” ๋ฐฉ์‹์„ ํƒํ–ˆ๋‹ค.

  1. ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ(๋ผ์šฐํ„ฐ ์ด๋™)
  2. ๋ชจ๋‹ฌ ์˜คํ”ˆ

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
}

 

๊ฒฐ๊ณผ

discoverํƒญ์—์„œsavedQuries์— ์ฟผ๋ฆฌ๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ metrics ์ง€ํ‘œ ์ƒ์„ฑํ•˜์˜€๋‹ค.
(message:"ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ:*" OR message:"๋ชจ๋‹ฌ ๋ฐฉ๋ฌธ:*") event.type:default(message:"ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ:*" OR message:"๋ชจ๋‹ฌ ๋ฐฉ๋ฌธ:*") event.type:default

 

metric ๋ฐ์ดํ„ฐ ์ปค์Šคํ…€

 

 

 

 

๋จผ์ € sentry๋ฅผ ์ ์šฉํ•ด๋ณด๊ฒ ๋‹ค๊ณ  ๋‚˜์„  ์ดํ›„ ๊ตฌ์ถ•๋ถ€ํ„ฐ ์ ์šฉ๊นŒ์ง€ ๋‚ด ์†์œผ๋กœ ์ผ๊ถœ๋Š”๋ฐ, ์‹ค์ œ๋กœ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์›์ธ์„ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด์„œ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ด๊ณ  ์žˆ๋‹ค. ๊ตฌ์ถ•๋„ ์–ด๋ ค์› ์ง€๋งŒ ์†Œ์Šค๋งต ์—ฐ๋™ํ•  ๋•Œ๊ฐ€ ์ œ์ผ ํž˜๋“ค์—ˆ๋Š”๋ฐ ๊ทธ๋ž˜๋„ ์ ์šฉ์„ ์™„๋ฃŒํ•ด์„œ ์ž˜ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์„œ ๋งค์šฐ ๋ฟŒ๋“ฏํ•œ ๊ฒฝํ—˜์ด์—ˆ๋‹ค.

๋Œ“๊ธ€