API 정의 + 네이버 블로그 검색 API 예제

진실·2021년 7월 29일
2
post-thumbnail

API란

웹 개발을 하는 분들이라면 네이버 오픈 API, 카카오 오픈 API 등의 표현을 한번 씩 접해보았을 것입니다.
보통 이런 API에서는 서버의 기능을 일부 제공해주는데요, 대표적으로 '~를 통한 로그인'의 기능이 있습니다.

API : Application Programming Interface
API는 다른 어플리케이션에서 현재 프로그램의 기능을 사용할 수 있도록 해주는 인터페이스를 뜻합니다.

따라서 네이버 로그인 API, 네이버 검색 API 등은 각각 네이버에서 제공하는 로그인, 검색 기능을 다른 어플리케이션에서도 사용할 수 있도록 하는 매개체가 되는 것입니다.

하지만 API를 누구나 사용할 수 있는 것은 아닙니다. API를 개방한 서버 측에서는 API를 사용하는 어플리케이션에 API 키(=client secret key)를 발급해줍니다. 따라서 이 API 키를 가진 도메인에서만 서버의 기능을 사용할 수 있게 됩니다.

그리고 API를 개방한 서버 측에서는 기능의 사용 횟수에 제한을 걸기도 합니다. 어플리케이션 측에서 검색 결과와 같이 서버에 부하가 걸릴 수 있는 API를 과도하게 사용하게 될 경우에는 서버에 무리가 가기 때문입니다.

다만 API를 제공하지 않는 서버도 있습니다. 이러한 경우에는 어플리케이션이 직접 크롤링을 해서 서버의 데이터를 수집합니다.

API 사용 예시 - 1

Node.js를 기준으로 예시를 작성했습니다.

  1. https://developers.naver.com/apps/#/register

    위 링크에 들어가서 사용할 어플리케이션을 등록해 줍니다

  2. 사용 API에 "검색"을 추가합니다.

  3. 발급 받은 CLIENT ID와 CLIENT SECRET을 변수에 넣어 줍니다.

  4. 모듈을 실행시킵니다.

// 네이버 검색 API예제는 블로그를 비롯 전문자료까지 호출방법이 동일하므로 blog검색만 대표로 예제를 올렸습니다.
// 네이버 검색 Open API 예제 - 블로그 검색
var express = require('express');
var app = express();
var client_id = 'YOUR_CLIENT_ID'; // 발급받은 CLIENT ID를 넣어줍니다.
var client_secret = 'YOUR_CLIENT_SECRET'; // 발급받은 CLIENT SECRET을 넣어줍니다.
app.get('/search/blog', function (req, res) {
   var api_url = 'https://openapi.naver.com/v1/search/blog?query=' + encodeURI(req.query.query); // json 결과
//   var api_url = 'https://openapi.naver.com/v1/search/blog.xml?query=' + encodeURI(req.query.query); // xml 결과
   var request = require('request');
   var options = {
       url: api_url,
       headers: {'X-Naver-Client-Id':client_id, 'X-Naver-Client-Secret': client_secret}
    };
   request.get(options, function (error, response, body) {
     if (!error && response.statusCode == 200) {
       res.writeHead(200, {'Content-Type': 'text/json;charset=utf-8'});
       res.end(body);
     } else {
       res.status(response.statusCode).end();
       console.log('error = ' + response.statusCode);
     }
   });
 });
 app.listen(3000, function () {
   console.log('http://127.0.0.1:3000/search/blog?query=검색어 app listening on port 3000!');
 });

코드 출처 - 네이버 개발자 센터

  1. 실행 결과

http://localhost:3000/saerch/blog?query="노티드 도넛"
에 대한 검색 결과입니다.

