IT_Study/Personal Project

API 활용 Web 프로젝트 (2) : Naver Datalab API를 활용한 Data Visualization 하기

__Vivacé__ 2023. 3. 17. 17:08

요구사항 요약

 

A. API Server Build

i. Express에서 API를 가져오기

ii. API를 axios로 가져와서 return하는 project 생성

B. Vue.js로 Visualization

i. axios로 express 단에 data를 request

ii. 받아온 data를 chart로 표현

 


0. Default Setting

Express 서버는 8081번 포트, Vue 서버는 8080번 포트로 진행 예정

frontend

$ vue create .
# 유라 보고 설정 보기

$ npm i chart.js@2.9 vue-chartjs@3.5 axios moment

backend

$ npm init
$ npm i morgan axios cors dotenv express

 


1. Naver Developer API 등록

네이버 개발자 센터 홈페이지 접속

 

상단 탭 [Application] → [Application 등록]

 

비로그인 오픈 API 서비스 환경 설정

127.0.0.1 → localhost에서 접근을 허용하겠다는 의미

나중에 배포 하려면, API 허용 주소를 본인의 AWS Address를 기입하면 된다.

 


2. 코드 작성

 

기입 후, 나오는 애플리케이션 정보를 .env 파일에 넣음

 

// backend/index.js 파일 내용

// 웹 애플리케이션 프레임워크로써, 라우팅, 미들웨어 등 기능을 제공
const express = require('express');
const app = express();

// CORS(Cross-Origin Resource Sharing)를 설정하고, 보안을 강화할 수 있음
const cors = require('cors');

// HTTP 클라이언트 라이브러리로써, 서버 간 통신이나 외부 API를 호출하는 데 사용
const axios = require('axios');

// HTTP 요청 로거 미들웨어로써, 로그를 기록하는 데 사용
const morgan = require('morgan');

// 서버가 리스닝할 포트를 8081로 설정
const PORT = 8081;

// .env 파일을 읽어올 수 있게
const dotenv = require('dotenv');
// dotenv 설정을 불러오고, 환경 변수를 프로세스 환경 변수에 할당
dotenv.config();

// JSON 형식의 요청 본문을 처리할 수 있도록 express.json 미들웨어를 사용
app.use(express.json());

// 개발 환경용 로깅 포맷인 'dev'를 사용하여 morgan 미들웨어를 적용
// -> HTTP 요청에 대한 로그를 기록
app.use(morgan('dev'));

// CORS 미들웨어를 적용 -> 다른 도메인의 클라이언트와 리소스를 공유
app.use(cors());

app.get("/", (req, res) => {
    // dotenv에 작성한 내용 가져오는 방법
    // process.env.[작성한 변수 명]
    console.log(process.env.CLIENT_ID);

    return res.json({ test: "HELLO"});
})

// 파일을 읽어서 리턴
app.get("/api/data", (req, res)=>{

})

// 요청을 보내고, 값을 파일로 저장
app.post("/api/data", (req, res)=>{

})

// 파일을 삭제
app.delete("/api/data", (req, res)=>{

})

// 설정한 포트(8081)로 서버를 시작
app.listen(PORT, () => console.log(`${PORT} 서버 기동 중`));

POST 정의

1. Naver API Document에 있는 Node.js를 참고하여 작성

// backend/index.js 파일 내용 중 post 부분

// 치킨과 삼겹살을 검색해서 트렌드 조회

