React 애플리케이션에서 Wyn API로 임베디드 BI 구현하기
이번 글에서는 Wyn Enterprise를 통합하여 최종 사용자에게 임베디드 BI를 제공하는 방법을 다룰 예정입니다. 이를 위해 React 애플리케이션과 Wyn의 API 엔드포인트를 활용할 것입니다.
본격적으로 들어가기 전에, 먼저 임베디드 BI란 무엇이고 왜 오늘날의 애플리케이션에서 필수적인지 살펴보겠습니다.
임베디드 BI의 데이터 인사이트
임베디드 BI가 등장하기 전에는 데이터 인사이트를 얻기 위해 별도의 분석 도구를 사용해야 했습니다. 그러나 이 도구들은 기존 비즈니스 프로세스와 완전히 분리되어 있어 사용자가 일상적인 업무 애플리케이션과 외부 분석 도구를 오가야 했습니다.
이러한 분리는 아래와 같은 문제를 초래했습니다.
- 업무 효율성 저하
- 가치 창출까지 걸리는 시간 증가
- 조직 전반의 불편
임베디드 BI는 이 문제를 해결합니다.
비즈니스 애플리케이션 내에 BI 기능을 직접 통합해 보고서, 대시보드, 데이터 시각화, 분석 기능을 한 곳에서 제공합니다. 별도의 도구 전환이 필요 없습니다.

Wyn과 GraphQL API을 이용한 임베디드 BI
Wyn Enterprise는 협업형 셀프서비스 리포팅 & 분석, 데이터 시각화, 데이터 관리 기능을 제공하는 BI 솔루션입니다. Wyn API는 GraphQL을 사용하여 데이터를 조회·수정·실행할 수 있습니다.
이 글은 React 애플리케이션이 준비되어 있다고 가정합니다. 없다면 다음 명령어로 간단히 생성할 수 있습니다.
npx create-react-app my-app
cd my-app
npm start
이번 글을 끝까지 따라 하면 애플리케이션은 다음과 같은 모습이 됩니다.