{
"lastBuildDate": "Wed, 28 Jul 2021 18:45:57 +0900",
"total": 29856,
"start": 1,
"display": 10,
"items": [
{
"title": "핫한 제주 <b>노티드 도넛</b> 다녀온 후기!(메뉴 추천)",
"link": "https:\/\/blog.naver.com\/betelgiuse?Redirect=Log&logNo=222441433706",
"description": "시간 관게없이 4,000원이 발생하고 <b>노티드</b> 영수증을 지참하시면 2,000원을 할인해준답니다. 또 이곳의... 그리고 제주 <b>노티드 도넛</b><b>도넛</b> 종류 말고도 쿠키와 크로플 종류들도 준비되어 있으니 취향에 따라 다른... ",
"bloggername": "굿린",
"bloggerlink": "https://blog.naver.com/betelgiuse",
"postdate": "20210722"

},
{
"title": "제주 애월 카페 <b>노티드 도넛</b> 또 먹어보고 싶다",
"link": "https:\/\/blog.naver.com\/jujupnara2?Redirect=Log&logNo=222418598086",
"description": "<b>노티드</b> 식빵 이벤트 포스터 색칠놀이 할 수 있는 귀여운 밑그림과 스마일 스티커가 있었는데 제한이 없어 조카들이랑 제꺼 스티커만 색깔별로 챙김'ㅅ' 제주 애월 카페 <b>노티드 도넛</b> 컵이며 <b>도넛</b>박스며..... ",
"bloggername": "밥벌레의 잡스런 이야기",
"bloggerlink": "https://blog.naver.com/jujupnara2",
"postdate": "20210703"

},
{
"title": "제주 <b>노티드도넛</b> 애월점 이용 후기! 주차 방법과 메뉴 추천까지",
"link": "https:\/\/blog.naver.com\/nce0623?Redirect=Log&logNo=222438813570",
"description": "카페 <b>노티드</b> 제주'를 검색해가면 알아서 주차장 쪽으로 안내하더라고요. 또한 제주 <b>노티드도넛</b> 애월점... ■ <b>도넛</b> 메뉴 메뉴는 줄 서면서도 중간중간 세워져 있는 입간판에서 확인할 수 있습니다. 단종된 메뉴... ",
"bloggername": "에릭의 쉽지 않은 일상",
"bloggerlink": "https://blog.naver.com/nce0623",
"postdate": "20210721"

},
{
"title": "제주 <b>노티드도넛</b> 메뉴 추천 예약 스마트주문 정보",
"link": "https:\/\/blog.naver.com\/river0703?Redirect=Log&logNo=222412634118",
"description": "박혀나와 <b>도넛</b>을 담아주는거 같던데, 제주 <b>노티드 도넛</b>은 스티거를 맘껏 붙일 수 있게 되어 있었다.... 맛은 아니었지만 유명한 곳에서 먹어봤다는걸로 만족했다. 카페 <b>노티드</b> 제주 메뉴 추천은 그냥 우유크림 하나!",
"bloggername": "깡스의 사진찍는 블로그",
"bloggerlink": "https://blog.naver.com/river0703",
"postdate": "20210628"

},
{
"title": "제주도애월카페 <b>노티드 도넛</b> 안먹으면 서운~",
"link": "https:\/\/blog.naver.com\/jara777?Redirect=Log&logNo=222447635221",
"description": "요기 <b>노티드 도넛</b>이었어요 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 이 곳 영업시간은 오전 10시부터 오후 8시... 알림오면 그 긴 줄을 뚫고 가실 수 있답니다 이상 뚜뚜의 제주도 애월카페 <b>노티드</b> 제주 후기마칠게요 뿅",
"bloggername": "뚜뚜의 신혼일기:)",
"bloggerlink": "https://blog.naver.com/jara777",
"postdate": "20210727"

},
{
"title": "압구정로데오 카페 <b>노티드</b> 청담 : <b>도넛</b> 메뉴 추천~",
"link": "https:\/\/blog.naver.com\/goldpfeil?Redirect=Log&logNo=222431770831",
"description": "압구정로데오 카페 <b>노티드</b> 청담 <b>도넛</b>메뉴 추천!! SNS에서 한동안 난리가 났던 카페 <b>노티드 도넛</b>!!! 약간 뒷북(?)이지만.. 저도 맛보고 왔습니다 ㅎㅎ 지난주 압구정로데오 갔다가 카페 <b>노티드</b> 청담에... ",
"bloggername": "허브허브의 여행 & 패션",
"bloggerlink": "https://blog.naver.com/goldpfeil",
"postdate": "20210715"

},
{
"title": "<b>노티드도넛</b> 카페 <b>노티드</b> 한남 테이크아웃 뒷북후기",
"link": "https:\/\/blog.naver.com\/vicki79?Redirect=Log&logNo=222446746523",
"description": "곳은 #카페<b>노티드</b>한남 블루스퀘어에서 뮤지컬보고 걸어서 방문했다. 블루스퀘어에서 걸어가기에 멀지... 해치운 <b>노티드도넛</b> <b>도넛</b>의 빵 부분이 튀긴 부분이라 냉장보관시 식감이 딱딱해 질 수 있다고 설명서에... ",
"bloggername": "메이니 일상의 작은 즐거움♪",
"bloggerlink": "https://blog.naver.com/vicki79",
"postdate": "20210727"

},
{
"title": "서울숲 카페 오브코하우스 <b>노티드 도넛</b> 있어요",
"link": "https:\/\/blog.naver.com\/baemju?Redirect=Log&logNo=222424996829",
"description": "뚝섬역 근처 블루보틀에 갔다가 2차로 가본 곳이에요 ㅎㅎ 언젠가 꼭 먹어봐야지 했던 <b>노티드도</b>... 드디어 <b>노티드 도넛</b>을 먹어보다니요,, 딱히 <b>도넛</b>을 좋아하는건 아닌데 sns에서 사진으로 워낙 많이... ",
"bloggername": "민주 일기",
"bloggerlink": "https://blog.naver.com/baemju",
"postdate": "20210708"

},
{
"title": "안국역 카페 <b>노티드</b> Knotted <b>도넛</b> 맛집",
"link": "https:\/\/blog.naver.com\/sodam3826?Redirect=Log&logNo=222438605962",
"description": "저번에 다녀온 안국역 카페 <b>노티드</b>  드디어 <b>노티드 도넛</b>을 먹어보았습니다!!!! 그동안 지나가면서... 때문에 요즘 만나는 사람은 가족과 직장 동료들과 남자친구 뿐…ㅋ) 안국역 카페 <b>노티드도넛</b> <b>도넛</b> 맛집  추천해요",
"bloggername": "기억, 추억, 생각",
"bloggerlink": "https://blog.naver.com/sodam3826",
"postdate": "20210720"

},
{
"title": "<b>노티드 도넛</b> 성수 피치스도원 핫플예감",
"link": "https:\/\/blog.naver.com\/mpburberry?Redirect=Log&logNo=222338106169",
"description": "<b>도넛</b>은 저번에 잠실 <b>노티드</b> 방문했을 때 맛났던 카야잼이 없어서 아쉬웠는데 대신 그때 품절이라 못 먹었던 밀크와 바닐라를 하나씩 구매했어요. 밀크는 <b>노티드 도넛</b> 중에서 베스트 상품이기도 하고 꼭 먹고... ",
"bloggername": "Mr.Lee 블로그",
"bloggerlink": "https://blog.naver.com/mpburberry",
"postdate": "20210504"

}
]
}