app.post("/api/data", async(req, res)=>{
    // document에 나와 있는 url 주소
    const url = '<https://openapi.naver.com/v1/datalab/search>';
    // document에 나와 있는 header setting
    const headers = {
        'X-Naver-Client-Id': process.env.CLIENT_ID,
        'X-Naver-Client-Secret': process.env.CLIENT_SECRET,
        'Content-Type': 'application/json'
    }

    // Document의 API reference 참고해서 params 넣어줄 것
    const request_body = {
        startDate: "2022-01-01",
        endDate: "2023-03-17",
        timeUnit: "month", // "date" or "week" or "month"
        
        // {주제어, 검색어 묶음) 최대 5개의 쌍을 배열로 설정 가능
        keywordGroups: [
            {
                groupName: "치킨", // 검색어 묶음을 대표하는 이름
                keywords: ["치킨", "BBQ", "BHC"] // 검색어 최대 20개 가능
            },
            {
                groupName: "삼겹살",
                keywords: ["삼겹살", "고기"]
            }
        ]
    }

    // async & await를 사용하기 위해 try-catch 사용
    try {
        // Document의 양식에 따라 작성
        const result = await axios.post(url, request_body, {
            headers: headers
        })
        console.log(result);

        return res.json(result.data.results);
    } 
    catch (error) {
        console.log(error);
        return res.json(error);
    }
})

 

결과


2. Postman에서 Body 작성 후 값 보내기

// backend/index.js 파일 내용 중 post 부분 수정

app.post("/api/data", async(req, res)=>{
    const url = '<https://openapi.naver.com/v1/datalab/search>';
    const headers = {
        'X-Naver-Client-Id': process.env.CLIENT_ID,
        'X-Naver-Client-Secret': process.env.CLIENT_SECRET,
        'Content-Type': 'application/json'
    }

    const request_body = {
        startDate: req.body.startDate,
        endDate: req.body.endDate,
        timeUnit: req.body.timeUnit,
        keywordGroups: req.body.keywordGroups
    }

    // ... try-catch 생략
})

 

결과

 


API Result를 파일로 저장하기

// backend/uploads 폴더 생성 후 진행

// backend/index.js에 해당 부분 추가 작성

// 파일 시스템과 관련된 기능을 제공
// 파일 읽고 쓰기, 디렉토리 생성 & 삭제, 파일 권한 설정 등 가능
const fs = require('fs');

// ..중략..

app.post("/api/data", async(req, res)=>{
    const url = '<https://openapi.naver.com/v1/datalab/search>';
    const headers = {
        'X-Naver-Client-Id': process.env.CLIENT_ID,
        'X-Naver-Client-Secret': process.env.CLIENT_SECRET,
        'Content-Type': 'application/json'
    }

    const request_body = {
        startDate: "2022-01-01",
        endDate: "2023-03-17",
        timeUnit: "month",
        keywordGroups: [
            {
                groupName: "치킨",
                keywords: ["치킨", "BBQ", "BHC"]
            },
            {
                groupName: "삼겹살",
                keywords: ["삼겹살", "고기"]
            }
        ]
    }

    try {
        const result = await axios.post(url, request_body, {
            headers: headers
        })
        console.log(result);
        
        // result.data.result를 파일로 저장할 것

        // JSON.stringify : "JSON"을 "문자열"로
        // JSON.parse : "문자열"을 "JSON"으로
        fs.writeFile("./uploads/chart.json", JSON.stringify(result.data.results), function (err){
            console.log(err);
        })

        return res.json(result.data.results);
    } 
    catch (error) {
        console.log(error);
        return res.json(error);
    }
})

 

파일로 저장된 json 파일을 읽어오기

app.get("/api/data", (req, res)=>{
    // uploads/chart.json을 읽어서 return
    try {
        fs.readFile('./uploads/chart.json', 'utf8', (error, data) => {
            if (error){
                console.log(error);
            }

            return res.json(JSON.parse(data));
        })
    } 
    catch (error) {
        console.log(error);
        return res.json(error);
    }
})

 

결과

 


전체 코드

// 웹 애플리케이션 프레임워크로써, 라우팅, 미들웨어 등 기능을 제공
const express = require('express');
const app = express();

// CORS(Cross-Origin Resource Sharing)를 설정하고, 보안을 강화할 수 있음
const cors = require('cors');

// HTTP 클라이언트 라이브러리로써, 서버 간 통신이나 외부 API를 호출하는 데 사용
const axios = require('axios');

