Naver Search API와 Kakao Map API를 활용하여 특정 키워드로 검색한 장소 검색 결과를 지도에 표시하는 기능을 구현해 보도록 하자.
먼저 네이버 API 이용신청 하는 과정이 필요하다. 그리고 Spring Initializr를 이용하여 Spring Boot로 Demo 프로젝트를 생성하여 코드를 작성하는 과정으로 넘어간다.
💡Naver Application
https://developers.naver.com/main/
네이버 개발자 플랫폼 애플리케이션 등록 페이지에 들어간다.
SDK(Software Development Kit)
소프트웨어 개발 키트(SDK)는 개발자에게 다른 프로그램에 추가하거나 연결할 수 있는 커스텀 앱을 제작할 수 있는 기능을 제공하는 도구 모음이다.
애플리케이션 등록
애플리케이션 이름을 작성하고, 사용할 API를 선택한다.
API는 한 가지 이상 선택할 수 있다. 검색 API를 사용할 예정이므로 '검색'을 선택한다.
서비스 환경 설정
http://localhost:8090
WEB에서 사용할 예정이므로 WEB 환경에서 서비스할 URL을 설정해 주었다.
비로그인 오픈 API를 사용할 예정이므로 로컬 URL인 localhost를 입력하였다.
애플리케이션 정보
API 등록을 마치면 Client ID와 Client Secret을 부여받는다.
API를 요청할 때 Client ID와 Client Secret가 필요한데, 여기서 생성한 Key를 Spring Boot에서 사용하면 된다.
참고로 현재 네이버 비로그인 오픈 API는 하루 25000번까지 호출할 수 있다.
Open API 가이드
비로그인 방식 오픈 API
비로그인 방식 오픈 API는 HTTP 헤더에 클라이언트 아이디와 클라이언트 시크릿 값만 전송해 사용할 수 있는 오픈 API이다. 네이버 로그인의 인증을 통한 접근 토큰을 획득할 필요가 없다.
- 데이터랩: 네이버 데이터랩의 검색어 트렌드와 쇼핑인사이트를 API로 실행할 수 있게 하는 API입니다.
- 검색: 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API입니다. 그 외에 지역 검색 결과와 성인 검색어 판별 기능, 오타 변환 기능을 제공합니다.
- 단축URL: 원본 URL을 https://me2.do/example과 같은 형태의 짧은 URL로 반환받을 수 있는 API입니다.
- 이미지 캡차: 네이버 서비스에서 사용하는 이미지 캡차 기능을 외부 서비스에 사용할 수 있게 하는 API입니다.
- 음성 캡차: 네이버 서비스에서 사용하는 음성 캡차 기능을 외부 서비스에 사용할 수 있게 하는 API입니다.
- 네이버 공유하기: 콘텐츠를 네이버 블로그, 네이버 카페, PHOLAR에 공유할 수 있게 하는 API입니다.
- 네이버 오픈메인: 웹 페이지를 네이버 메인에 추가할 수 있게 하는 플러그인입니다.
- Clova Face Recognition: 입력된 사진 이미지 속의 얼굴을 인식하거나 얼굴 감지를 이용한 애플리케이션을 만들 수 있게 하는 API입니다.
- Papago 번역: 인공 신경망 기술 기반의 기계 번역 결과를 반환하는 API입니다.
네이버 API Documents는 위 링크를 참고한다.
이 문서의 최종 수정일은 2021년 8월 27일이므로, 큰 변경 없이 사용되고 있는 것으로 보인다.
💡Naver Search API 구현
application.properties
# Naver API
#spring.config.import=classpath:api-key.yaml
naver.client-id=발급받은 Client ID
naver.secret=발급받은 Secret
naver:
client-id: 발급받은 Client ID
secret: 발급받은 Secert
프로퍼티 파일에 발급받은 Client ID와 Secret을 추가한다. 또는 yaml 파일을 생성하여 추가해도 된다.
Controller
package com.test.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 검색 API 요청을 처리하는 메인 컨트롤러입니다.
*
* @author 이승원
* @since 2024.02.28.
*/
@Controller
public class MainController {
/**
* 검색을 요청할 페이지로 이동합니다.
*
* @param model 검색 결과를 뷰에 전달하기 위한 데이터 모델
* @return 검색 결과를 표시할 뷰의 경로
*/
@GetMapping(value = "/search")
public String search(Model model) {
return "user/api/search";
}
}
templates 폴더의 search.html 파일로 이동하는 엔드포인트를 만들었다.
엔드포인트란?
URL 또는 URI라고도 하는 엔드포인트는 웹 애플리케이션에서 클라이언트가 서버에 요청을 보내는 특정한 URL을 가리킨다.
이 URL은 일반적으로 특정 기능이나 리소스를 가리키며, 클라이언트가 서버에게 어떤 동작을 수행해야 하는지를 알려준다. 예를 들어, 사용자가 웹 브라우저에서 "http://localhost/demo/search"와 같은 URL을 요청하면, 이것은 /demo/search" 엔드포인트에 해당한다.
2023.09.20 - [Programming/HTML] - [HTML] URL(URI): 프로토콜, 도메인, 웹 서버 포트번호
URL(URI)에 대해서는 위 글을 참고한다.
RestController
package com.test.demo.book.controller;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import groovy.util.logging.Slf4j;
/**
* 네이버 검색 API를 통해 장소 정보를 검색하는 RestController입니다.
* 네이버 API를 호출하여 지정된 이름 또는 동적으로 지정된 검색어를 기반으로 장소를 검색하고, 검색된 장소 목록을 반환합니다.
*
* @author 이승원
* @since 2024.02.28.
*/
@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerController {
@Value("${naver.client-id}")
private String NAVER_API_ID;
@Value("${naver.secret}")
private String NAVER_API_SECRET;
/**
* 네이버 검색 API를 이용하여 지정된 이름으로 장소를 검색합니다.
*
* @param name 검색할 장소의 이름
* @return 검색된 장소 목록
*/
@GetMapping("/naver/{name}")
public List<Map<String, String>> naver(@PathVariable String name) {
return searchRestaurant(name);
}
/**
* 네이버 검색 API를 이용하여 동적으로 검색어를 지정하여 장소를 검색합니다.
*
* @param query 동적으로 지정된 검색어
* @return 검색된 장소 목록
*/
@GetMapping("/naver")
public List<Map<String, String>> naverSearchDynamic(@RequestParam String query) {
return searchRestaurant(query);
}
/**
* 네이버 검색 API를 이용하여 장소를 검색하는 메서드입니다.
*
* @param query 검색할 장소의 이름 또는 검색어
* @return 검색된 장소 목록
*/
private List<Map<String, String>> searchRestaurant(String query) {
List<Map<String, String>> restaurants = new ArrayList<>();
try {
// UTF-8로 인코딩된 검색어 생성
ByteBuffer buffer = StandardCharsets.UTF_8.encode(query);
String encode = StandardCharsets.UTF_8.decode(buffer).toString();
// 네이버 검색 API를 호출하기 위한 URI 생성
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com").path("/v1/search/local")
.queryParam("query", encode).queryParam("display", 10).queryParam("start", 1)
.queryParam("sort", "random").encode().build().toUri();
// RestTemplate을 사용하여 네이버 API에 요청을 보냄
RestTemplate restTemplate = new RestTemplate();
RequestEntity<Void> req = RequestEntity.get(uri).header("X-Naver-Client-Id", NAVER_API_ID)
.header("X-Naver-Client-Secret", NAVER_API_SECRET).build();
// API 응답 데이터를 JSON 형식으로 변환
ResponseEntity<String> response = restTemplate.exchange(req, String.class);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response.getBody());
// 검색 결과 중에서 장소 정보를 추출하여 리스트에 저장
JsonNode itemsNode = rootNode.path("items");
for (JsonNode itemNode : itemsNode) {
Map<String, String> restaurant = new HashMap<>();
restaurant.put("title", itemNode.path("title").asText()); // 장소 이름
restaurant.put("address", itemNode.path("address").asText()); // 장소 주소
/*
* restaurant.put("mapx", itemNode.path("mapx").asText());
* restaurant.put("mapy", itemNode.path("mapy").asText());
*/
// 위도와 경도를 double 형식으로 변환하여 저장
double latitude = Double.parseDouble(itemNode.path("mapy").asText()) / 1e7; // 위도
double longitude = Double.parseDouble(itemNode.path("mapx").asText()) / 1e7; // 경도
restaurant.put("latitude", String.valueOf(latitude));
restaurant.put("longitude", String.valueOf(longitude));
restaurants.add(restaurant); // 리스트에 추가
}
} catch (Exception e) {
e.printStackTrace();
}
return restaurants;
}
}
네이버 API 요청을 날릴 RestController를 생성하였다.
여기서 각각의 코드를 분할하여 상세하게 확인해 보도록 하자.
API 사용 인증
@Value("${naver.client-id}")
private String NAVER_API_ID;
@Value("${naver.secret}")
private String NAVER_API_SECRET;
이 코드는 네이버 API를 호출할 때 해당 API에 인증하기 위해 사용되는 정보를 가져오는 용도로 사용된다.
${naver.client-id}와 ${naver.secret}는 스프링의 외부 설정 파일(application.properties 또는 application.yml)에 정의된 값들을 가져온다. 이 값들은 네이버 API에 접근할 때 필요한 클라이언트 ID와 Secret 키에 해당한다.
2개의 엔드포인트 정의
/**
* 네이버 검색 API를 이용하여 지정된 이름으로 장소를 검색합니다.
*
* @param name 검색할 장소의 이름
* @return 검색된 장소 목록
*/
@GetMapping("/naver/{name}")
public List<Map<String, String>> naver(@PathVariable String name) {
return searchRestaurant(name);
}
/**
* 네이버 검색 API를 이용하여 동적으로 검색어를 지정하여 장소를 검색합니다.
*
* @param query 동적으로 지정된 검색어
* @return 검색된 장소 목록
*/
@GetMapping("/naver")
public List<Map<String, String>> naverSearchDynamic(@RequestParam String query) {
return searchRestaurant(query);
}
스프링 프레임워크의 @GetMapping 어노테이션을 사용하여 두 가지 엔드포인트를 정의하였다.
이 두 가지 엔드포인트는 사용자가 검색하고자 하는 방식에 따라 호출할 수 있도록 구현했다.
☝️첫 번째 메서드 naver는 경로에 이름을 포함하여 호출된다. 예를 들어, "/api/server/naver/장소"과 같이 호출된다. 이 엔드포인트는 이름을 매개변수로 받아와 해당 이름으로 장소을 검색하고, 검색된 장소 목록을 반환한다.
✌️두 번째 메서드 naverSearchDynamic은 쿼리 매개변수로 검색어를 동적으로 받아와 장소을 검색한다. 즉, 동적으로 네이버 검색을 수행하는 메서드인 셈이다. 이 엔드포인트는 경로가 "/api/server/naver"이며, query라는 이름의 쿼리 매개변수를 받는다. 예를 들어, "/api/server/naver?query=장소"과 같이 호출된다.
이 메서드들은 스프링 프레임워크의 @PathVariable과 @RequestParam 어노테이션을 사용하여 경로 변수와 쿼리 매개변수를 추출한다. 이를 통해 사용자가 제공한 정보를 받아올 수 있다.
@PathVariable 어노테이션은 URL 경로의 일부를 변수로 사용할 때 지정한다. 예를 들어, /api/server/naver/{name}와 같은 URL에서 {name} 부분에 해당하는 값을 메서드의 매개변수로 전달받을 수 있다. 이처럼 @PathVariable 어노테이션은 경로 변수를 추출하는데 사용된다.
@RequestParam 어노테이션은 URL에서 요청된 쿼리 파라미터를 매개변수로 받아올 때 사용된다. 즉, URL에 ?key=value와 같은 형태로 전달되는 데이터를 처리할 때 사용된다. 이 어노테이션을 사용하면 요청의 쿼리 문자열에서 특정한 매개변수를 추출하여 사용할 수 있다.
요약하면, @PathVariable은 URL 경로에서 값을 추출하고, @RequestParam은 URL의 쿼리 파라미터에서 값을 추출하는 것이다.
검색 API를 사용하여 장소 검색
/**
* 네이버 검색 API를 이용하여 장소를 검색하는 메서드입니다.
*
* @param query 검색할 장소의 이름 또는 검색어
* @return 검색된 장소 목록
*/
private List<Map<String, String>> searchRestaurant(String query) {
List<Map<String, String>> restaurants = new ArrayList<>();
try {
// UTF-8로 인코딩된 검색어 생성
ByteBuffer buffer = StandardCharsets.UTF_8.encode(query);
String encode = StandardCharsets.UTF_8.decode(buffer).toString();
// 네이버 검색 API를 호출하기 위한 URI 생성
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com").path("/v1/search/local")
.queryParam("query", encode).queryParam("display", 10).queryParam("start", 1)
.queryParam("sort", "random").encode().build().toUri();
// RestTemplate을 사용하여 네이버 API에 요청을 보냄
RestTemplate restTemplate = new RestTemplate();
RequestEntity<Void> req = RequestEntity.get(uri).header("X-Naver-Client-Id", NAVER_API_ID)
.header("X-Naver-Client-Secret", NAVER_API_SECRET).build();
// API 응답 데이터를 JSON 형식으로 변환
ResponseEntity<String> response = restTemplate.exchange(req, String.class);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response.getBody());
// 검색 결과 중에서 장소 정보를 추출하여 리스트에 저장
JsonNode itemsNode = rootNode.path("items");
for (JsonNode itemNode : itemsNode) {
Map<String, String> restaurant = new HashMap<>();
restaurant.put("title", itemNode.path("title").asText()); // 장소 이름
restaurant.put("address", itemNode.path("address").asText()); // 장소 주소
/*
* restaurant.put("mapx", itemNode.path("mapx").asText());
* restaurant.put("mapy", itemNode.path("mapy").asText());
*/
// 위도와 경도를 double 형식으로 변환하여 저장
double latitude = Double.parseDouble(itemNode.path("mapy").asText()) / 1e7; // 위도
double longitude = Double.parseDouble(itemNode.path("mapx").asText()) / 1e7; // 경도
restaurant.put("latitude", String.valueOf(latitude));
restaurant.put("longitude", String.valueOf(longitude));
restaurants.add(restaurant); // 리스트에 추가
}
} catch (Exception e) {
e.printStackTrace();
}
return restaurants;
}
이 메서드는 네이버 검색 API를 사용하여 장소를 검색하는 기능을 담당한다.
각 부분을 상세하게 살펴보도록 하자.
// UTF-8로 인코딩된 검색어 생성
ByteBuffer buffer = StandardCharsets.UTF_8.encode(query);
String encode = StandardCharsets.UTF_8.decode(buffer).toString();
이 부분은 UTF-8로 인코딩 된 검색어를 생성하는 과정이다.
1. ByteBuffer 생성
검색어(query)를 UTF-8 문자열로 인코딩하기 위해 ByteBuffer 객체를 생성하였다.
StandardCharsets.UTF_8.encode(query)를 호출하여 UTF-8 문자셋으로 인코딩 된 바이트 시퀀스를 포함하는 ByteBuffer를 얻는다.
2. 문자열로 디코딩
생성된 ByteBuffer를 UTF-8 문자열로 디코딩하였다.
StandardCharsets.UTF_8.decode(buffer).toString()을 호출하여 ByteBuffer에 있는 UTF-8 인코딩 된 데이터를 문자열로 디코딩한다. 이렇게 디코딩된 문자열은 검색어의 UTF-8 표현을 나타낸다.
이 과정을 통해 검색어(query)가 UTF-8 형식으로 올바르게 인코딩되고, 이를 URI에 안전하게 포함하여 네이버 검색 API에 요청할 수 있게 된다.
// 네이버 검색 API를 호출하기 위한 URI 생성
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com").path("/v1/search/local")
.queryParam("query", encode).queryParam("display", 10).queryParam("start", 1)
.queryParam("sort", "random").encode().build().toUri();
이 부분은 네이버 검색 API를 호출하기 위한 URI를 생성하는 과정이다.
1. URI 구성 시작
UriComponentsBuilder.fromUriString("https://openapi.naver.com")를 호출하여 URI의 기본 부분을 설정한다. 여기서는 네이버의 API 엔드포인트 주소인 "https://openapi.naver.com"를 사용하였다.
2. 경로 추가
.path("/v1/search/local")를 호출하여 API의 경로를 추가한다. 여기서는 로컬 검색을 위한 엔드포인트인 "/v1/search/local"을 사용한다.
3. 쿼리 파라미터 추가
.queryParam("query", encode)를 호출하여 검색어(query)를 쿼리 파라미터로 추가한다. 앞서 UTF-8로 인코딩 된 검색어를 포함한 변수 encode가 사용되었다.
.queryParam("display", 10)를 호출하여 검색 결과로 반환할 항목의 개수를 지정한다. 여기서는 최대 10개의 항목이 반환되도록 했다.
.queryParam("start", 1)를 호출하여 검색 결과 목록의 시작 인덱스를 지정한다. 여기서는 첫 번째 항목부터 시작한다.
.queryParam("sort", "random")를 호출하여 검색 결과의 정렬 방식을 지정한다. 여기서는 랜덤한 순서로 결과를 반환하도록 했다.
4. 인코딩 및 URI 빌드
.encode().build().toUri()를 호출하여 URI를 인코딩하고 최종적으로 빌드한다. 이 과정에서 쿼리 파라미터 값들이 URL 인코딩 되고, 최종적으로 완성된 URI 객체가 반환된다.
이렇게 생성된 URI는 네이버의 로컬 검색 API를 호출하기 위한 완전한 엔드포인트 주소가 된다. 이 URI를 사용하여 네이버 API에 요청을 보내고 검색 결과를 가져올 수 있다.
// RestTemplate을 사용하여 네이버 API에 요청을 보냄
RestTemplate restTemplate = new RestTemplate();
RequestEntity<Void> req = RequestEntity.get(uri).header("X-Naver-Client-Id", NAVER_API_ID)
.header("X-Naver-Client-Secret", NAVER_API_SECRET).build();
이 부분은 RestTemplate을 사용하여 네이버 API에 HTTP 요청을 보내는 과정이다.
1. RestTemplate 생성
RestTemplate restTemplate = new RestTemplate();을 통해 RestTemplate 객체를 생성한다. 이 객체는 HTTP 통신을 쉽게 수행할 수 있도록 지원하는 Spring의 클래스이다.
2. HTTP 요청 생성
RequestEntity.get(uri)를 호출하여 GET 메서드를 사용하는 HTTP 요청을 생성한다. 여기서 uri는 이전에 생성한 네이버 API 호출을 위한 URI이다.
3. 헤더 설정
.header("X-Naver-Client-Id", NAVER_API_ID)를 호출하여 요청 헤더에 네이버 API 클라이언트 아이디를 추가한다. 이는 네이버 API를 호출할 때 필요한 클라이언트 아이디이다.
.header("X-Naver-Client-Secret", NAVER_API_SECRET)를 호출하여 요청 헤더에 네이버 API 클라이언트 시크릿을 추가한다. 이는 API 호출 시 사용되는 클라이언트 시크릿 값이다.
4. 요청 엔티티 빌드
.build()를 호출하여 최종적으로 요청 엔티티를 빌드한다. 이 엔티티에는 요청 URL, 메서드, 헤더 등이 포함된다.
이렇게 생성된 요청 엔티티를 사용하여 RestTemplate을 통해 네이버 API에 HTTP GET 요청을 보내고, API 서버로부터 응답을 받을 수 있다. 요청은 클라이언트 아이디와 시크릿을 포함하여 안전하게 이루어진다.
// API 응답 데이터를 JSON 형식으로 변환
ResponseEntity<String> response = restTemplate.exchange(req, String.class);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response.getBody());
이 부분은 네이버 API로부터 받은 응답 데이터를 JSON 형식으로 변환하는 과정이다.
1. HTTP 요청 보내기
restTemplate.exchange(req, String.class)를 호출하여 네이버 API에 HTTP 요청을 보낸다. 이 메서드는 요청을 보내고, API 서버로부터의 응답을 받아온다. 이때, ResponseEntity<String> 형태로 응답을 받게 되는데, 여기서 String은 응답 본문을 문자열로 받겠다는 의미이다.
즉, 이 메서드는 네이버 API로부터의 응답을 문자열 형태로 받아오는 역할을 한다. 받아온 문자열 데이터를 이후에는 원하는 형태로 처리하고 활용할 수 있다.
2. JSON 변환
응답 데이터인 response.getBody()는 문자열 형태로 되어 있다. 이 문자열을 JSON 형식으로 변환해야 데이터를 쉽게 다룰 수 있기 때문에 ObjectMapper를 사용하여 JSON 형식의 문자열을 Java 객체로 변환한다.
ObjectMapper의 readTree() 메서드를 사용하여 문자열을 JsonNode 객체로 변환하였다. 이를 통해 JSON 데이터의 구조를 분석하고 필요한 정보를 추출할 수 있다. 그리고 JsonNode 객체는 JSON 데이터의 트리 구조를 가지며, 각 노드는 JSON 데이터의 키와 값에 대응한다.
이렇게 변환된 JsonNode 객체를 통해 응답 데이터의 구조를 파악하고 필요한 정보를 추출하여 사용할 수 있다. 예를 들어, rootNode.path("items")와 같이 특정 키에 해당하는 값을 가져올 수 있으며, 이를 통해 장소의 이름, 주소, 위치 등의 정보를 추출하여 리스트에 저장할 수 있다.
// 검색 결과 중에서 장소 정보를 추출하여 리스트에 저장
JsonNode itemsNode = rootNode.path("items");
for (JsonNode itemNode : itemsNode) {
Map<String, String> restaurant = new HashMap<>();
restaurant.put("title", itemNode.path("title").asText()); // 장소 이름
restaurant.put("address", itemNode.path("address").asText()); // 장소 주소
/*
* restaurant.put("mapx", itemNode.path("mapx").asText());
* restaurant.put("mapy", itemNode.path("mapy").asText());
*/
// 위도와 경도를 double 형식으로 변환하여 저장
double latitude = Double.parseDouble(itemNode.path("mapy").asText()) / 1e7; // 위도
double longitude = Double.parseDouble(itemNode.path("mapx").asText()) / 1e7; // 경도
restaurant.put("latitude", String.valueOf(latitude));
restaurant.put("longitude", String.valueOf(longitude));
restaurants.add(restaurant); // 리스트에 추가
}
이 부분은 네이버 검색 API로부터 받아온 JSON 응답 데이터에서 장소 정보를 추출하여 리스트에 저장하는 부분이다.
먼저, rootNode.path("items")를 사용하여 JSON 응답 데이터에서 "items" 키에 해당하는 값을 가져온다. 이 부분은 실제 검색 결과에 해당하는 항목들을 담고 있는 배열이다. 그런 다음, for 루프를 사용하여 각 항목을 순회한다. 각 항목은 itemNode로 표현되며, 이는 JSON 응답 데이터에서 각 장소의 정보를 담고 있는 객체이다.
각 장소 정보에서는 "title" 키를 통해 장소의 이름을 가져와 restaurant 맵에 추가하고, 마찬가지로 "address" 키를 통해 장소의 주소를 가져와서 맵에 추가하였다. 주석 처리된 부분은 장소의 위도와 경도 정보를 가져오는 부분이다.
그런데 위도와 경도 데이터를 그대로 가져왔을 때 소수점이 사라진 채로 가져오기 때문에 1e7로 나누어서 정확한 위도와 경도 데이터를 가져올 수 있도록 전처리하였다. 위도와 경도 정보는 문자열로 받아와서 Double로 변환한 후에 사용하였다.
return restaurants;
마지막으로 각 장소의 정보를 맵에 담은 후에 이를 리스트에 추가하였다. 이렇게 하면 모든 장소에 대한 정보가 리스트에 저장되며, 검색된 장소 목록을 return하여 반환하게 된다.
restaurants는 리스트 안에 여러 개의 맵(Map)이 포함된 자료구조이다. 이 맵은 키(key)와 값(value) 쌍으로 구성되어 있으며, 이 정보는 장소의 이름(title), 주소(address), 위도(latitude), 경도(longitude)로 구성된다.
Demo search.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>네이버 검색</title>
<style>
#map {
width: 100%;
height: 400px;
}
#modal {
display: none; /* 모달 초기에는 숨겨짐 */
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4); /* 배경 어둡게 */
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto; /* 중앙 정렬 */
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover, .close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
#markerInfo, #clickedMarkerInfo {
margin-top: 20px;
padding: 10px;
background-color: #f2f2f2;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=2b631c4003121607c7cb22ff2f41d62f"></script>
</head>
<body>
<h1>네이버 검색</h1>
<input type="text" id="searchInput" placeholder="검색어를 입력하세요">
<button onclick="search()">검색</button>
<div id="searchQuery"></div> <!-- 검색 결과 -->
<div id="searchResults"></div> <!-- 검색 결과 목록 -->
<div id="map"></div> <!-- 지도 -->
<div id="modal">
<div class="modal-content">
<span class="close">×</span> <!-- 모달 닫기 버튼 -->
<p id="modalContent">마커 정보</p> <!-- 모달 내용 -->
</div>
</div>
<div id="clickedMarkerInfo"></div> <!-- 클릭한 마커 정보 -->
<script>
// 검색 함수
function search() {
let query = document.getElementById('searchInput').value; // 입력된 검색어
$.ajax({
url: '/demo/api/server/naver/' + encodeURIComponent(query),
method: 'GET',
success: function(data) {
// 검색 결과
displaySearchResults(data);
// 지도에 마커 표시
displayMarkers(data);
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
}
// 검색 결과 출력 함수
function displaySearchResults(restaurants) {
let searchResultsElement = document.getElementById('searchResults');
searchResultsElement.innerHTML = ''; // 이전 검색 결과 지우기
// 검색 결과를 목록으로 변환하여 출력
restaurants.forEach(function(restaurant) {
let listItem = document.createElement('div');
// 각 장소의 이름과 주소 표시
listItem.innerHTML = '<strong>'
+ restaurant.title.replace(/<[^>]+>/g, '') // HTML 태그 제거
+ '</strong><br>' + restaurant.address;
searchResultsElement.appendChild(listItem);
});
}
// 마커 표시 함수
const imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";
function displayMarkers(restaurants) {
const map = new kakao.maps.Map(document.getElementById('map'), {
center: new kakao.maps.LatLng(restaurants[0].latitude, restaurants[0].longitude),
level: 3
});
// 모든 장소에 대해 마커 표시
restaurants.forEach(function(restaurant) {
const latitude = restaurant.latitude; // 위도
const longitude = restaurant.longitude; // 경도
// 마커 이미지 크기
const imageSize = new kakao.maps.Size(24, 35);
// 마커 이미지 생성
const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);
// 마커 생성
const marker = new kakao.maps.Marker({
map: map, // 지도
position: new kakao.maps.LatLng(latitude, longitude), // 위치
title: restaurant.title.replace(/<[^>]+>/g, ''), // 이름
image: markerImage // 이미지
});
// 마커 클릭 이벤트 등록
kakao.maps.event.addListener(marker, 'click', function() {
// 클릭한 마커의 정보를 모달로 표시
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
modal.style.display = "block";
modalContent.innerHTML = this.getTitle();
// 클릭한 마커 정보를 아래에도 표시
const clickedMarkerInfo = document.getElementById('clickedMarkerInfo');
clickedMarkerInfo.innerHTML = this.getTitle();
});
});
}
// 모달 닫기 버튼 클릭 이벤트
const closeBtn = document.getElementsByClassName("close")[0];
closeBtn.onclick = function() {
const modal = document.getElementById('modal');
modal.style.display = "none";
}
</script>
</body>
</html>
개선한 search.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>네이버 검색 API</title>
<link rel="stylesheet" href="/demo/assets/css/style.css">
</head>
<body>
<div class="container">
<h1>네이버 검색 API</h1>
<div class="search-container">
<input type="text" id="searchInput" placeholder="검색어를 입력하세요">
<button id="searchBtn">검색</button>
</div>
<div id="map">검색을 시작해주세요</div>
<div id="searchResults"></div>
</div>
<div id="modal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<p id="modalContent">마커 정보</p>
</div>
</div>
<!-- <div id="clickedMarkerInfo" class="clicked-marker-info"></div> -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=2b631c4003121607c7cb22ff2f41d62f"></script>
<script src="/demo/assets/js/script.js"></script>
</body>
</html>
사용자가 검색어를 입력하고 해당 장소의 위치를 지도에 표시하며, 선택한 장소의 상세 정보를 모달 창에서 확인할 수 있는 간단한 검색 웹 애플리케이션이다.
추가한 기능으로는 검색 기능, 검색 결과 표시, 지도 표시, 모달 창, 외부 라이브러리가 있다. 각 코드를 부분적으로 살펴보도록 하자.
style.css
/* style.css */
@font-face {
font-family: 'Pretendard-Regular';
src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Pretendard-Regular', sans-serif;
background-color: #f9f9f9;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.search-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
#searchInput {
width: 200px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 10px;
}
#searchBtn {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#searchBtn:hover {
background-color: #45a049;
}
#searchResults {
margin-top: 20px;
}
#map {
width: 100%;
height: 400px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover, .close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.clicked-marker-info {
margin-top: 20px;
padding: 10px;
background-color: #f2f2f2;
border: 1px solid #ccc;
border-radius: 8px;
}
.restaurant-item {
border: 1px solid #ccc;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.restaurant-item strong {
color: #333;
font-size: 18px;
}
.restaurant-item p {
margin-top: 10px;
color: #666;
font-size: 16px;
}
script.js
// script.js
$(document).ready(function() {
// 검색 버튼 클릭 이벤트 처리
$('#searchBtn').click(function() {
search(); // 검색 함수 호출
});
});
$('#searchInput').keypress(function(event) {
if (event.which === 13) { // Enter 키를 눌렀을 때
$('#searchBtn').click(); // 검색 버튼 클릭 이벤트 호출
}
});
// 검색 함수
function search() {
let query = $('#searchInput').val(); // 검색어 가져오기
$.ajax({
url: '/demo/api/server/naver/' + encodeURIComponent(query),
method: 'GET',
success: function(data) {
displaySearchResults(data); // 검색 결과 표시 함수 호출
displayMarkers(data); // 마커 표시 함수 호출
},
error: function(xhr, status, error) {
console.error('Error:', error); // 에러 발생 시 콘솔에 로그 출력
}
});
}
// 클릭한 장소 정보를 저장하는 전역 변수
let clickedRestaurant;
// 검색 결과 출력 함수
function displaySearchResults(restaurants) {
let searchResultsElement = document.getElementById('searchResults');
searchResultsElement.innerHTML = ''; // 이전 검색 결과 지우기
// 검색 결과를 목록으로 변환하여 출력
restaurants.forEach(function(restaurant) {
let listItem = document.createElement('div'); // 장소 목록 아이템 생성
listItem.classList.add('restaurant-item'); // CSS 클래스 추가
// 각 장소의 이름과 주소 표시
listItem.innerHTML = '<strong>'
+ restaurant.title.replace(/<[^>]+>/g, '') // HTML 태그 제거
+ '</strong><br>' + restaurant.address;
searchResultsElement.appendChild(listItem); // 장소 목록에 아이템 추가
// 장소 목록 아이템에 클릭 이벤트 추가
listItem.addEventListener('click', function() {
// 클릭한 장소 정보 저장
clickedRestaurant = restaurant;
// 해당 위치로 지도 이동
moveMapToRestaurantLocation();
});
});
}
// 지도 위치 이동 함수
function moveMapToRestaurantLocation() {
if (clickedRestaurant) { // 클릭한 장소 정보가 존재하는 경우
const mapContainer = document.getElementById('map'); // 지도를 표시할 영역의 DOM 요소
const mapOption = { // 지도 옵션 설정
center: new kakao.maps.LatLng(clickedRestaurant.latitude, clickedRestaurant.longitude), // 지도의 중심좌표 설정
level: 3 // 지도 확대 레벨 설정
};
// 지도 객체 생성
const map = new kakao.maps.Map(mapContainer, mapOption);
// 클릭한 장소 위치에 마커 생성
const marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도 객체 설정
position: new kakao.maps.LatLng(clickedRestaurant.latitude, clickedRestaurant.longitude), // 마커의 위치 설정
title: clickedRestaurant.title.replace(/<[^>]+>/g, '') // 마커에 표시될 타이틀 설정 (HTML 태그 제거)
});
// 마커를 클릭했을 때 모달을 표시하고 내용을 설정하는 이벤트 리스너 등록
kakao.maps.event.addListener(marker, 'click', function() {
const modal = $('#modal'); // 모달 요소 선택
const modalContent = $('#modalContent'); // 모달 내용 요소 선택
modal.css("display", "block"); // 모달 표시
modalContent.html(this.getTitle()); // 모달 내용 설정 (마커의 타이틀로 설정)
const clickedMarkerInfo = $('#clickedMarkerInfo'); // 클릭한 마커 정보 요소 선택
clickedMarkerInfo.html(this.getTitle()); // 클릭한 마커의 타이틀 표시
});
}
}
const imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";
// 마커 표시 함수
function displayMarkers(restaurants) {
// 지도 생성 및 초기화
const map = new kakao.maps.Map(document.getElementById('map'), {
center: new kakao.maps.LatLng(restaurants[0].latitude, restaurants[0].longitude), // 첫 번째 장소의 위치를 중심으로 설정
level: 3 // 지도 확대 레벨 설정
});
// 각 장소에 대한 마커 표시
restaurants.forEach(function(restaurant) {
const latitude = restaurant.latitude; // 장소의 위도
const longitude = restaurant.longitude; // 장소의 경도
const imageSize = new kakao.maps.Size(24, 35); // 마커 이미지 크기 설정
const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize); // 마커 이미지 생성
// 장소의 위치에 마커 생성
const marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도 객체 설정
position: new kakao.maps.LatLng(latitude, longitude), // 마커의 위치 설정
title: restaurant.title.replace(/<[^>]+>/g, ''), // 마커에 표시될 타이틀 설정 (HTML 태그 제거)
image: markerImage // 마커에 사용될 이미지 설정
});
// 마커를 클릭했을 때 모달을 표시하고 내용을 설정하는 이벤트 리스너 등록
kakao.maps.event.addListener(marker, 'click', function() {
const modal = $('#modal'); // 모달 요소 선택
const modalContent = $('#modalContent'); // 모달 내용 요소 선택
modal.css("display", "block"); // 모달 표시
modalContent.html(this.getTitle()); // 모달 내용 설정 (마커의 타이틀로 설정)
const clickedMarkerInfo = $('#clickedMarkerInfo'); // 클릭한 마커 정보 요소 선택
clickedMarkerInfo.html(this.getTitle()); // 클릭한 마커의 타이틀 표시
});
});
}
// 모달 닫기 버튼 클릭 이벤트
$('.close').click(function() {
const modal = $('#modal');
modal.css("display", "none"); // 모달 숨기기
});
외부 스크립트 파일 추가
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=2b631c4003121607c7cb22ff2f41d62f"></script>
1. jQuery 스크립트
jQuery는 JavaScript를 간소화하고 웹 페이지 상호작용을 쉽게 만드는 데 사용되는 JavaScript 라이브러리이다. 이 코드는 jQuery 버전 3.6.0을 사용하여 제공된 CDN(Content Delivery Network)에서 jQuery 라이브러리를 가져온다.
2. Kakao Maps SDK 스크립트
Kakao Maps SDK는 카카오가 제공하는 지도 서비스를 웹 애플리케이션에서 사용할 수 있도록 지원하는 JavaScript SDK이다. 이 코드는 카카오 개발자 센터에서 제공되는 SDK 스크립트를 가져와서 사용한다.
여기서 appkey 매개변수는 개발자가 카카오에서 발급받은 지도 서비스 API 키를 설정한다. 이 키는 서비스를 사용할 권한을 부여한다.
이렇게 추가된 스크립트 파일은 해당 URL에서 JavaScript 코드를 다운로드하여 실행하고, 웹 페이지에서 해당 라이브러리의 기능을 사용할 수 있도록 한다.
https://developers.kakao.com/product/map
카카오 지도 SDK에 대해서는 위 Document를 참고한다.
화면에 표시할 HTML 요소 추가
<h1>네이버 검색</h1>
<input type="text" id="searchInput" placeholder="검색어를 입력하세요">
<button onclick="search()">검색</button>
<div id="searchQuery"></div> <!-- 검색 결과 -->
<div id="searchResults"></div> <!-- 검색 결과 목록 -->
<div id="map"></div> <!-- 지도 -->
<div id="modal">
<div class="modal-content">
<span class="close">×</span> <!-- 모달 닫기 버튼 -->
<p id="modalContent">마커 정보</p> <!-- 모달 내용 -->
</div>
</div>
<div id="clickedMarkerInfo"></div> <!-- 클릭한 마커 정보 -->
각 요소는 웹 페이지에서 특정 기능을 수행하거나 정보를 표시하는 데 사용된다.
검색 함수
// 검색 함수
function search() {
let query = document.getElementById('searchInput').value; // 입력된 검색어
$.ajax({
url: '/demo/api/server/naver/' + encodeURIComponent(query),
method: 'GET',
success: function(data) {
// 검색 결과
displaySearchResults(data);
// 지도에 마커 표시
displayMarkers(data);
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
}
JavaScript 함수 search()는 웹 페이지에서 사용자가 검색어를 입력하고 검색 버튼을 클릭할 때 실행된다.
이 함수는 AJAX를 사용하여 서버에 검색 요청을 보내고, 그 결과를 받아와서 화면에 표시한다.
AJAX란?
AJAX(Aynchronous JavaScript and XML)는 비동기적으로 서버와 통신하여 데이터를 주고받는 기술이다. 이를 통해 페이지 새로고침 없이도 데이터를 동적으로 로드하고 업데이트할 수 있다. 또한, 페이지를 다시 로드하지 않고도 서버와 상호작용하여 동적으로 데이터를 처리할 수 있기 때문에 웹 애플리케이션의 사용자 경험을 향상시키고 서버 부하를 감소시킬 수 있다.
2023.10.25 - [Programming/JDBC] - [JDBC] Ajax (Asynchronous JavaScript and XML)
AJAX에 대한 자세한 설명은 위 글을 참고한다.
1. 검색어 가져오기
let query = document.getElementById('searchInput').value;
HTML 요소 중 id가 searchInput인 입력 필드에서 사용자가 입력한 검색어를 가져온다.
2. AJAX 요청 보내기
$.ajax({
url: '/demo/api/server/naver/' + encodeURIComponent(query),
method: 'GET',
success: function(data) {
// 검색 결과 처리
},
error: function(xhr, status, error) {
// 오류 처리
}
});
URL에서 요청하는 URL을 지정한다. 여기서는 /demo/api/server/naver/ 엔드포인트에 GET 요청을 보내고, 검색어를 인코딩하여 URL에 포함시킨다.
메서드에서 HTTP 요청 메서드를 지정한다. 여기서는 GET 요청을 사용하여 데이터를 가져온다.
성공 콜백은 요청이 성공했을 때 실행되는 함수이다. 서버에서 받아온 데이터를 매개변수 data로 전달받아 처리한다.
오류 콜백은 요청이 실패했을 때 실행되는 함수이다. 오류 메시지를 콘솔에 출력하거나 오류 처리를 수행한다.
3. 검색 결과 처리
success: function(data) {
displaySearchResults(data);
displayMarkers(data);
}
성공 콜백 함수 내에서 서버로부터 받은 데이터를 화면에 표시하는 두 가지 동작을 수행한다.
검색 결과 출력 함수
// 검색 결과 출력 함수
function displaySearchResults(restaurants) {
let searchResultsElement = document.getElementById('searchResults');
searchResultsElement.innerHTML = ''; // 이전 검색 결과 지우기
// 검색 결과를 목록으로 변환하여 출력
restaurants.forEach(function(restaurant) {
let listItem = document.createElement('div');
// 각 장소의 이름과 주소 표시
listItem.innerHTML = '<strong>'
+ restaurant.title.replace(/<[^>]+>/g, '') // HTML 태그 제거
+ '</strong><br>' + restaurant.address;
searchResultsElement.appendChild(listItem);
});
}
마커 표시 함수
/ 마커 표시 함수
const imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";
function displayMarkers(restaurants) {
const map = new kakao.maps.Map(document.getElementById('map'), {
center: new kakao.maps.LatLng(restaurants[0].latitude, restaurants[0].longitude),
level: 3
});
// 모든 장소에 대해 마커 표시
restaurants.forEach(function(restaurant) {
const latitude = restaurant.latitude; // 위도
const longitude = restaurant.longitude; // 경도
// 마커 이미지 크기
const imageSize = new kakao.maps.Size(24, 35);
// 마커 이미지 생성
const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);
// 마커 생성
const marker = new kakao.maps.Marker({
map: map, // 지도
position: new kakao.maps.LatLng(latitude, longitude), // 위치
title: restaurant.title.replace(/<[^>]+>/g, ''), // 이름
image: markerImage // 이미지
});
// 마커 클릭 이벤트 등록
kakao.maps.event.addListener(marker, 'click', function() {
// 클릭한 마커의 정보를 모달로 표시
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
modal.style.display = "block";
modalContent.innerHTML = this.getTitle();
// 클릭한 마커 정보를 아래에도 표시
const clickedMarkerInfo = document.getElementById('clickedMarkerInfo');
clickedMarkerInfo.innerHTML = this.getTitle();
});
});
}
모달 닫기 버튼 클릭 이벤트
// 모달 닫기 버튼 클릭 이벤트
const closeBtn = document.getElementsByClassName("close")[0];
closeBtn.onclick = function() {
const modal = document.getElementById('modal');
modal.style.display = "none";
}
💡Naver Search API 결과
네이버 검색 API를 구현한 첫 화면이다.
검색 이전에는 지도가 표시될 위치에 '검색을 시작해주세요'라는 문구를 표시한다.
검색어로 'TCCINS'를 입력했을 때의 화면이다.🏢
검색어로 '수원 스타필드'를 입력했을 때의 화면이다.
하단의 검색 결과를 클릭하면 해당 위치로 이동하며 마커가 집중된다.
이번 프로젝트에서 네이버 검색 API에서 받아온 위치 정보를 이용하여 마커의 위치를 설정하고, 검색된 정보를 기반으로 Kakao Map API를 활용하여 지도에 마커를 표시하였다.
API를 잘 활용할 수 있도록 Document를 통해 기능을 숙지하는 것 외에도 언어 모델을 사용하여 프로젝트에 적용할 수 있도록 해야겠다.
참고 자료
API 공통 가이드, Naver Developers, 2021.08.27.
카카오 API, Kakao Developers, 2024.02.28.
SpringBoot에서 Naver API 연동하기, 판교의 메타몽, 2021.12.08.