이렇게 json 형식으로 "노티드 도넛"에 대한 결과가 제공됩니다.
이제 어플리케이션은 검색 결과를 활용해서 새롭게 가공할 수 있습니다.

API 사용 예시 - 2

위와 같은 JSON 형태는 가시성이 떨어지므로 간단하게 css를 적용해보았습니다.

메인 페이지

검색 결과

"안국역 카페"에 대한 검색 결과입니다.
제목을 누르면 해당 블로그 포스트로 이동하도록 하였습니다.

app.js

var express = require('express');
var nunjucks = require('nunjucks');
var axios = require('axios');
var app = express();
var client_id = 'CLIENTID_ID';
var client_secret = 'CLIENT_SECRET';
// replace client id and secret with your owns

app.set('view engine', 'html');

nunjucks.configure('views', {
  express : app,
  watch : true,
})

app.use(express.json());
app.use(express.urlencoded({extended : false}));

app.get('/', (req, res)=>{
  res.render('main');
});

app.post('/search', async(req, res)=>{
  query = req.body.query;
  // get query input

  var api_url = 'https://openapi.naver.com/v1/search/blog?query=' + encodeURI(query);
  var options = {
    headers : {'X-Naver-Client-Id':client_id, 'X-Naver-Client-Secret': client_secret}
  }; // headers for get request
  await axios.get(api_url, options)
  .then((response)=>{
    if(response.status===200){
      items = response.data.items;
      items.map((x)=>{
        x.title = x.title.replace(/<b>/g, '');
        x.title = x.title.replace(/<\/b>/g, '');
        x.description = x.description.replace(/<b>/g, '');
        x.description = x.description.replace(/<\/b>/g, '');
      }); // remove html tags in query result
   
      res.render('result', {items : items});
    }
  })
  .catch((err)=>{
    console.error(err);
  });
});


 app.listen(3000, function () {
   console.log('http://127.0.0.1:3000/search/blog?query=검색어 app listening on port 3000!');
 });