// HTTP 요청 로거 미들웨어로써, 로그를 기록하는 데 사용
const morgan = require('morgan');

// 서버가 리스닝할 포트를 8081로 설정
const PORT = 8081;

// 파일 시스템과 관련된 기능을 제공
// 파일 읽고 쓰기, 디렉토리 생성 & 삭제, 파일 권한 설정 등 가능
const fs = require('fs');

// .env 파일을 읽어올 수 있게
const dotenv = require('dotenv');
// dotenv 설정을 불러오고, 환경 변수를 프로세스 환경 변수에 할당
dotenv.config();

// JSON 형식의 요청 본문을 처리할 수 있도록 express.json 미들웨어를 사용
app.use(express.json());

// 개발 환경용 로깅 포맷인 'dev'를 사용하여 morgan 미들웨어를 적용
// -> HTTP 요청에 대한 로그를 기록
app.use(morgan('dev'));

// CORS 미들웨어를 적용 -> 다른 도메인의 클라이언트와 리소스를 공유
app.use(cors());

app.get("/", (req, res) => {
    // dotenv에 작성한 내용 가져오는 방법
    // process.env.[작성한 변수 명]
    console.log(process.env.CLIENT_ID);

    return res.json({ test: "HELLO"});
})

// 파일을 읽어서 리턴
app.get("/api/data", (req, res)=>{
    // uploads/chart.json을 읽어서 return
    try {
        fs.readFile('./uploads/chart.json', 'utf8', (error, data) => {
            if (error){
                console.log(error);
            }

            return res.json(JSON.parse(data));
        })
    } 
    catch (error) {
        console.log(error);
        return res.json(error);
    }
})

// 요청을 보내고, 값을 파일로 저장
// 치킨과 삼겹살을 검색해서 트렌드 조회

app.post("/api/data", async(req, res)=>{
    const url = '<https://openapi.naver.com/v1/datalab/search>';
    const headers = {
        'X-Naver-Client-Id': process.env.CLIENT_ID,
        'X-Naver-Client-Secret': process.env.CLIENT_SECRET,
        'Content-Type': 'application/json'
    }

    const request_body = {
        startDate: "2022-01-01",
        endDate: "2023-03-17",
        timeUnit: "month",
        keywordGroups: [
            {
                groupName: "치킨",
                keywords: ["치킨", "BBQ", "BHC"]
            },
            {
                groupName: "삼겹살",
                keywords: ["삼겹살", "고기"]
            }
        ]
    }

    try {
        const result = await axios.post(url, request_body, {
            headers: headers
        })
        console.log(result);

        fs.writeFile("./uploads/chart.json", JSON.stringify(result.data.results), function (err){
            console.log(err);
        })

        return res.json(result.data.results);
    } 
    catch (error) {
        console.log(error);
        return res.json(error);
    }
})

// 파일을 삭제
app.delete("/api/data", (req, res)=>{

})

// 설정한 포트(8081)로 서버를 시작
app.listen(PORT, () => console.log(`${PORT} 서버 기동 중`));

 


Front-end

 

main.js 주석 처리

// frontend/src/main.js 내용

import Vue from 'vue'
import App from './App.vue'
// import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  // router,
  store,
  render: h => h(App)
}).$mount('#app')

App.vue 내용 삭제

// frontend/src/App.vue 내용

<template>
  <div id="app">

  </div>
</template>

<style>

</style>

Components 폴더에 ReactiveBarChart.js 추가

// frontend/src/components/ReactiveBarChart.js

// Line : 선형 차트를 그리는 데 사용되는 클래스
// mixins : 재사용 가능한 Vue 컴포넌트 로직을 구현하는 데 사용
import {Line, mixins} from "vue-chartjs"

