학습 목표
이 포스트를 마치면 다음을 할 수 있습니다:
- Public Links로 외부 공유
- Static Embedding으로 웹사이트에 대시보드 삽입
- Interactive Embedding으로 맞춤형 경험 제공
- Signed Embedding으로 보안 강화
- 화이트라벨링으로 브랜딩 커스터마이징
- 임베딩 성능 최적화
공유 방법 개요
graph TD
A[Metabase Sharing] --> B[Internal]
A --> C[External]
B --> B1[Direct Links]
B --> B2[Subscriptions]
C --> C1[Public Links]
C --> C2[Static Embedding]
C --> C3[Interactive Embedding]
style C1 fill:#e8f5e9
style C2 fill:#fff4e1
style C3 fill:#e1f5ff
공유 방법 비교
| 방법 | 로그인 필요 | 보안 | 커스터마이징 | 사용 사례 |
| Direct Link | ✅ Yes | 높음 | 낮음 | 내부 팀 |
| Public Link | ❌ No | 낮음 | 낮음 | 공개 데이터 |
| Static Embed | ❌ No | 중간 | 중간 | 마케팅 사이트 |
| Interactive Embed | ❌ No | 높음 | 높음 | SaaS 앱 |
Public Links (공개 링크)
개념
Public Link는 로그인 없이 누구나 볼 수 있는 링크입니다.
장점:
단점:
- 누구나 접근 가능
- 민감한 데이터 부적합
- 제한된 보안
Public Link 생성
1
2
3
4
5
| 1. Question 또는 Dashboard 열기
2. ... menu > "Sharing and embedding"
3. "Public link" 탭
4. "Enable" 클릭
5. Link 복사
|
생성된 링크 예시:
1
| https://metabase.company.com/public/dashboard/abc123def456
|
Public Link 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Public link settings:
☑ Enabled
Parameters (optional):
- date_range: Last 30 days (기본값 고정)
- category: Electronics (특정 값만 보이도록)
Expiration:
○ Never expires
● Expires on: 2025-06-30
Access control:
☑ Require password: ••••••••
☑ Disable downloads
|
사용 사례
1. 공개 메트릭 대시보드
1
2
3
4
5
6
7
8
9
10
11
12
| Use case: 회사 투명성
Dashboard: "Company Metrics"
Metrics:
- Total revenue (누적)
- Employee count
- Customer count
- Product launches
Public link: https://metrics.company.com
- Company 홈페이지 푸터에 링크
- "Transparency" 페이지에 임베드
|
2. 마케팅 캠페인 결과
1
2
3
4
5
6
7
8
9
10
11
12
| Use case: 파트너/투자자 공유
Dashboard: "Q1 Campaign Results"
Metrics:
- Campaign reach
- Conversion rates
- ROI summary
Public link with password:
- 비밀번호: Q1Results2025
- 만료: 2025-04-30
- 이메일로 링크 전송
|
3. 이벤트 실시간 현황
1
2
3
4
5
6
7
8
9
10
11
12
13
| Use case: 웨비나/컨퍼런스 통계
Dashboard: "Live Event Dashboard"
Metrics:
- Current attendees
- Questions asked
- Poll results
- Engagement metrics
Public link:
- 이벤트 중에만 활성화
- 화면에 프로젝션
- 실시간 업데이트
|
Static Embedding (정적 임베딩)
개념
HTML iframe으로 웹사이트에 대시보드 삽입
1
2
3
4
5
6
7
| <iframe
src="https://metabase.company.com/public/dashboard/abc123"
frameborder="0"
width="800"
height="600"
allowtransparency
></iframe>
|
Embedding 설정
1
2
3
4
5
6
7
8
9
| Admin > Settings > Embedding
Static embedding:
☑ Enable static embedding
Embedding secret key:
- Auto-generated
- Used for signed embeds
- Keep secret!
|
Embed Code 생성
1
2
3
4
5
6
7
8
| 1. Dashboard > ... menu > "Sharing and embedding"
2. "Embed" 탭
3. Appearance 설정:
- Theme: Light / Dark / Transparent
- Bordered: Yes / No
- Titled: Yes / No
4. Parameters (optional)
5. Copy iframe code
|
생성된 코드:
1
2
3
4
5
6
7
8
9
10
| <iframe
src="https://metabase.company.com/embed/dashboard/TOKEN#
bordered=false&
titled=false&
theme=transparent"
frameborder="0"
width="100%"
height="600"
allowtransparency
></iframe>
|
Styling 커스터마이징
Responsive embed:
1
2
3
4
5
6
7
8
| <div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe
src="https://metabase.company.com/embed/dashboard/TOKEN"
frameborder="0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
allowtransparency
></iframe>
</div>
|
투명 배경:
1
2
3
4
5
6
7
8
9
| <!-- Metabase 설정 -->
theme=transparent
<!-- CSS 추가 -->
<style>
iframe {
background: transparent;
}
</style>
|
다크 모드:
1
2
3
4
| <iframe
src="https://metabase.company.com/embed/dashboard/TOKEN#theme=night"
...
></iframe>
|
사용 사례
1. 제품 페이지 통계
1
2
3
4
5
6
7
8
9
10
11
| <!-- Product landing page -->
<section id="stats">
<h2>Real-time Product Stats</h2>
<iframe
src="https://metabase.company.com/embed/dashboard/product-stats#
theme=transparent&
bordered=false"
width="100%"
height="400"
></iframe>
</section>
|
2. 고객 포털
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- Customer dashboard -->
<div class="customer-portal">
<h1>Welcome, {{customer_name}}</h1>
<!-- Usage stats -->
<iframe
src="https://metabase.company.com/embed/question/usage-stats?
customer_id={{customer_id}}"
width="100%"
height="300"
></iframe>
</div>
|
3. 블로그 포스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- Blog post with embedded chart -->
<article>
<h1>Our Growth in 2025</h1>
<p>We're excited to share our growth metrics...</p>
<figure>
<iframe
src="https://metabase.company.com/embed/question/growth-chart"
width="800"
height="400"
></iframe>
<figcaption>Monthly revenue growth</figcaption>
</figure>
</article>
|
Interactive Embedding (인터랙티브 임베딩)
개념
사용자별 맞춤 데이터를 보여주는 동적 임베딩
graph LR
A[Your App] --> B[Generate Token]
B --> C[Signed URL]
C --> D[Metabase Embed]
D --> E[User-specific Data]
🚀 v0.56 신기능: Embedded Analytics SDK가 도입되어 iframe 방식 외에도 React/Vue 컴포넌트 스타일의 임베딩이 가능해졌습니다. 더 나은 사용자 경험과 세밀한 제어가 가능합니다.
Signed Embedding 설정
Step 1: Metabase 설정
1
2
3
4
5
6
| Admin > Settings > Embedding
☑ Enable embedding
Embedding secret key: YOUR_SECRET_KEY
(Keep this secret!)
|
Step 2: 서버에서 Token 생성
Node.js 예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| const jwt = require('jsonwebtoken');
function generateMetabaseToken(dashboardId, params) {
const METABASE_SITE_URL = 'https://metabase.company.com';
const METABASE_SECRET_KEY = process.env.METABASE_SECRET_KEY;
const payload = {
resource: { dashboard: dashboardId },
params: params,
exp: Math.round(Date.now() / 1000) + (10 * 60) // 10분 후 만료
};
const token = jwt.sign(payload, METABASE_SECRET_KEY);
return `${METABASE_SITE_URL}/embed/dashboard/${token}#
bordered=false&
titled=true`;
}
// Usage
const url = generateMetabaseToken(123, {
customer_id: 456,
date_range: 'past30days'
});
|
Python 예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import jwt
import time
def generate_metabase_token(dashboard_id, params):
METABASE_SITE_URL = 'https://metabase.company.com'
METABASE_SECRET_KEY = os.environ['METABASE_SECRET_KEY']
payload = {
'resource': {'dashboard': dashboard_id},
'params': params,
'exp': int(time.time()) + (10 * 60) # 10분 후 만료
}
token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm='HS256')
return f'{METABASE_SITE_URL}/embed/dashboard/{token}#bordered=false&titled=true'
# Usage
url = generate_metabase_token(123, {
'customer_id': 456,
'date_range': 'past30days'
})
|
Step 3: 프론트엔드에 삽입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // React 예제
function CustomerDashboard({ customerId }) {
const [embedUrl, setEmbedUrl] = useState('');
useEffect(() => {
// 서버에서 signed URL 가져오기
fetch('/api/metabase-embed-url', {
method: 'POST',
body: JSON.stringify({
dashboard_id: 123,
customer_id: customerId
})
})
.then(res => res.json())
.then(data => setEmbedUrl(data.url));
}, [customerId]);
return (
<iframe
src={embedUrl}
frameBorder="0"
width="100%"
height="600"
allowTransparency
/>
);
}
|
Parameters 전달
Dashboard 파라미터 설정:
1
2
3
4
5
6
7
8
9
10
11
| Dashboard: Customer Analytics
Parameters:
- customer_id (Number)
- date_range (Date)
- region (Text)
Embedding settings:
customer_id: Locked (서버에서만 설정)
date_range: Editable (사용자가 변경 가능)
region: Disabled (사용 안 함)
|
Token에 파라미터 포함:
1
2
3
4
5
6
| const params = {
customer_id: 456, // Locked - 사용자가 못 바꿈
date_range: 'past30days' // Editable - 사용자가 바꿀 수 있음
};
const url = generateMetabaseToken(123, params);
|
신규: Embedded Analytics SDK (v0.56+)
React SDK 사용:
1
| npm install @metabase/embedding-sdk-react
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import { MetabaseProvider, MetabaseDashboard } from "@metabase/embedding-sdk-react";
function App() {
return (
<MetabaseProvider
config={{
metabaseInstanceUrl: "https://metabase.company.com",
jwtProviderUri: "/api/metabase-sso",
}}
>
<MetabaseDashboard
dashboardId={123}
parameters={{
tenant_id: currentUser.tenantId,
date_range: "past30days"
}}
onLoad={(dashboard) => {
console.log("Dashboard loaded:", dashboard.name);
}}
onError={(error) => {
console.error("Failed to load:", error);
}}
withDownloads={false}
withTitle={true}
/>
</MetabaseProvider>
);
}
|
Vue SDK 사용:
1
| npm install @metabase/embedding-sdk-vue
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <template>
<MetabaseDashboard
:dashboard-id="123"
:parameters="dashboardParams"
@load="handleLoad"
@error="handleError"
/>
</template>
<script setup>
import { MetabaseDashboard } from "@metabase/embedding-sdk-vue";
const dashboardParams = ref({
tenant_id: currentUser.value.tenantId,
date_range: "past30days"
});
const handleLoad = (dashboard) => {
console.log("Loaded:", dashboard);
};
const handleError = (error) => {
console.error("Error:", error);
};
</script>
|
SDK 장점:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 1. 타입 안전성:
- TypeScript 지원
- IDE 자동완성
- 컴파일 타임 에러 감지
2. 런타임 제어:
- 파라미터 동적 업데이트
- 이벤트 핸들링
- 프로그래밍 방식 액션
3. 프레임워크 통합:
- React/Vue 생명주기 통합
- 상태 관리 연동
- 컴포넌트 재사용성
4. 향상된 UX:
- 더 빠른 로딩
- 부드러운 전환
- 네이티브 느낌
|
사용 사례
1. SaaS 고객 대시보드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Multi-tenant SaaS application
function TenantDashboard({ tenantId, user }) {
const embedUrl = generateMetabaseToken(DASHBOARD_ID, {
tenant_id: tenantId, // 각 고객의 데이터만
user_role: user.role, // 역할별 데이터 필터링
subscription_tier: user.tier // 구독 등급별 기능
});
return <iframe src={embedUrl} ... />;
}
// Tenant A sees only their data
// Tenant B sees only their data
|
2. 이커머스 판매자 포털
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // Seller dashboard on marketplace
function SellerDashboard({ sellerId }) {
const dashboards = {
sales: generateMetabaseToken(SALES_DASHBOARD_ID, {
seller_id: sellerId,
date_range: 'past30days'
}),
products: generateMetabaseToken(PRODUCTS_DASHBOARD_ID, {
seller_id: sellerId
}),
customers: generateMetabaseToken(CUSTOMERS_DASHBOARD_ID, {
seller_id: sellerId
})
};
return (
<Tabs>
<Tab label="Sales">
<iframe src={dashboards.sales} />
</Tab>
<Tab label="Products">
<iframe src={dashboards.products} />
</Tab>
<Tab label="Customers">
<iframe src={dashboards.customers} />
</Tab>
</Tabs>
);
}
|
3. IoT 디바이스 모니터링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Device monitoring dashboard
function DeviceDashboard({ deviceId, timeRange }) {
const embedUrl = generateMetabaseToken(DEVICE_DASHBOARD_ID, {
device_id: deviceId,
time_range: timeRange,
metrics: ['temperature', 'humidity', 'battery']
});
return (
<div>
<h2>Device {deviceId} Monitoring</h2>
<iframe src={embedUrl} width="100%" height="800" />
</div>
);
}
|
화이트라벨링 (White-labeling)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| Admin > Settings > Appearance
Application name: "Your Company Analytics"
Logo:
- Upload logo (SVG, PNG)
- Size: 60px height recommended
Favicon:
- Upload favicon (ICO, PNG)
Colors:
- Primary color: #007bff
- Accent color: #28a745
- Navigation bar color: #343a40
Font:
- Custom font URL (Google Fonts)
- font-family: 'Roboto', sans-serif
|
Embedded 브랜딩
로고 제거:
1
2
3
4
5
| Embedding settings:
☑ Hide Metabase logo
☑ Hide "Powered by Metabase"
Note: Requires paid plan
|
커스텀 CSS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <style>
/* iframe 내부 스타일링 (제한적) */
.embed iframe {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* iframe 외부 래퍼 스타일링 */
.dashboard-container {
background: #f5f5f5;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
</style>
<div class="dashboard-container">
<h1>Your Company Analytics</h1>
<div class="embed">
<iframe src="..." width="100%" height="600"></iframe>
</div>
</div>
|
커스텀 도메인:
1
2
3
4
5
6
7
8
9
10
| Metabase URL: https://metabase.company.com
↓ CNAME
Custom domain: https://analytics.company.com
Embedding에서 사용:
https://analytics.company.com/embed/dashboard/...
User experience:
- 회사 도메인으로 보임
- 브랜드 일관성 유지
|
성능 최적화
1. 캐싱 전략
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Admin > Settings > Caching
Embedded dashboard caching:
☑ Enable caching
TTL: 1 hour (3600 seconds)
Query caching:
☑ Enable
Minimum duration: 1 second
Maximum age: 24 hours
Result:
- 빠른 로딩
- 데이터베이스 부하 감소
- 사용자 경험 개선
|
2. 로딩 최적화
1
2
3
4
5
6
7
8
9
10
| // Lazy loading
<iframe
src="..."
loading="lazy"
width="100%"
height="600"
></iframe>
// Preload hint
<link rel="preconnect" href="https://metabase.company.com">
|
3. 응답형 디자인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <!-- Container query 사용 -->
<div class="dashboard-embed">
<iframe
src="..."
width="100%"
height="auto"
style="aspect-ratio: 16/9; min-height: 400px;"
></iframe>
</div>
<style>
@media (max-width: 768px) {
.dashboard-embed iframe {
min-height: 300px;
}
}
</style>
|
4. 로딩 상태 표시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| function EmbeddedDashboard({ dashboardId }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
return (
<div className="embed-container">
{loading && (
<div className="loading-spinner">
Loading dashboard...
</div>
)}
{error && (
<div className="error-message">
Failed to load dashboard. Please try again.
</div>
)}
<iframe
src={getEmbedUrl(dashboardId)}
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
setError('Failed to load');
}}
style={{ display: loading ? 'none' : 'block' }}
/>
</div>
);
}
|
보안 고려사항
1. Token 보안
1
2
3
4
5
6
7
8
9
10
11
| ✅ 좋은 예:
- 서버에서 token 생성
- SECRET_KEY는 환경 변수로 관리
- Token에 만료 시간 설정 (5-10분)
- HTTPS only
❌ 나쁜 예:
- 클라이언트에서 token 생성
- SECRET_KEY 노출
- 만료 없는 token
- HTTP 사용
|
2. Parameter 보안
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Locked parameters (사용자가 변경 불가)
const secureParams = {
tenant_id: user.tenantId, // ✅ Locked
user_role: user.role, // ✅ Locked
is_premium: user.isPremium // ✅ Locked
};
// Editable parameters (사용자가 변경 가능)
const editableParams = {
date_range: 'past30days', // User can change
metric: 'revenue' // User can change
};
const url = generateToken(dashboardId, {
...secureParams,
...editableParams
});
|
3. CSP (Content Security Policy)
1
2
3
4
5
6
| <meta http-equiv="Content-Security-Policy"
content="
default-src 'self';
frame-src https://metabase.company.com;
script-src 'self';
">
|
4. Rate Limiting
1
2
3
4
5
6
7
8
9
10
11
12
| // Express middleware 예제
const rateLimit = require('express-rate-limit');
const embedLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 100 요청
message: 'Too many requests, please try again later'
});
app.post('/api/metabase-embed-url', embedLimiter, async (req, res) => {
// Generate embed URL
});
|
실전 연습 문제
연습 1: Public Link 공유 (초급)
과제: 회사 메트릭 Public Dashboard 생성
요구사항:
- 매출, 고객 수, 주문 수 표시
- 민감한 정보 제외
- 3개월 후 만료
- 비밀번호 보호
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| Dashboard: "Public Company Metrics"
Widgets:
1. Total Revenue (Number)
- Sum(total) WHERE status = 'completed'
- Hide exact amount, show trend only
2. Customer Count (Number)
- Count(distinct customer_id)
3. Order Count (Number)
- Count(id) WHERE status = 'completed'
4. Monthly Trend (Line chart)
- Revenue by month (last 12 months)
Public Link settings:
☑ Enabled
Expires: 2025-06-30
Password: Metrics2025!
☑ Disable downloads
☑ Hide exact values on charts (show trends only)
Filters:
- Date range: Last 12 months (fixed)
- Status: completed (hidden)
Share:
- Add to company homepage
- Email link to stakeholders
- Include password in separate channel
|
연습 2: Static Embedding (중급)
과제: 마케팅 사이트에 제품 통계 임베딩
요구사항:
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
| <!-- Product landing page -->
<!DOCTYPE html>
<html>
<head>
<title>Product Analytics</title>
<style>
:root {
--brand-primary: #007bff;
--brand-secondary: #6c757d;
}
.stats-section {
background: linear-gradient(135deg, var(--brand-primary), var(--brand-secondary));
padding: 60px 20px;
color: white;
}
.stats-container {
max-width: 1200px;
margin: 0 auto;
}
.stats-title {
text-align: center;
margin-bottom: 40px;
font-size: 2.5rem;
}
.embed-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.embed-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
@media (max-width: 768px) {
.stats-title {
font-size: 1.8rem;
}
.embed-wrapper {
padding-bottom: 75%; /* Taller on mobile */
}
}
</style>
</head>
<body>
<section class="stats-section">
<div class="stats-container">
<h2 class="stats-title">Real-time Product Statistics</h2>
<div class="embed-wrapper">
<iframe
src="https://metabase.company.com/embed/dashboard/PRODUCT_STATS_TOKEN#
theme=transparent&
bordered=false&
titled=false"
allowtransparency
></iframe>
</div>
<p style="text-align: center; margin-top: 20px; opacity: 0.9;">
Last updated: <span id="last-updated"></span>
</p>
</div>
</section>
<script>
// Update timestamp
document.getElementById('last-updated').textContent =
new Date().toLocaleString();
// Refresh every 5 minutes
setInterval(() => {
location.reload();
}, 5 * 60 * 1000);
</script>
</body>
</html>
|
연습 3: Interactive Embedding (고급)
과제: SaaS 앱에 고객별 대시보드 구현
요구사항:
- 사용자별 데이터 분리
- 구독 등급별 기능 제한
- Secure token 생성
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
| // Backend (Node.js + Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// Environment variables
const METABASE_SECRET_KEY = process.env.METABASE_SECRET_KEY;
const METABASE_SITE_URL = process.env.METABASE_SITE_URL;
// Dashboard IDs by subscription tier
const DASHBOARDS = {
free: 10, // Basic metrics only
pro: 20, // Advanced analytics
enterprise: 30 // Full analytics suite
};
// Generate Metabase embed token
function generateEmbedToken(user, dashboardType) {
const dashboardId = DASHBOARDS[user.subscriptionTier] || DASHBOARDS.free;
const payload = {
resource: { dashboard: dashboardId },
params: {
// Locked parameters (user cannot change)
tenant_id: user.tenantId,
user_role: user.role,
subscription_tier: user.subscriptionTier,
// Editable parameters (user can change)
date_range: 'past30days'
},
exp: Math.round(Date.now() / 1000) + (10 * 60) // 10분 후 만료
};
const token = jwt.sign(payload, METABASE_SECRET_KEY);
return {
url: `${METABASE_SITE_URL}/embed/dashboard/${token}#
bordered=false&
titled=true&
theme=light`,
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
};
}
// API endpoint
app.post('/api/dashboard-embed', authenticateUser, (req, res) => {
const user = req.user;
// Permission check
if (!user.hasPermission('view_analytics')) {
return res.status(403).json({ error: 'Access denied' });
}
// Generate embed token
const embed = generateEmbedToken(user, req.body.dashboardType);
res.json(embed);
});
// Frontend (React)
function CustomerDashboard() {
const [embedUrl, setEmbedUrl] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const user = useAuth();
useEffect(() => {
// Fetch embed URL from backend
async function fetchEmbedUrl() {
try {
const response = await fetch('/api/dashboard-embed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user.token}`
},
body: JSON.stringify({
dashboardType: 'main'
})
});
if (!response.ok) {
throw new Error('Failed to load dashboard');
}
const data = await response.json();
setEmbedUrl(data.url);
setLoading(false);
// Refresh token before expiration
const refreshTime = new Date(data.expiresAt).getTime() - Date.now() - 60000;
setTimeout(fetchEmbedUrl, refreshTime);
} catch (err) {
setError(err.message);
setLoading(false);
}
}
fetchEmbedUrl();
}, [user]);
if (loading) {
return (
<div className="dashboard-loading">
<Spinner />
<p>Loading your analytics...</p>
</div>
);
}
if (error) {
return (
<div className="dashboard-error">
<p>Failed to load dashboard: {error}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return (
<div className="dashboard-container">
<header className="dashboard-header">
<h1>Your Analytics Dashboard</h1>
<div className="user-info">
<span>{user.name}</span>
<span className="badge">{user.subscriptionTier}</span>
</div>
</header>
<div className="dashboard-embed">
<iframe
src={embedUrl}
frameBorder="0"
width="100%"
height="800"
allowTransparency
/>
</div>
{user.subscriptionTier === 'free' && (
<div className="upgrade-banner">
<p>Upgrade to Pro for advanced analytics!</p>
<button>Upgrade Now</button>
</div>
)}
</div>
);
}
|
Metabase Dashboard Setup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| Dashboard: Customer Analytics (ID: 20)
Parameters:
- tenant_id (Number): Locked
Used in: WHERE tenant_id = {{tenant_id}}
- user_role (Text): Locked
Used in: CASE WHEN {{user_role}} = 'admin' THEN ...
- subscription_tier (Text): Locked
Used in: Feature gating
- date_range (Date): Editable
Used in: WHERE created_at {{date_range}}
Questions:
1. Your Revenue (visible to all tiers)
2. Customer Breakdown (Pro+)
3. Advanced Metrics (Enterprise only)
CASE WHEN {{subscription_tier}} IN ('enterprise')
THEN actual_query
ELSE "Upgrade to see this"
Row-level security:
All queries include: WHERE tenant_id = {{tenant_id}}
|
다음 단계
임베딩과 공유를 마스터했습니다. 다음 포스트에서는:
- 성능 최적화: 캐싱, 쿼리 튜닝, 인덱싱
- 대용량 데이터 처리: 샘플링, 집계 테이블
- 모니터링: 성능 메트릭, 슬로우 쿼리 분석
요약
임베딩 방법 선택 가이드
graph TD
A[Embedding Need] --> B{로그인 필요?}
B -->|No| C{보안 중요?}
B -->|Yes| D[Direct Link]
C -->|No| E[Public Link]
C -->|Yes| F{사용자별 데이터?}
F -->|No| G[Static Embed]
F -->|Yes| H[Interactive Embed]
체크리스트
Public Links:
- 민감한 데이터 제거
- 만료 날짜 설정
- 비밀번호 보호 고려
- Download 제한
Static Embedding:
- 적절한 theme 선택
- 반응형 디자인 적용
- 브랜딩 일관성 유지
- 성능 최적화
Interactive Embedding:
- Secure token 생성
- Parameter 보안 검토
- Token 만료 처리
- Rate limiting 적용
- Error handling 구현
다음 포스트에서는 Metabase 성능을 최적화하는 방법을 배웁니다!
📚 시리즈 전체 목차
🚀 기초편 (1-5화)
- Metabase 소개와 핵심 개념
- 설치와 초기 설정
- 샘플 데이터 둘러보기
- 첫 차트 만들기 - 실습 완전정복
- 대시보드 만들기 - 한 화면에 모으기
💪 활용편 (6-10화)
- 필터와 파라미터
- SQL 네이티브 쿼리
- 데이터 모델링
- 자동화와 알림
- 권한과 보안
🎯 고급편 (11-16화)
- [임베딩과 공유] (현재 글)
- 성능 최적화
- 멀티 데이터소스
- 커스터마이징
- 운영과 모니터링
- 실전 프로젝트