나의 개발 일지

검색 기능 - URL에 query string 붙여서 요청과 응답 해보기

designer DK 2024. 10. 26. 15:50
728x90

상품 목록에 대해 원하는 상품 목록만 나오도록 할 수 있는 검색 기능을 진행해보았다.

특히 url 주소에 검색어를 넣어서 get 요청을하고 거기에 대한 응답을 할 수 있도록 만들어서 프론트엔드 백엔드 간 api를 통해 검색결과가 보여지도록 구현해보았다.

 

이번에도 gpt와 함께 코드를 짜서 모르는 개념들도 많이 나왔는데 새롭게 알게 된 개념들 위주로 정리해본다.

아래 코드가 url을 통한 검색을 구현한 프론트엔드 코드이다.

 

// 검색 기능을 수행하는 함수
function searchProducts(searchQuery) {
if (searchQuery) {
// 검색어가 있을 경우 해당 검색어로 검색 요청
fetch(`${URI}/api/product?name=${encodeURIComponent(searchQuery)}`)
.then((response) => response.json())
.then((data) => {
console.log("검색된 데이터:", data);
itemListContainer.innerHTML = "";
 
// 검색 결과 없을 때
if (data.data.length === 0) {
itemListContainer.innerHTML = `
<p>검색 결과가 없습니다.</p>
`;
return; // 더 이상 작업하지 않고 함수 종료
}
// 검색된 목록 표시
data.data.forEach((item, i) => {
const itemList = `
<div class="item-list" id="item-list${i}">
<span id="item-list-index">${i}</span>
<span id="item-list-sku">${item.sku}</span>
<span id="item-list-name">${item.name}</span>
<span id="item-list-price">${item.price}</span>
<img id="item-list-image" src="${item.image}" alt="">
<span id="item-list-status">${item.status}</span>
<button id="item-list-delete">삭제</button>
<button id="item-list-edit">수정</button>
</div>
`;
itemListContainer.insertAdjacentHTML("beforeend", itemList);
});
})
.catch((error) => console.error("Error:", error));
} else {
// 검색어가 없을 경우 전체 목록 다시 불러오기
getProducts();
}
}

// 검색 버튼 클릭 시 검색 수행
document.getElementById("search-btn").addEventListener("click", () => {
const searchInput = document.getElementById("search-input").value.trim();

// 검색어가 있을 때만 요청
if (searchInput) {
// URL에 쿼리 파라미터 추가
history.pushState({ search: true }, '', `?name=${encodeURIComponent(searchInput)}`);

// 검색 수행
searchProducts(searchInput);
} else {
alert("검색어를 입력하세요.");
}
});

// 뒤로 가기 또는 앞으로 가기 시 처리
window.addEventListener("popstate", (event) => {
const searchParams = new URLSearchParams(location.search);
const searchQuery = searchParams.get("name");

// 검색어가 있으면 입력 필드에 설정
document.getElementById("search-input").value = searchQuery || "";

// 검색어가 있으면 검색 수행, 없으면 전체 목록 다시 불러오기
searchProducts(searchQuery);
});

 

이 코드는 searchQuery와 searchInput 변수 값이 매우 중요해서 일단 뒤에서부터 보면 더 편한데

먼저 new URLSearchParams(location.search)라는 값을 searchParams라는 변수에 넣어준다.

여기서 location.search는 브라우저의 현재 URL에서 쿼리 스트링(물음표 뒤에 오는 부분)을 가져올 수 있다.

예를 들어, URL이 http://example.com/?name=test일 때, location.search는 "?name=test" 값을 반환하는 원리.

 

URLSearchParams는 URL의 쿼리 문자열을 다루기 위한 JavaScript 인터페이스이다. 이를 통해 URL에 포함된 쿼리 매개변수들을 쉽게 추가, 삭제, 수정할 수 있는데 여기서는 이걸 searchParams이라는 변수로 넣어준 후 searchParams.get("name")이라는 메소드를 실행해서 name에 대한 파라미터로 들어온 값을 get해서 searchQuery라는 변수로 넣어주도록 진행되었다.

 

검색어가 있는 경우 searchProducts 함수에 인자로 searchQuery를 넣어주면서 실행되는데

