next.js를 기반으로 한 서비스에 백엔드 서버와는 별개로 간단하게 인기 검색어 기능을 간단하게 추가해 본 경험을 기록합니다.
해당 시도를 해본 이유는 인기 검색어가 크게 필요하다기 보다, Firebase나 noSQL 느낌의 DB를 간단히 써보고 싶다! 라는 생각이 강했습니다. 갑자기 noSQL을 써보고 싶던 건 Firebase나 다른 클라우드 서비스에서 무료로 제공하는 DB(Azure Cosmos DB 등)가 많기 때문인데, 그냥 놀리고 있기도 아까워 한번 사용해보고 싶다는 생각이었습니다.
또한 Serverless 들을 조합해 보고 싶기도 했습니다. 그러나 Firestore만을 이용하게 되어 따로 클라우드 서버리스를 경험해보지는 못했습니다.
어떻게 만들 것인가?
간단히 검색들을 기록하고, 상위순으로 반환해주면 되겠다는 생각으로 시작했습니다. noSQL DB를 이용하면 이러한 검색어들을 저장하고 이용하기 좋겠다는 생각을 했습니다. 기능보다 사실 DB를 써보고 싶은 것이었기에 사용할 DB를 먼저 정하기로 했습니다.
DB 선택
DB선택에는 Firebase를 써보기로 했습니다. 이유는 이전에 여러 튜토리얼들에서 Firebase의 테스트용 Realtime Database를 써본 적이 있지만, 실제 프로덕션용으로 써본 적이 없고, Realtime DB보다 권장하는 Firestore DB를 써본 적이 없기 때문입니다. 즉 조금 사용해 본 경험이 있는 것을 심화해서 사용해보고 싶었습니다.
검색어를 저장할 방법
그 다음으로는 검색어의 저장방식을 생각해 보았습니다. 가장 먼저 생각했던 것은 각각의 검색을 저장하는 것이었습니다. 즉 아래와 같은 형태가 될 것입니다.
[
{
"keyword": "검색어1",
"searchedAt": 2023-02-03 03:30
},
{
"keyword": "검색어2",
"searchedAt": 2023-02-03 03:31
}
]
위와 같이 검색어와 검색시간을 저장하고 일정시간(하루)이 지난 후 계속 삭제하는 방식을 처음으로 생각했습니다. 그러나 이런 방법이라면 검색시마다 각 문서가 추가되고, 지속적으로 확인하여 일정시간이 지난 문서들을 삭제해 주어야 했습니다. 그리고 인기검색어 확인 시 매번 모든 키워드들을 확인해서 인기 검색어를 선별하는 작업이 필요했습니다.
그다음 생각한 것은 다음과 같은 방식입니다.
[
{
"keyword": "검색어1",
"searchCount": 5,
"lastSearchedAt": 2023-02-03 03:30
},
{
"keyword": "검색어2",
"searchCount": 4,
"lastSearchedAt": 2023-02-03 03:31
}
]
위와 같이 검색 횟수와 마지막 검색 시간을 저장하는 방식입니다. 해당 방식으로 문서의 양을 크게 줄일 수 있을 것으로 보였습니다.
매일 초기화해주거나, 매일 한번 searchCount에 0.xx배를 해서 감소시키는 방법을 사용한다면 매일의 인기 검색어를 보여줄 수 있을 것 같았습니다. 그리고 마지막 검색시간 정보도 기록하여 많이 검색되지 않은 기록을 탈락시킬 수도 있습니다.
이런 방식을 사용하여 정확도는 떨어지나, 용량과 성능면에서 이득을 볼 수 있었습니다.
구현
이제 구현의 시간이었습니다. 흔히 Firebase의 서비스들은 백엔드 서버를 가지지 않고도 빠르게 앱을 론칭할 수 있게 도와주는 것들로 유명합니다. 즉 마치 백엔드 서버를 이용하듯 Firestore 서비스를 사용할 수 있었습니다.
아래는 Next.js 프레임워크를 이용한 js앱으로 개발한 기록입니다.
firebase 사용
우선 npm install firebase로 설치 후, firebaseConfig를 설정하고 getFirestore()를 통하여 db를 가져왔습니다.
https://firebase.google.com/docs/firestore/quickstart?hl=ko&authuser=0
next.js 백엔드 api 추가
이후 인기 검색어를 가져오는 api endpoint를 추가했습니다.
import db from "./firebase/firebaseAdmin";
import admin from "firebase-admin";
export default async function handler(req, res) {
if (req.method === "POST") {
// 검색어 저장
const { keyword } = req.body;
const keywordRef = db.collection("keywords").doc(keyword);
try {
const docSnap = await keywordRef.get();
if (docSnap.exists) {
await keywordRef.update({
count: admin.firestore.FieldValue.increment(1),
lastSearched: new Date(),
});
} else {
await keywordRef.set({
count: 1,
lastSearched: new Date(),
});
}
res.status(200).json({ message: "Keyword updated successfully" });
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === "GET") {
// 인기 검색어 조회
try {
const snapshot = await db.collection("keywords").orderBy("count", "desc").limit(10).get();
const keywords = snapshot.docs.map((doc) => doc.id);
res.status(200).json(keywords);
} catch (error) {
res.status(500).json({ error: error.message });
}
} else {
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
검색 시 post로 검색어를 전달하게 했고, post시 이미 존재하는 문서면 count를 올리고 최근 검색시간을 업데이트하고, 아니면 새로 문서를 만들게 설정했습니다.
인기 검색어를 불러오기 위해 get 요청을 주면, 최대 10개까지 인기 검색어를 가져오게 했습니다.
! 이때 firebase가 아닌 firebase-admin을 사용했는데 전환의 이유는 이후에 정리했습니다.
데이터 보안
이때 FireStore은 테스트 시가 아닐 때는 다음과 같은 보안 정책을 적용하길 권장합니다.
// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
이는 auth가 되지 않은 유저의 경우 요청이 안되게 하는 것으로, 웹브라우저 클라이언트단에서 인증된 유저에 한정하여 작동하게 하는것으로 보입니다. 즉 유저 인증이 필요한 것인데, Firebase 인증을 쓰지 않음으로 해당 정책을 사용하기엔 문제가 있었습니다. 그렇다고 테스트모드로 계속 정책을 열어둘 수도 없었습니다.
시행착오: Firebase 앱 체크 시도
이때 Firebase 인증이 아닌 Firebase 앱 체크를 계속 시도했었는데, 앱 체크는 인증과 달리 추가적인 보안을 위한 장치로써, 해당 조치를 취한다고 해도 request.auth != null 조건을 통과할 수 있는 방안은 아닌 것으로 보입니다. 이를 착각하여 앱 체크를 계속 시도했고, reCAPTCHA 적용까지 한 후 되지 않은 것을 보고서 잘못한 것을 깨달았습니다.
https://firebase.google.com/docs/app-check?authuser=0&hl=ko
데이터 보안 통과
데이터 보안 통과를 위해 인증을 하거나 다른 방식을 사용해야 했습니다. 이쯤에서 추가적으로 인증을 진행하거나 하면 점점 더 배보다 배꼽이 커지는 느낌으로 시간이 더 걸릴 듯하여 최대한 피하고 싶었습니다.
원하는 것은 어차피 next.js 서버사이드가 존재하니 서버사이드에서만 화이트리스트로 접근가능하여도 되는데, 이를 위한 firebase-admin이 있었습니다. 해당 방식으로 firebase를 사용 시에는 위의 보안 규칙을 무시하고 무조건적으로 FireStore에 접근이 가능했습니다.
firebase-admin 설정
firebase-admin 사용을 위해서는 적절한 json파일을 받아 인증하고 사용할 수 있습니다. 형식은 아래와 같습니다.
{
"type": "service_account",
"project_id": "~~~",
"private_key_id": "~~~",
"private_key": "~~~",
"client_email": "~~~",
"client_id": "~~~",
"auth_uri": "~~~",
"token_uri": "~~~",
"auth_provider_x509_cert_url": "~~~",
"client_x509_cert_url": "~~~",
"universe_domain": "~~~"
}
이후 firebaseAdmin.js 파일을 설정해 줍니다.
import admin from "firebase-admin";
if (!admin.apps.length) {
const serviceAccount = require("~~~.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}
export default admin.firestore();
이렇게 하면 db가 export 되어 기존 firebase의 db와 매우 비슷한 방식으로 사용이 가능한 것으로 보입니다.
후기
대략적으로 만들어낼 수 있었지만 아직 매일 어느 정도 초기화하는 기능을 추가해놓지 않아서 추가적으로 작업이 필요합니다.
작업에는 대략 3시간 정도 걸렸는데 시간을 클라우드 서비스들을 최대한 간단히 이용해서 빨리 짜보겠다는 목적은 달성할 수 있던 것 같습니다.
과정 중에 인증과 reCAPTCHA 등 평소에 관심이 많던 악의적인 사용자들을 거르는 법에 조금 더 지식이 생긴 것 같고, 검색어들을 어떻게 저장할지 구조를 짜는 것도 재밌었습니다.
'회고 & 기록' 카테고리의 다른 글
썸네일 이미지 용량 최적화를 고민해보았다 (0) | 2023.12.28 |
---|---|
CSE3210 오픈소스응용프로그래밍을 수강하고 (0) | 2023.12.09 |
CSE1312 이산구조를 수강하고 (0) | 2023.12.08 |
앱 개발 프레임워크->웹 프레임워크->웹페이지 렌더링들을 둘러본 이야기 (0) | 2023.08.05 |
카카오 로그인후 기존페이지로 복귀 시도기 (0) | 2023.06.27 |