웹 개발을 하는 분들이라면 네이버 오픈 API, 카카오 오픈 API 등의 표현을 한번 씩 접해보았을 것입니다.
보통 이런 API에서는 서버의 기능을 일부 제공해주는데요, 대표적으로 '~를 통한 로그인'의 기능이 있습니다.
API : Application Programming Interface
API는 다른 어플리케이션에서 현재 프로그램의 기능을 사용할 수 있도록 해주는 인터페이스를 뜻합니다.
따라서 네이버 로그인 API, 네이버 검색 API 등은 각각 네이버에서 제공하는 로그인, 검색 기능을 다른 어플리케이션에서도 사용할 수 있도록 하는 매개체가 되는 것입니다.
하지만 API를 누구나 사용할 수 있는 것은 아닙니다. API를 개방한 서버 측에서는 API를 사용하는 어플리케이션에 API 키(=client secret key)를 발급해줍니다. 따라서 이 API 키를 가진 도메인에서만 서버의 기능을 사용할 수 있게 됩니다.
그리고 API를 개방한 서버 측에서는 기능의 사용 횟수에 제한을 걸기도 합니다. 어플리케이션 측에서 검색 결과와 같이 서버에 부하가 걸릴 수 있는 API를 과도하게 사용하게 될 경우에는 서버에 무리가 가기 때문입니다.
다만 API를 제공하지 않는 서버도 있습니다. 이러한 경우에는 어플리케이션이 직접 크롤링을 해서 서버의 데이터를 수집합니다.
Node.js를 기준으로 예시를 작성했습니다.
https://developers.naver.com/apps/#/register
위 링크에 들어가서 사용할 어플리케이션을 등록해 줍니다
사용 API에 "검색"을 추가합니다.
발급 받은 CLIENT ID와 CLIENT SECRET을 변수에 넣어 줍니다.
모듈을 실행시킵니다.
// 네이버 검색 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!');
});
코드 출처 - 네이버 개발자 센터
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 형식으로 "노티드 도넛"에 대한 결과가 제공됩니다.
이제 어플리케이션은 검색 결과를 활용해서 새롭게 가공할 수 있습니다.
위와 같은 JSON 형태는 가시성이 떨어지므로 간단하게 css를 적용해보았습니다.
메인 페이지
검색 결과
"안국역 카페"에 대한 검색 결과입니다.
제목을 누르면 해당 블로그 포스트로 이동하도록 하였습니다.
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를 포함해줬습니다.
그리고 검색 결과를 랜더링 해줍니다.
<!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>
<!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 파일을 만드는 게 나을 뻔했습니당...
query 받는것을 req.body.query 했을때 undefined가 뜨는데 프론트쪽에서 query 보내는 부분 코드 공유 부탁드려도 될까요??