수월한 IT

RESTful API와 chart.js를 사용한 데이터 시각 본문

BOOKS/Spring Boot

RESTful API와 chart.js를 사용한 데이터 시각

IT여행자 2025. 3. 24. 15:12
728x90

안녕하세요~ IT 이곳저곳을 여행하고 있는 IT여행자입니다.

이번에 여행할 목적지는 스프링부트에서 RESTful API와 chart.js를 사용한 데이터 시각화 코스를 여행해 볼까 합니다. 기본적으로 스프링 부트에 대한 개념이 필요한 여행 코스이기는 하지만 관련 동영상과 본 문서를 참고하시면 그리 어렵지 않게 여행해 볼 수 있는 코스라 생각됩니다.

관련 영상에서는 아래와 같은 코스별로 진행되고 있지만 본 문서에서는 최종 코스의 코드만을 사용하여 설명하면서 코스를 여행하도록 하겠습니다. 단계별 과정이 궁금하시면 영상을 참조해 주시기 바랍니다.

 

 

[전체 여행 코스]

전체 여행 코스는 아래와 같습니다. 그러나 지면의 한계상 중간중간 코드의 변경 과정을 다루기엔 조금 어려움이 있어 중간 과정은 생략하고 각 코스별 완성된 코드로만 진행하도록 하겠습니다.

 

 

[여행 준비물]

  • 개발툴 : VSC(Visual Studio Code) 과 VSC의 Extension
    • Extension Pack for Java
    • Spring Boot Extension Pack
  • JDK Installed
  • chart.js (CDN유형으로 사용함)
  • 차트에 사용할 데이터는 DB를 사용하지 않고 임의의 데이터를 사용함.

 

[실행결과]

 

 

 


 

Spring  Boot 프로젝트 생성

(1) VSC에 Extension이 설치되었다면 Ctrl + Shift + p를 눌러 프로젝트 생성을 진행합니다.

(2) 입력창이 나오면 아래 그림과 같이 "Spring initializr:"검색하여 진행합니다.

(3) 다음은 아래의 표와 같이 적당한 값을 설정하여 진행하시면 됩니다.

프로젝트 종류 Spring initilaizr 로 검색후 선택
Spring Boot Version 3.4.4(각자 버전은 다를 수 있음)
project language Java
Group Id kr.jobtc (도메인이나 적당한 이름 가능)
Artifact Id restfulNchartjs(프로젝트명이 됨.)
packaging type War
Java version 17(각자 버전은 다를 수 있음)
dependencies Spring Web,
Spring Boot DevTool

 

프로젝트가 생성되고 초기화 되기 위해서 약간의 시간이 걸립니다.

 

HTML

CSS와 JavaScript 코드는 따로 분리하지 않고 하나의 HTML 파일에 작성되었습니다.

 

[resources/static/chart.html]

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- (1) -->
    <title>chart</title>
    <style>
      <!-- css -->
    </style>
</head>
<body>
    <div id="chart">
        <h2>Restful API and chart.js</h2>
        <div id="chart_area"> <!-- (2) -->
            <canvas id="mychart"></canvas> <!-- (3) -->
        </div>
        <div id="btnZone">
            <button type="button" id="btnMonth">월별</button>
            <button type="button" id="btnQuater">분기별</button>
            <label><input type="radio" name="chartType" class="chartType" value="bar" checked>BAR</label>
            <label><input type="radio" name="chartType" class="chartType" value="line">LINE</label>
            <label><input type="radio" name="chartType" class="chartType" value="pie">PIE</label>
            <button type="button" id="btnReload">새로고침</button>
        </div>
    </div>
    <script>
      // 자바스크립트 코드
    </script>
</body>
</html>

 

(1)  chart.js 라이브러리를 사용하기 위해 CDN형식으로 라이브러리를 가져오는 부분입니다. 공식사이트는 "http://chartjs.org"를 참고해 주시기 바랍니다.

(2)  차트의 크기나 위치를 지정할 때 <canvas/> 태그에 직접 작업하는 것보다 보다 유연하고 편리해서 <canvas/> 태그를 <div/> 태그로 감싸주었습니다.

(3) 차트를 그릴 때 사용되는 태그입니다. chart.js 라이브러리는 <canvas/> 태그를 사용하여 차트를 그립니다.

 