// 현재 모듈에서 기본으로 내보낼 Vue 컴포넌트 객체를 정의
export default{
    // 이 컴포넌트는 Line 클래스를 확장 -> 선형 차트를 그리는 기능을 상속받음
    extends: Line,
    // 'chartData' prop이 변경될 때마다 차트를 다시 렌더링하도록 만듦
    mixins: [mixins.reactiveProp],
    // 차트의 옵션을 설정하는 데 사용
    props: ['options'],
    
    // 컴포넌트가 DOM에 마운트된 직후에 호출
    mounted(){
        this.renderChart(this.chartData, this.options)
    }
}

App.vue 내용 추가

<template>
  <div id="app">
    <!-- ReaciveBarChart -->
    <reactive-bar-chart
      style="width:40vw"
      :chart-data="$store.state.chartData" // 차트 데이터를 Vuex 스토어에서 가져옴
    >
    </reactive-bar-chart>

  </div>
</template>

<script>
import ReactiveBarChart from "./components/ReactiveBarChat"

  export default{
    components: {
      ReactiveBarChart
    },
    
    data(){
      return{
        chartData: null  // 차트 데이터를 저장할 변수 초기화
      }
    },

    mounted(){
      this.generateData(); // 컴포넌트가 마운트되면 generateData 메소드 호출

      // 2초마다 generateData 메소드를 호출하여 차트 데이터 업데이트
      setInterval(this.generateData, 2000); 

      // Vuex 스토어의 generateChartData 액션을 호출
      this.$store.dispatch("generateChartData");
    },

    // 차트 데이터를 생성하는 메소드
    methods: {
      generateData() {
        let newArray = [];
        let newArray2 = [];

        for (let i = 0; i < 10; i++) {
          let randomValue = Math.floor(Math.random() * 10);
          newArray.push(randomValue);
        }

        for (let i = 0; i < 10; i++) {
          let randomValue = Math.floor(Math.random() * 10);
          newArray2.push(randomValue);
        }

        this.chartData = {
          labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
          datasets: [
            {
              label: "Data One",
              backgroundColor: "#f87979",
              data: newArray,
              fill: false,
            },
            {
              label: "Data One",
              backgroundColor: "#f87979",
              data: newArray2,
              fill: false,
            },
          ],
        };

      }
    }
  }
</script>

<style>

</style>

 

결과

Data가 2초마다 동적으로 변하는 그래프가 만들어짐

 

 


앞으로 할 내용

 

버튼을 누르면 API에서 데이터를 가져와 시각화할 예정

이 데이터를 vuex에서 관리 → 비동기로 데이터를 받아서 관리할 예정

 


api.js 작성

// frontend/src/utils/api.js 에 작성

const axios = require('axios');

// axios 인스턴스를 생성하고 api 변수에 할당
const api = axios.create({
    // Backend Address로 설정
    baseURL: '<http://localhost:8081/api>'
});

export const dataLap = {
    get: () => {
        return api.get("/data");
    },

    post: (data) => {
        return api.post("/data", data);
    }
}

index.js 작성

// frontend/src/store/index.js 에 작성

import Vue from 'vue'
import Vuex from 'vuex'
import { dataLap } from "../utils/api"

Vue.use(Vuex)

// 임의의 색상을 생성하는 함수
function makeColor(){
  return "#" + Math.round(Math.random() * 0xffffff).toString(16);
}

export default new Vuex.Store({
  state: {
    chartData: {}
  },
  getters: {
  },
  mutations: {
    CHANGE_CHART_DATA(state, data){
      state.chartData = data;
    }
  },

  // 비동기 처리를 하면서 state를 변경하는 경우 actions 사용
  // 차트의 데이터가 localhost:8081/api/data
  actions: {
    async generateChartData({commit}){

      // API 요청을 통해 데이터를 가져옴
      const result = await dataLap.get();
      console.log(result.data);
      
      // 가져온 데이터를 차트 데이터 형식에 맞게 변환
      const chartData = {
        labels: result.data[0].data.map(li => li.period),
        datasets: result.data.reduce((acc, cur) => {
          //cur.label-> cur.title로 변경
          const label = cur.title;
          const data = cur.data.map(li => li.ratio);
          acc.push({label:label, data: data, fill:false, backgroundColor:makeColor(),
          borderColor:makeColor()})
          return acc
        }, [])
      }
      
      // 변환한 차트 데이터를 뮤테이션을 통해 상태에 반영
      commit("CHANGE_CHART_DATA", chartData)

    }
  },
  modules: {
  }
})