뒤로가기나 앞으로가기 버튼을 눌러서 페이지 주소가 바뀌어도 그 검색 내용에 맞게 페이지가 갱신되도록 하고 싶어서 window에 popstate라는 이벤트를 설정하고 searchProducts 함수가 popstate 이벤트가 일어날 때 마다 실행되도록 만들어주었다.

이렇게 하면 뒤로가기 버튼을 눌러도 이전 검색 결과에 맞게 그 결과들도 갱신된다.

 

그럼 이젠 코드 가장 위쪽에 있는 searchProducts를 보아야한다.

이 함수는 url 주소에 검색한 내용, 즉 쿼리스트링을 넣어서 백엔드에 검색결과를 요청하는 기능이다.

이 경우 검색 '쿼리스트링'이라는걸 요청함에도 불고하고 get 방식으로 요청을 할 수 있다.

즉 body에 내용을 넣어서 POST시키는 방식이 아닌 url에 쿼리스트링을 넣고 get 으로 요청하는 방식인데 여기서 넣게되는 쿼리스트링 부분을 함수 파라미터 값으로 제어할 수 있게 만들었다. (searchQuery, SearchInput 등)

 

요청하는 주소 뒤에 ?name=${encodeURIComponent(검색 값)} 으로 요청하는데

?가 쿼리스트링을 넣는다는 의미이며 그 중 name 부분에 검색어 입력 값을 넣어서 요청한다.

 

참고로 encodeURIComponent는 URL에서 안전하게 사용할 수 있도록 문자열을 인코딩해 주는 JavaScript 함수인데

예를 들어, 공백, 특수 문자, 또는 URL에 특별한 의미가 있는 문자를 주소에 넣어도 문제없도록 인코딩해준다.

 

아무튼 이 함수는 백엔드로 해당 url주소를 보내면서 요청을 하게되고 그 응답을 받게되면

검색결과가 없을 때에는 없다는 메시지를 띄워주고

검색결과가 있을 땐 해당 목록을 표시해주며 (forEach문)

아무것도 입력 안한 경우는 전체 목록을 보여주도록 세팅되었다.

 

그러고나면 검색 버튼을 누르게되고 그에 따른 과정이 나오는데

검색창 인풋에 입력해준 내용을 searchInput이라는 변수값으로 넣어주고 그 값을 이용해 실제 브라우저 주소 값을 바꿔준다.

여기서는 history.pushState라는 것이 사용되었는데 현재 브라우저 주소 페이지에서 url주소 값만 푸쉬해서 바꿔주고 페이지를 새로고침을 하지 않기 때문에 검색 결과로 인해 페이지가 바뀌어도 페이지 전체가 바뀌지 않아 좀 더 자연스러운 사용성을 제공해준다.

 

다음은 백엔드 코드.

productController.getProducts = async (req, res) => {
try {
//검색 값이 있을 때
const { name } = req.query;
//검색 조건 (복수 고려)
const condition = name ? { name: { $regex: name, $options: "i" } } : {}
//선언
let query = Product.find(condition);
//실행
const productList = await query.exec()

res.status(200).json({ status: "ok", data: productList });
} catch (error) {
res.status(400).json({ status: "fail", message: error.message });
};
};

 

검색내용이 지금은 name 밖에 없긴 한데 다른 쿼리스트링 값이 들어올 수 있다는 것도 고려한 구조로 만들기 위해 condition이라는 변수를 사용해서 다른 쿼리가 와도 대응이 쉽도록 확장성 있게 만들어졌다.

 

여기 검색 조건을 보면 $regex:name, $options:"i"가 쓰였는데 

$regex 는 검색어가 들어왔을 때 그걸 포함하는 데이터도 같이 검색해주기 때문에 좀 더 유연한 검색을 구현할 수 있다.

$options:"i"를 하게되면 대소문자 구분없이 검색해주기 때문에 이 또한 좀 더 유연한 검색에 유리하다.

 

condition이라는 변수로 DB에서 find를 한 후 query라는 변수에 넣어준 후 이를 .exec라는 것으로 실행시켜준다. 여기서 exec는 실행을 담당한다.

이렇게 하면 검색어가 있다면 그걸 토대로 db값을 찾아서 응답해주고 검색어가 없는 경우는 전체를 find시켜서 전체 목록을 응답해준다. :{}

 

이렇게 url주소를 통한 프론트엔드부터 백엔드까지의 코드가 잘 구현되었다.

검색어 입력 전 전체 목록
검색어 입력 후 실행된 화면