템플릿 엔진은 nunjucks를 사용했습니다.

네이버에서 제공하는 코드의 request 패키지가 작년 초부터 deprecated됐다길래 request 부분을 axios로 바꿨습니다.
그리고 비동기 처리를 하기 위해서 async~await을 사용했습니다.

응답 데이터에 b 태그가 포함돼 있어서 없애줬습니다. 참고로 replace 메서드는 첫번재 파라미터가 리터럴일 경우 일치하는 첫번째 부분만 변경하기 때문에 전부 찾을 수 있도록 정규표현식으로 g를 포함해줬습니다.

그리고 검색 결과를 랜더링 해줍니다.

main.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Naver Blog Searching</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
    @import url('https://fonts.googleapis.com/icon?family=Material+Icons');

    body {
      background: #e2e1e0;
      text-align: center;
    }

    .container {
      background: #fff;
      margin: 30px auto;
      border-radius: 2px;
      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
      width: 60%;
      min-height: 90vh;
      display : flex;
      flex-direction: column;
      align-content: center;
    }

    .search-header {
      display: block;
      font-family: "lobster";
      font-size: 80px;
      margin-top : 150px;
    }
    .search-content{
      margin : 50px auto;
      border : none;
      border : 2px solid #a0a0a0;
      border-radius : 25px;
      width : 350px;
      padding : 5px 5px;
    }
    #search-input {
      margin : 0 auto;
      width : 300px;
      padding : 5px;
      border : none;
    }
    #search-input:focus{
      outline : none;
      background-color: #fff;
    }
    #search-btn{
      display : inline-block;
      background: none;
      padding : 5px;
      border : none;
    }
    #search-btn:focus{
      outline : none;
    }
    #btn-icon{
      font-family : Material Icons;
      font-weight : bold;
      cursor : pointer;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="search-header">Search Topics</div>
    <div class="search-content">
      <form action="/search" method="post" content-type>
        <input id="search-input" name="query" type="text" placeholder="  search..."><button id="search-btn"><span id="btn-icon">search</button>
      </form>
    </div>
  </div>
</body>

</html>

result.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Naver Blog Searching</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
    @import url('https://fonts.googleapis.com/icon?family=Material+Icons');

    body {
      background: #e2e1e0;
      text-align: center;
    }
    .container {
      background: #fff;
      margin: 30px auto;
      border-radius: 2px;
      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
      width: 60%;
      min-height: 90vh;
      display : flex;
      flex-direction: column;
      align-content: center;
    }
    .search-header {
      display: block;
      font-family: "lobster";
      font-size: 40px;
      margin-top : 30px;
    }
    .search-header a{
      text-decoration: none;
      color : black;
    }
    .search-result{
      border-radius : 6px;
      background-color: #fafafa;
      border : 1px solid #e0e0e0;
      margin : 30px 50px;
      padding : 20px 20px;
      text-align : left;
    }
    .title{
      margin-bottom : 10px;
    }
    .title a{
      text-decoration: none;
      color : black;
    }
    .description{
      font-size: 14px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="search-header"><a href="/">Search Topics</a></div>
    <div clas="search-content">
      {% for item in items %}
        <div class="search-result">
          <div class="title">
            <a href={{item.link}}><strong>{{item.title}}</strong></a>
          </div>
          <div class="description">
            {{item.description}}
          </div>
        </div>
      {% endfor %}
    </div>
  </div>
</body>

</html>

생각보다 css가 많이 들어가서 따로 css 파일을 만드는 게 나을 뻔했습니당...

profile
반갑습니다.

1개의 댓글

comment-user-thumbnail
2022년 3월 18일

query 받는것을 req.body.query 했을때 undefined가 뜨는데 프론트쪽에서 query 보내는 부분 코드 공유 부탁드려도 될까요??

답글 달기