Component에 Form.vue 추가

<template>
    <div>
        <br>
        <!-- startDate -->
        <div>
            시작일: <input type="date" v-model="startDate">
        </div>
        <!-- endDate -->
        <div>
            종료일: <input type="date" v-model="endDate">
        </div>
        <!-- timeUnit -->
        <select v-model="timeUnit">
            <option value="date">일간</option>
            <option value="week">주간</option>
            <option value="month">월간</option>
        </select>
        <!-- KeywordGroups -->

        <!-- 그룹 안에 각자 groupName, keywords가 필요하다 -->
        <!-- 1차로 팀 명을 만들고, 그 안에 groupName, keywords 정의 -->

        <div>
            그룹명: <input v-model="userInputGroupName">
            <button @click="tempGroupAdd"> 추가 </button>

            {{tempGroupName}}
        </div>

        <div>
            키워드: <input v-model="userInputKeyword">
            <button @click="tempKeywordAdd"> 추가 </button>
        
            <div v-if="tempKeywords.length">
                추가된 키워드
                <div v-for="temp in tempKeywords" :key="temp">
                    {{temp}}
                </div> 
            </div>
        </div>

        <div>
            <button @click="makeGroup">그룹 확정</button>
        </div>

        <div>
            확정된 그룹
            <div v-for="keywordGroup in keywordGroups" :key="keywordGroup">
                <div>그룹 이름: {{keywordGroup.groupName}}</div>
                <div>그룹 키워드: {{keywordGroup.keywords}}</div>
            </div>
        </div>

        <div>
            <button @click="submitForm">제출</button>
        </div>
    </div>
</template>

<script>
import {dataLap} from "../utils/api"

export default {
    data(){
        return{
            startDate: "",
            endDate: "",
            timeUnit: "",
            keywordGroups: [],
            userInputGroupName: "",
            userInputKeyword: "",
            tempGroupName: "",
            tempKeywords: []
        }
    },
    methods:{
        // {groupName: "치킨", keywords:['BBQ', 'BHC']}을 submit해야 함
        tempGroupAdd(){
            this.tempGroupName = this.userInputGroupName;
        },

        tempKeywordAdd(){
            this.tempKeywords.push(this.userInputKeyword);
        },

        makeGroup(){
            this.keywordGroups.push({
                groupName: this.tempGroupName,
                keywords: this.tempKeywords
            });
            this.tempGroupName = "";
            this.tempKeywords = [];
        },

        async submitForm(){
            // api POST 요청을 보냄
            const result = await dataLap.post({
                startDate: this.startDate,
                endDate: this.endDate,
                timeUnit: this.timeUnit,
                keywordGroups: this.keywordGroups
            });

            console.log(result);
            this.$store.dispatch("generateChartData");
        }
    }
}
</script>

<style>

</style>

App.vue에 Form 추가

// frontend/src/App.vue 에 해당 내용 추가

<template>
  <div id="app">
    <!-- ReaciveBarChart -->
    <reactive-bar-chart
      style="width:40vw"
      :chart-data="$store.state.chartData"
    >
    </reactive-bar-chart>
    <Form></Form>
  </div>
</template>

<script>
import ReactiveBarChart from "./components/ReactiveBarChat"
import Form from "./components/Form.vue"

  export default{
    components: {
      ReactiveBarChart,
      Form
    },

// ...생략...

 

결과

[시작일], [종료일], [날짜], [그룹명], [키워드] 입력후 제출 시, 그래프가 구성되는 걸 수 있음