Wyn API 구현 단계
이번 글에서 다룰 Wyn API 기능 목록은 다음과 같습니다.
- 인증(Authentication)
- 보고서 목록 가져오기(Get Reports List)
- 보고서 정보 가져오기(Get Report Info)
- 보고서 보기(View Report)
- 보고서 내보내기(Export Report)
- 대시보드 목록 가져오기(Get Dashboards List)
- 대시보드 정보 가져오기(Get Dashboard Info)
- 대시보드 보기(View Dashboard)
1. 인증(Authentication)
보안이 필요한 모든 애플리케이션과 마찬가지로 Wyn Enterprise도 인증 절차가 필요합니다.
Wyn의 인증 절차는 간단하며, username, password, clientid, clientsecret이 필요합니다.
- username : Wyn 포털 로그인 계정
- password : Wyn 포털 로그인 비밀번호
- client_id : (기본값) integration
- client_secret : {serverUrl}/management#client-management에서 integration client_id에 해당하는 값
* 관리자 계정으로 로그인해야 client_secret 확인 가능합니다.
API 호출
POST {serverUrl}/connect/token
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=password
&username=<username>
&password=<password>
&client_id=integration
&client_secret={client_secret}
요청 예시
export async function GetAccessToken(url, user, password) {
const endpoint = `${url}/connect/token`
const resolveResponse = async (response) => {
const jsonResponse = await response.json()
if (jsonResponse.error) return null
return jsonResponse.access_token
}
return await fetch(endpoint, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: '*/*',
},
method: 'post',
body: `grant_type=password&username=${user}&password=${password}&client_id=integration&client_secret=eunGKas3Pqd6FMwx9eUpdS7xmz`,
})
.then(await resolveResponse)
.catch((error) => {
alert(error)
return null
})
}
응답 예시
{
"access_token": "nf489aafehf39uadfkjhacf98uashf9hfiasef89aqoduxn",
"expires_in": 315360000,
"token_type": "Bearer"
}
2. 보고서 목록 가져오기(Get Reports List)
인증 후 Wyn 서버에 저장된 모든 보고서 목록을 조회합니다.
GraphQL 쿼리
query {
documenttypes(key: "rdl") {
documents {
id
title
type
}
}
}
-
key:"rdl"→ 보고서 -
key:"dbd"→ 대시보드 - 다른 key:
dsc(데이터 소스),dataset(데이터셋) 등
API 호출
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
Body: {GraphQL query}
요청 예시
const getReportsList = (token: string, serverURL: string) => async () => {
const graphQuery = {
query: 'query { documenttypes(key:"rdl") { documents{ id, title } } }'
};
const response = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if (response && response.requestError) {
alert(response && response.requestError);
return;
}
const { data: { documenttypes } } = response;
return documenttypes[0].documents;
}
응답 예시
{
"data": {
"documenttypes": [
{
"documents": [
{
"id": "4b7e3f22-41e1-409e-bd22-c36d4a734e54",
"filename": "TestPageReport.rdlx",
"title": "TestPageReport",
"type": "rdl"
},
{
"id": "6b8d4j22-41e1-409e-bd22-cadjsf48afj4",
"filename": "TestReport2.rdlx",
"title": "TestReport2",
"type": "rdl"
}
]
}
]
},
"errors": null
}
3. 보고서 정보 가져오기(Get Report Info)
이제 모든 보고서 목록이 준비되었으니, 애플리케이션에서 보고서를 선택하여 확인해보겠습니다.
보고서를 보기 전에 보고서에 몇 가지 매개변수가 필요할 수 있습니다. 이때 'reportInfo' API 엔드포인트가 유용합니다. reportInfo API 엔드포인트는 보고서 ID를 기반으로 매개변수 목록(있는 경우)과 기본값을 반환하므로, 후속 쿼리에 매개변수 값을 쉽게 전송할 수 있습니다. GraphQL 쿼리에는 보고서 ID와 액세스 토큰만 필요합니다.
GraphQL 쿼리
query {
reportInfo(reportId: "${id}") {
name,
parameters {
name,
validValues {
values {
label,
value
}
}
defaultValue {
values
}
}
}
}
API 호출
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
{GraphQL query}
요청 예시
const getReportInfo = (token: string, serverURL: string) => async (id: string): Promise<ReportingTypes.ReportInfoResponse | null> => {
const graphQuery = {
query: `query { reportInfo(reportId: "${id}") { name, parameters { name, validValues { values { label, value } } defaultValue { values }} } }`
};
const response = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if (response && response.errors) {
alert(response && response.errors[0] && response.errors[0].message);
return null;
}
const { data: { reportInfo } } = response;
return reportInfo as ReportingTypes.ReportInfoResponse;
}
응답 예시
{
"data": {
"reportInfo": {
"name": "CashFlowReport",
"parameters": [
{
"name": "DateRange",
"validValues": {
"values": [
{ "label": "This Calender Year", "value": "calenderYear" },
{ "label": "This Month", "value": "month" },
{ "label": "Last Month", "value": "lastMonth" },
{ "label": "This Quarter", "value": "quarter" },
{ "label": "This Fiscal Year", "value": "fiscalYear" }
]
},
"defaultValue": {
"values": [
"calenderYear"
]
}
}
]
}
},
"errors": null
}
4. 보고서 보기(View Report)
이제 애플리케이션에서 보고서를 확인해 보겠습니다. 애플리케이션 내에서 보고서를 표시하기 위해 iFrame을 사용할 것입니다.
방법 1: API를 통한 iFrame 표시
보고서를 보기 위한 API 호출은 3단계로 진행됩니다.
첫 번째 단계는 선택한 보고서 ID로 '렌더링' 엔드포인트를 호출하는 것입니다. 이 호출은 서버에서 보고서를 실행하고 작업 ID를 반환합니다.
두 번째 단계는 이 작업 ID를 사용하여 'job' API 엔드포인트를 호출하는 것입니다. 이 엔드포인트는 서버에서 문서가 렌더링되면 문서의 상태와 문서 ID를 반환합니다.
마지막으로, 이 문서 ID를 사용하여 'exportDocument' 엔드포인트를 호출하여 내보낸 문서의 URL을 가져오고, 이 URL은 iFrame의 소스로 설정되어 사용자에게 표시됩니다.
-
render→ jobId 반환 -
job→ 문서 상태와 documentId 반환 -
exportDocument→ resultUrl 반환 → iFrame src에 설정
GraphQL 쿼리
# 1. Render 요청
mutation {
render(reportId: "${id}", renderPayload: ${renderPayloadString}) {
jobId
}
}
# 2. Job 상태 조회
query {
job(jobId: "${id}") {
status,
documentId,
error
}
}
# 3. Document 내보내기
mutation {
exportDocument(
documentId: "${id}",
exportExtension: "${exportExtension}",
renderPayload: ${renderPayloadString}
) {
resultUrl,
verificationUrl
}
}
API 호출
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
{GraphQL query}
요청 예시:
const renderDocument = (token: string, serverURL: string) => async (
reportID: string,
exportExtension: string,
renderPayload: ReportingTypes.RenderPayload
) => {
const getJobID = async (id) => {
const renderPayloadString = JSON.stringify(renderPayload).replace(/"([^(")"]+)":/g, "$1:");
const graphQuery = {
query: `mutation { render(reportId: "${id}", renderPayload: ${renderPayloadString}) { jobId } }`
};
const response = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if (response && response.errors) {
alert(response && response.errors[0] && response.errors[0].message);
return;
}
const { data: { render: { jobId } } } = response;
return jobId;
}
const getDocumentID = async (id) => {
const graphQuery = {
query: `query { job(jobId: "${id}") { status, documentId, error } }`
};
const response: ReportingTypes.ResponseJobStatus = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if (response && response.errors) {
alert(response && response.errors[0] && response.errors[0].message);
return null;
}
const { data: { job: { status, documentId, error } } } = response;
if (status === "ERROR") {
alert(error);
return null;
}
if (status === "RUNNING") {
return getDocumentID(id);
}
return documentId;
}
const jobID = await getJobID(reportID);
const documentID = await getDocumentID(jobID);
if (!documentID) return { documentID: null, documentURL: null };
let renderDocumentPayload: ReportingTypes.RenderDocumentPayload = {
interactiveActions: renderPayload.interactiveActions,
settings: renderPayload.settings,
};
// override Print option, for disabling Content-Disposition as attachment
renderDocumentPayload && renderDocumentPayload.settings &&
renderDocumentPayload.settings.push({ key: 'Print', value: 'true' });
const documentURL = await exportDocument(token, serverURL)(documentID, exportExtension, renderDocumentPayload);
return { documentID, documentURL };
}
응답 예시
1. 작업 ID
{
"data": {
"render": {
"jobId": "51c18ab0-5557-4635-93fc-fedba01a7dc2"
}
},
"errors": null
}
2. 문서ID
{
"data": {
"job": {
"status": "SUCCESS",
"documentId": "a532662d-j24l-4486-b6c9-f70ae237fa81",
"error": null
}
},
"errors": null
}
3. 문서URL
{
"data": {
"exportDocument": {
"resultUrl": "/api/workerJob/997ea20c-u7s3-4e8c-8bdf-92f7482s077b",
"verificationUrl": "/api/workerJob/928ad40c-e5c4-4gh3-8csf-92asd3f3077b/verify"
}
},
"errors": null
}
'resultUrl'은 iFrame의 소스로 사용됩니다.
내보낸 문서의 유형은 'exportDocument' 엔드포인트에 전달된 'exportExtension' 인수에 의해 결정됩니다.
'verificationUrl'은 결과 스트림을 읽은 후 내보내기 오류를 확인하는 데 선택적으로 사용할 수 있습니다.
방법 2: Direct URL - 직접 URL
iFrame에 보고서를 표시하는 또 다른 방법이 있습니다.
이 대체 방법은 iFrame의 소스를 필요한 보고서 ID와 액세스 토큰을 사용하여 다음 URL로 설정하는 것입니다.
{serverUrl}/reports/view/{reportId}?token={token}
5. 보고서 내보내기(Export Report)
모든 비즈니스 애플리케이션, 특히 BI 및 분석 도구를 사용하는 애플리케이션에서는 추가 분석을 위한 내보내기 기능을 제공하고 이해관계자 및 동료와 공유하는 것이 중요합니다. 보고서가 표시되면 'exportReport' API 엔드포인트를 사용하여 원하는 형식으로 내보낼 수 있습니다.
Wyn의 API는 다음 형식으로 보고서를 내보내는 기능을 제공합니다.
- Excel
- Image
- HTML
- CSV
- JSON
- DOCX
exportReport 엔드포인트는 내보낸 문서의 URL을 반환하며, 해당 문서는 새 창에서 열려 다운로드됩니다.
GraphQL 쿼리
mutation {
exportReport(
reportId: "${id}",
exportExtension: "${exportExtension}",
renderPayload: ${renderPayloadString}
) {
resultUrl,
verificationUrl
}
}
엔드포인트에는 reportId, 내보내기 형식(Excel 및 PDF), 그리고 renderPayload가 필요합니다.
renderPayload는 {key, value} 쌍으로 구성된 배열로, 보고서 매개변수를 키로 포함하고 매개변수화된 보고서에 대한 해당 값을 포함합니다.
API 호출
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
{GraphQL query}
요청 예시:
const exportReport =
(token: string, serverURL: string) =>
async (
id: string,
exportExtension: string,
renderPayload: ReportingTypes.RenderPayload
) => {
const renderPayloadString = JSON.stringify(renderPayload).replace(
/"([^(")"]+)":/g,
'$1:'
)
const graphQuery = {
query: `mutation { exportReport(reportId: "${id}", exportExtension: "${exportExtension}", renderPayload: ${renderPayloadString}) { resultUrl, verificationUrl } }`,
}
const response = await httpHelper.postJson(
`${serverURL}/api/GraphQL?token=${token}`,
graphQuery
)
if (response && response.errors) {
alert(response && response.errors[0] && response.errors[0].message)
return
}
const {
data: {
exportReport: { resultUrl },
},
} = response
return resultUrl
}
예시 응답
{
"data": {
"exportReport": {
"resultUrl": "/api/workerJob/8263c103-234d-44ec-83v7-0d8f488d5dac",
"verificationUrl": "/api/workerJob/8263c103-234d-44ec-83v7-0d8f488d5dac/verify"
}
},
"errors": null
}
6. 대시보드 목록 가져오기(Get Dashboards List)
보고서에서 벗어나 대시보드를 살펴보겠습니다. 대시보드는 임베디드 BI 및 비즈니스 애플리케이션에서 필수적인 요소가 되었습니다.
대시보드는 핵심 성과 지표(KPI)를 한눈에 파악하고 차트, 게이지, 매트릭스, 표와 같은 데이터 시각화를 활용하여 비즈니스의 다양한 측면을 추적하고 분석하는 데 도움을 줍니다.
보고서와 동일한
documenttypes
API를 사용하되
key:"dbd"
로 호출합니다.
GraphQL 예시
query {
documenttypes(key: "dbd") {
documents {
id,
title,
type
}
}
}
API 호출
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
{GraphQL query}
요청 예시
const getDashboardsList = (token: string, serverURL: string) => async () => {
const graphQuery = {
query: 'query { documenttypes(key:"dbd") { documents{ id, title, type } } }'
};
const response = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if (response && response.requestError) {
alert(response && response.requestError);
return;
}
const { data: { documenttypes } } = response;
return documenttypes[0].documents;
}
응답 예시
{
"data": {
"documenttypes": [
{
"documents": [
{
"id": "5d07566bf87n1z0001da661c",
"filename": "Regional Revenue Analytics.dbd",
"title": "Regional Revenue Analytics",
"type": "dbd"
},
{
"id": "5d0667d2k87x7b0001da6613",
"filename": "Income Statement.dbd",
"title": "Income Statement",
"type": "dbd"
},
{
"id": "5d020a23h58e7b0001da64b1",
"filename": "HealthDashboard.dbd",
"title": "HealthDashboard",
"type": "dbd"
},
]
}
]
},
"errors": null
}
7. 대시보드 정보 가져오기(Get Dashboard Info)
대시보드의 매개변수와 기본값, 데이터셋 참조 정보를 조회합니다.
대시보드 정보를 가져오는 API 엔드포인트는 아래 GraphQL 쿼리에서 언급된 다른 인수와 함께 'document'입니다. 이 API 엔드포인트는 선택한 대시보드의 매개변수 목록과 함께 기본값, 그리고 매개변수 값의 출처 정보(예: 데이터세트에서 입력된 경우)를 반환합니다.
대시보드 정보에 데이터세트 참조가 있는 경우, 데이터세트 API 엔드포인트에 대한 추가 API 호출을 통해 매개변수에 사용 가능한 값을 가져와 드롭다운에 표시해야 합니다. 데이터세트 API 엔드포인트에는 데이터세트 ID와 열 이름이 인수로 필요합니다.
GraphQL 예시
query {
document(id: "${id}") {
... on Dashboard {
parameters {
name,
prompt,
hidden,
dataType,
dateOnly,
multiValue,
validValues {
datasetReference {
datasetId,
valueField
},
values {
label,
value
}
},
defaultValue {
values
}
}
}
}
}
API 호출
대시보드 정보:
POST {serverUrl}/api/GraphQL?token={token}
Content-Type: application/json
Body: {GraphQL query}
데이터 세트 값:
POST {serverurl}/api/pivot/datasets/{datasetid}/column-entries
Content-Type: application/json
Body: [{"columnId":"{columnname}","subColumnType":0}]
요청 예시
대시보드 정보 받기:
const getDashboardInfo = (token: string, serverURL: string) => async (id: string): Promise<ReportingTypes.DashboardInfoResponse | null> => {
const graphQuery = {
query: `query { document(id: "${id}") { ... on Dashboard { parameters { name, prompt, hidden, dataType, dateOnly, multiValue, validValues { datasetReference{ datasetId, valueField }, values { label, value } }, defaultValue { values } } } } }`
};
const response = await httpHelper.postJson(`${serverURL}/api/GraphQL?token=${token}`, graphQuery);
if ((response && response.errors) || (response.data.document.parameters && response.data.document.parameters.length === 0)) {
return null;
}
const { data: { document } } = response;
return document as ReportingTypes.DashboardInfoResponse;
}
응답 예시
대시보드 정보:
{
"data": {
"document": {
"parameters": [
{
"name": "Parameter1",
"prompt": "Parameter1",
"hidden": false,
"dataType": "String",
"dateOnly": false,
"multiValue": true,
"validValues": {
"datasetReference": {
"datasetId": "5d832bb6f23041001b31158a",
"valueField": "ShiftName"
},
"values": []
},
"defaultValue": {
"values": [
"Night",
"Morning"
]
}
}
]
}
},
"errors": null
}
데이터 세트 값:
[
{
"columnId": "ShiftName",
"subColumnType": 0,
"entries": [
"Night",
"Morning",
"Afternoon",
"Overall",
""
]
}
]
8. 대시보드 보기(View Dashboard)
매개변수 정보와 데이터세트의 값을 얻으면 드롭다운에서 값 목록을 선택하여 매개변수 값을 설정할 수 있습니다(매개변수에 기본값이나 데이터세트의 값이 있는 경우).
대시보드를 보려면 대시보드 ID와 매개변수 값을 사용하여 대시보드 URL을 생성해야 합니다.
URL이 생성되면 이를 iFrame의 소스로 설정하고 화면에서 대시보드를 볼 수 있습니다.
URL 예시
{serverUrl}/dashboards/view/${dashboardId}?token=${token}&dp={"Parameter1":["ParameterValue"]}
요청 예시
const viewDashboard = (token: string, serverURL: string) => async (
id: string,
parameters: { [key: string]: Array<any> }
) => {
var url = `/dashboards/view/${id}?token=${token}`;
let dp = "";
if (parameters && Object.keys(parameters) != null) {
dp = "&dp={";
var params = Object.values(parameters).reduce((k, v) => { return v });
Object.entries(params).map(([key, val]) => {
dp += Array.isArray(val)
? `\"${key}\":[${val.map(p => `\"${p}\"`).join(',')}] ,`
: `\"${key}\":[\"${val}\"] ,`;
});
dp = dp.slice(0, -1) + "}";
return url + dp;
} else {
return url;
}
}
응답 예시
/dashboards/view/5d8c59f14eca2600019d9858?token=dcf5d2f0400968294982c2664814c1cda5b6027f15c2359ce440927dc6035b87&dp={"Parameter1":["Night","Morning"]}
GraphiQL로 API 테스트하기
Wyn Enterprise는
{wynserverURL}/graphiql
경로에서 GraphQL API를 테스트할 수 있는 IDE를 제공합니다. Wyn 서버에 로그인한 상태여야 접근 가능합니다.
React 예제 프로젝트 다운로드
- Wyn-React 샘플 다운로드
- 설치
npm install
npm run start
로그인 후 보고서/대시보드 목록이 표시되며, 선택·실행·내보내기 기능을 사용할 수 있습니다.