나머지는 월별, 분기별 버튼과 차트의 종류를 나타내는 라이오 버튼 3개, 그리고 차트를 새로운 데이터로 그리는 새로고침 버튼이 전부입니다.

 

CSS

#chart{
    width:600px;
}
#chart > h2{
    text-align: center;
}
#chart_area{  /* (1) */
    width:100%;
    height:400px;
    border:2px solid #aaa;
}
#btnZone{
    text-align: center;
}

 

몇 개 되지 않는 코드이기에 별도의 파일로 작성하여 <link/>로 가져오셔도 되고 <style/> 안에 작성해도 무방합니다. 본 지면에서는 후자를 선택하였습니다.

(1)  <canvas/> 태그를 감싸고 있는 <div/> 태그의 크기를 지정하면 차트의 크기가 자동으로 부모 태그의 넓이만큼 지정됩니다.

 

JavaScript

자바스크립트는 아래와 같이 이루어져 있습니다. 

let chart; //차트 객체
let config; // 차트 속성
let ctx = document.querySelector("#mychart");
// 차트에 표시될 데이터들
let month=[];
let sale =[];
let bgColor=["#f00","#00f","#0f0","#ff0"];
let data=[]
let quater = []

let type="bar"; // bar, line, pie
let byList = "월별";

async function loadData(byList){ //(1)
}

( async ()=>{ //(2)
    await loadData(byList); //(3)
    config={ //(4)
        data :{
            labels:data.map(d=>d.month), /*x축항목 */
            datasets:[ //(5)
            {
                type : type,
                data : data.map(d=>d.sale),
                backgroundColor : data.map(d=>d.bgColor),
            },
            ]
        },
        options:{ //(6)
            plugins:{
                title:{ //(7)
                    text:"매출현황",
                    display:true,
                },
                legend:{ //(8)
                    display:false,
                },
            }
        }
    }

    chart = new Chart(ctx, config); //(9)
})()

// 차트의 종류
let chartType = document.querySelectorAll(".chartType");
chartType.forEach(btn=>{
    btn.addEventListener("change", ()=>{
       ...
    })
})

// 월별
let btnMonth = document.querySelector("#btnMonth");
btnMonth.addEventListener("click", async ()=>{
   ...
})
// 분기별
let btnQuater = document.querySelector("#btnQuater");
btnQuater.addEventListener("click", async ()=>{
   ...
})

// 데이터 새로고침
let btnReload = document.querySelector("#btnReload");
btnReload.addEventListener("click", async ()=>{
   ...
})

 

코드 당산에는 차트를 그릴 때 필요한 변수들을 정의해 놨습니다.

(1) loadData(byList) 함수는 매개변수로 "월별" 또는 "분기별" 값을 전달받아 페이지가 새로 고침 되거나 페이지가 처음 열릴 때, 차트의 종류가 바뀔 때마다 호출되어 차트에 그릴 데이터를 RESTful API를 호출하여 가져옵니다.

(2) 즉시 실행함수를 정의하여 페이지가 로딩될 때 무조건 실행되어 loadData(byList)를 호출하여 데이터를 세팅합니다. 즉시 실행함수에 asyc를 사용하여 동기화 블록으로 설정한 이유는 fetch() 문장들이 비동기적으로 작동되기 때문에 RESTful API로부터 데이터가 모두 수신되기 전에 차트를 그리는 코드가 실행될 염려가 있기 때문입니다.

(3) await loadData(byList)로 함수를 호출하여 RESTful API로 부터 데이터가 모두 세팅될 때까지 기다리게 합니다.

(4) 차트를 그리기 위해 필요한 각종 환경값을 설정하는 부분입니다.

(5) 차트에 표시될 그래프별로 datasets:{ {첫 번째 그래프 데이터}, {두 번째 그래프 데이터}... ] 형식으로 작성됩니다. 현재는 하나의 차트영역에 하나의 그래프만 그립니다.

(6) 차트를 그릴 때 추가로 필요한 내용들을 작성하는 영역입니다.

(7) 차트의 제목을 설정합니다.

(8) 범례를 표시하는 부분인데, 하나의 그래프 밖에 없기 때문에 범례 표시를 감추게  하였습니다.

(9) <canvas/> 태그와 config 값을 사용하여 차트를 그립니다.

 

 

loadData(byList ) 함수

동기화 블록으로 작성되었습니다.  가장 중요한 부분이라 볼 수 있습니다. 데이터가 필요할 때마다 서버(RESTful API)를 호출하여 데이터를 세팅하는 부분입니다.(이후로는 RESTful API 호출을 서버호출이라 칭함)

async function loadData(byList){
    let param = { //(1)
        "byList" : byList
    }
    let resp = await fetch("/loadData", { //(2)
        method: "POST",
        headers:{
            "Content-Type" : "application/json",  //(3)
        },
        body: JSON.stringify(param) //(4)
    });
    resp = await resp.json(); //(5)
    if(byList=="월별") data = resp.result;
    else               quater = resp.result;
}

 

(1) 서버에게 전달할 파라미터값을 설정합니다. 파라미터 값은 "월별" 또는 "분기별"입니다.

(2) fetch() 함수를 사용하여 서버를 호출합니다. fetch() 함수는 비동기적으로 작동되기 때문에 서버로부터 데이터가 모두 전달되기 전에 하위에 있는 스크립트 코드가 실행되어 정상적이기 않은 데이터를 사용하여 차트가 그려질 염려가 있기에 await를 사용하여 동기적 함수 기능으로 바꾸었습니다. 물론 fetch().then()과 같은 구조로 사용할 수도 있지만 본 여행지에서는 async와 await문을 사용하였습니다.

(3) 데이터의 송수신 형식을 모두 JSON 형식으로 지정하겠다는 의미입니다.

(4) 서버에 전달할 때는 JSON Object를 그대로 전송할 수 없고 문자열로 바꾸어 전송합니다.

(5) 서버로 부터 수신된 데이터를 JSON Object 구조로 파싱 합니다. 그런 후에 byList 타입에 따라 수신된 데이터(resp.result) 값을 data 또는 quater에 대입해 둡니다. 여기에서 "resp.result"의 의미는 서버에서 resultMap.put("result", data)와 같이 Map의 키값이 "result" 이기 때문입니다.

 

차트의 종류 선택

사용자가 라디오 버튼으로 되어 있는 "bar", "line", "pie"중 하나를 선택되면 호출되는 이벤트 처리 부분입니다.

// 차트의 종류
let chartType = document.querySelectorAll(".chartType"); //(1)
chartType.forEach(btn=>{
    btn.addEventListener("change", ()=>{ //(2)
        type = btn.value;
        if(type=='pie'){ 
            config.data.labels=null; //(3)
        }else{
            if(byList=="월별"){ 
                config.data.labels=data.map(d=>d.month); //(4)
            }else{
                config.data.labels=quater.map(q=>q.month); //(5)
            }
        }
        config.data.datasets[0].type = type; //(6)
        chart.update(); //(7)
    })
})

 

(1) 차트의 종류를 나타내는 모든 라디오 버튼을 가져옵니다.

(2) 라디오 버튼의 상태값이 바귈때 처리되는 이벤트 핸들러입니다.

(3) 차트의 타입의 pie라면 x축의 항목을 없앱니다.

(4) 데이터의 구분이 '월별' 이라면 월항목을 x축의 항목에 할당합니다.

(5) 데이터의 구분이 '분기별' 이라면 월별과 같은 d.month이지만 분기별 데이터가 들어있습니다. 이를 x축의 항목에 할당합니다.

(6) 차트의 종류를 할당합니다.

(7) 재설정 차트 값을 사용하여 차트를 업데이트하여 차트의 내용이 바뀌도록 해줍니다.

 

월별 분기별 데이터

월별, 분기별 버튼이 클릭되면 처리되는 스크립트 부분입니다. 차트에 표시될 값이 d.month인가, 아니면 d.quater 인가 부분만 다르게 되어 있으므로 두 개의 함수를 같이 살펴보겠습니다.

let btnMonth = document.querySelector("#btnMonth");
btnMonth.addEventListener("click", async ()=>{ //(1)
    byList = "월별";
    await loadData(byList); //(2)
    config.options.plugins.title.text="월별 매출현황"; //(3)
    if(type=="pie"){ //(4)
        config.data.labels=null;
    }else{
        config.data.labels = data.map(d=>d.month); 
    }
    config.data.datasets[0].backgroundColor = data.map(d=>d.bgColor); //(5)
    config.data.datasets[0].data = data.map(d=>d.sale); //(6)
    chart.update();
})
// 분기별
let btnQuater = document.querySelector("#btnQuater");
btnQuater.addEventListener("click", async ()=>{ //(1)
    byList="분기별";
    await loadData(byList); //(2)
    config.options.plugins.title.text="뷴기별 매출현황"; //(3)
    if(type=="pie"){ //(4)
        config.data.labels=null;
    }else{
        config.data.labels = quater.map(q=>q.month);
    }
    config.data.datasets[0].backgroundColor = quater.map(q=>q.bgColor); //(5)
    config.data.datasets[0].data = quater.map(q=>q.sale); //(6)
    chart.update();
})

 

(1) 월별 또는 분기별 버튼이 클릭되면 실행되는 이벤트 핸들러입니다. 핸들러 또한 async로 처리되어 있습니다. loadData(byList) 함수가 모두 처리될 때까지 기다리도록 하기 위한 조치입니다.

(2) byList값에 따라 월별 또는 분기별 데이터를 설정하는 함수를 동기식으로 호출합니다.

(3) 차트의 제목을 바꿉니다.

(4) 차트의 종류가 'pie'라면 x축의 항목을 제거합니다.

(5) 그래프의 바탕색을 월별 또는 분기별 데이터에 맞는 색으로 설정합니다.

(6) d.month 또는 d.quater 데이터를 datasets[0] 번째 배열의 data에 대입함으로써 월별 또는 분기별 차트가 그려지도록 조치합니다.

 

데이터 새로고침

데이터 새로고침 버튼이 클릭되면 서버에서 데이터를 랜덤 하게 발생시켜 돌려줍니다.

// 데이터 새로고침
let btnReload = document.querySelector("#btnReload");
btnReload.addEventListener("click", async ()=>{
    await fetch("/createData") //(1)
    if(byList=="월별"){ //(2)
        btnMonth.click();
    }else{
        btnQuater.click();
    }
})

 

(1) 동기 방식으로 서버 매핑인 "/createData"를 호출함으로써 데이터가 새로 만들어지도록 요청합니다.

(2) 현재 byListe값에 의해 월별 또는 분기별 버튼에 클릭이벤트를 발생시켜 차트가 그려지도록 작성하였습니다.

 


자바스크립트 부분을 모두 살펴보았으니 이번 코스는 컨트롤러를 살펴볼 것입니다.

 

컨트롤러 클래스

package kr.jobtc.restfulandcharjs;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController //(1)
public class ChartController {

    @Autowired //(2)
    ChartDto dto;

    @PostMapping("/loadData") //(3)
    public Map<String, Object> loadData(@RequestBody Map<String, String> param ){ //(4)
        Map<String, Object> resultMap = new HashMap<>();
        List<Map<String, Object>> data = null;
        String byList = param.get("byList"); //(5)

        if(byList.equals("월별")) data = dto.getMonthData(); //(6)
        else                      data = dto.getQuaterData();

        resultMap.put("result", data); //(7)
        return resultMap;
    }

    @GetMapping("/createData") //(8)
    public void createData(){
        dto.createData();
    }

}

 

(1) @RestController 어노테이션이 RESTful API역할을 한다고 보면 될 것 같습니다.

(2) @Component로 지정된 ChartDto 클래스를 싱글톤 형태로 객체로 만들어 자동 연결합니다.

(3) 자바스크립트의 fetch("/loadData")에 의해 호출되는 매핑 주소입니다.

(4) 자바스크립트의 loadData(byList) 함수 내에서 fetch문을 사용하여 서버를 호출할 때 "body:JSON.stringify(param)" 부분과 연결되는 부분입니다. JSON객체를 문자열로 바꾸어 전송하면 전송된 값이 Map구조로 변환됩니다. 

(5) 전달된 param안에서 "월별" 또는 "분기별" 정보를 가져와 대입합니다.

(6) 월별, 분기별에 따른 데이터를 ChartDto 클래스에서 가져옵니다.

(7) 전달된 차트 데이터를 "result"키에 담아 클라이언트에게 돌려줍니다. 자바스크립트의 loadData() 함수 내부를 보면 data = resp.result 와 같은 문장이 있습니다. 이때 result값이 resultMap.put("result", data)에서 키값 "result"를 의미합니다.

(8) 랜덤 하게 새로운 데이터를 만들기 위해 사용되는 메핑주소입니다.

 

한 줄 한줄 모두 설명을 하지는 못했지만 대략적인 흐름은 이해되셨을 것 같습니다.

 


마지막으로 실제 데이터를 발생시키는 ChartDto 클래스 내용을 살펴보겠습니다.

ChartDto 클래스

package kr.jobtc.restfulandcharjs;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.springframework.stereotype.Component;

@Component //(1)
public class ChartDto {
    String[] bgColor ={"#f00","#00f","#0f0", "#ff0"}; //(2)
    List<Map<String, Object>> data;
    
    // db 대용
    public void createData(){
        data = new ArrayList<>();
        Random rnd = new Random();
        for(int month=1; month<=12 ; month++){ //(3)
            int sale = rnd.nextInt(191)+10; // 10~200
            int color = (int)Math.ceil(month/3.0)-1;
            Map<String, Object> map = new HashMap<>();
            map.put("month", month);
            map.put("sale", sale);
            map.put("bgColor", bgColor[color]);
            data.add(map);
        }
    }

    // 월별 데이터
    public List<Map<String, Object>> getMonthData(){ //(4)
        if(data==null) createData();
        return data;
    }

    // 분기별 데이터
    public List<Map<String, Object>> getQuaterData(){ //(5)
        if(data==null) createData();
        List<Map<String, Object>> quaterData = new ArrayList();
        int[] saleData = new int[4];
        for(Map<String, Object> m : data){ //(6)
            Integer month = (Integer)m.get("month");
            Integer sale = (Integer)m.get("sale");
            Integer quater = (int)Math.ceil(month/3.0)-1; //(7)
            saleData[quater] += sale;
        }

        for(int i=0; i<saleData.length ; i++){ //(8)
            Map<String, Object> map = new HashMap<>();
            map.put("month", (i+1));
            map.put("sale", saleData[i]);
            map.put("bgColor", bgColor[i]);
            quaterData.add(map);
        }
        return quaterData;
    }
}

 

(1) @Component로 지정된 클래스는 스프링에서 하나의 부품역할을 하는 클래스가 됩니다. 이는 @RestController에서 @Autowired 어노테이션을 사용하여 자동 주입할 수 있습니다.

(2) 각 분기별이나 각 분기에 해당하는 월 그래프의 바탕색을 지정하기 위한 배열입니다.

(3) 1월~12월까지의 데이터를 랜덤 하게 발생시켜 list에 저장합니다.

(4) data객체에 값이 없다면 createData()를 호출하여 월별 데이터를 생성합니다.

(5) 월별 데이터를 사용하여 각 분기별 데이터를 생성하는 메서드입니다.

(6) 각 월별 판매액(sale)을 각 분기에 맞게 누적 연산하기 위한 반복문입니다.

(7) 월에 해당하는 숫자를 0~3까지의 숫자로 바꾸어 분기를 의미하는 배열 첨자로 사용하기 위한 계산식입니다.

(8) 분기별 데이터를 하나의 map을 만들어 반환합니다. 만들어진 데이터의 구조는 아래의 예시와 같습니다.

[

  { "month" : 1 , "sale" : 1000, "bgColor" : "#f00"},

  { "month" : 2 , "sale" : 2000, "bgColor" : "#00f"},

  { "month" : 3 , "sale" : 3000, "bgColor" : "#0f0"},

  { "month" : 4 , "sale" : 4000, "bgColor" : "#ff0"}

]

 

이것으로 RESTful API와 chart.js 라이브러리를 사용한 데이터 시각화 여행 코스를 마무리짓겠습니다.

 

저는 또 다른 IT여행지에서 찾아뵙겠습니다. 감사합니다.

 

관련 영상 : https://youtu.be/HE34s3ciz2s
관련 코드 : https://github.com/hiparkwg/restfulandcharjs.git