로컬 캐시 스토리지를 구현하려는데 생각보다 쉽지 않았다.
로컬 스토리지는 localStorage.setItem 등으로 쉽게 구현했는데, 캐시 스토리지 사용법은 잘 정리된 블로그를 찾기가 어려웠다.
따라서 크롬 블로그를 통해서 최대한 구현해보려고 했다!
https://web.dev/i18n/ko/cache-api-quick-guide/
나처럼 어려움을 겪는 사람들을 위해 캐시 스토리지를 구현하면서 발생한 에러와 설정하는 방법 디바운싱을 사용한 검색기능까지 공유해보려고 한다!
[내가 구현하고 싶은 것]
1. 검색창 입력으로부터 API 응답 결과를 로컬 캐시 스토리지에 저장한다.
2. 검색창에 똑같은 입력이라면 API를 호출하지 않고 캐시된 데이터를 불러온다.
=> '뇌'를 입력하면 이미 캐싱된 데이터를 불러오고, '갑'을 입력하면 새로운 입력어를 가진 이름으로 캐시가 저장되는것을 볼 수 있다.
=> api를 호출할 때 마다 'calling api' 가 출력되게끔 했는데, '뇌'를 입력하면 캐시된 데이터를 불러오기 때문에 콘솔이 찍히지 않는 것을 알 수 있다.
3. 입력마다 API 호출하지 않도록 API 호출 횟수를 줄이는 디바운싱을 구현한다!
# 1. 캐시 스토리지 저장 및 읽어오기
공유한 블로그를 보면 아래처럼
1. 캐시 생성을 위해 caches.open(캐시이름) 메서드를 사용하고
2. 캐시 저장은 cache.put(url, 새로 생성된 응답)을 사용하고,
3. 캐시 불러오기는 cache.match(url)을 사용한다.
따라서 캐시 코드를 아래와 같이 구현했다.
//cache.js
export async function setCachedData(cacheName, url, response) {
const cacheStorage = await caches.open(cacheName);
const init = {
headers: {
"Content-Type": "application/json, application/json; charset=utf-8",
"content-length": "2",
},
};
const clonedResponse = new Response(JSON.stringify(response), init);
await cacheStorage.put(url, clonedResponse);
return;
}
export async function getCachedData(cacheName, url) {
try {
const cacheStorage = await caches.open(cacheName);
const cachedResponse = await cacheStorage.match(url);
if (!cachedResponse || !cachedResponse.ok) {
return false;
}
return await cachedResponse.json();
} catch (error) {
console.error("Error while getting data from cache:", error);
return false;
}
}
// searchAPI.js
import { AxiosError } from "axios";
import { axiosInstance } from "./axiosInstance";
import { getCachedData, setCachedData } from "../utils/cache";
const searchKeyword = async (keyword) => {
try {
const cacheName = `cache_${keyword}`;
const url = `url자리입니당?name=${keyword}`;
let cachedData = await getCachedData(cacheName, url);
if (cachedData) {
return cachedData;
}
const response = await axiosInstance.get("/search-conditions", {
params: { name: keyword },
});
await setCachedData(cacheName, url, response);
console.info("calling api");
return response;
} catch (error) {
if (error instanceof AxiosError) {
return error.response;
}
console.error("Error while searching for keyword:", error);
return { data: [] }; // 빈 배열로 감싸서 반환
}
};
const searchApi = {
searchKeyword,
};
export default searchApi;
이에 더해 API 호출을 줄이기 위한 디바운스 함수를 만들어 타이머를 지정하고 입력의 가장 마지막에만 요청을 보내도록 했다.
// SearchBox.jsx
import React, { useState } from "react";
import searchApi from "../api/seachAPI";
import ResultList from "./ResultList";
import { debounce } from "../utils/debounce";
export default function SearchBox() {
const [searchText, setSearchText] = useState("");
const [result, setResult] = useState([]);
const handleChange = (e) => {
setSearchText(e.target.value);
debounce(() => handleSearch(e.target.value), 400)();
};
const handleSearch = async (searchValue) => {
try {
if (searchValue.length !== 0) {
const res = await searchApi.searchKeyword(searchValue);
setResult(res.data);
}
} catch (e) {
console.error("error", e);
}
};
return (
<div className="flex flex-col w-72 m-auto mt-10">
<div className="flex rounded-md overflow-hidden">
<input
className="p-3 flex-grow"
type="text"
value={searchText}
onChange={handleChange}
/>
<button className="bg-blue-600 text-white p-3">검색</button>
</div>
{searchText.length > 0 ? <ResultList result={result} /> : null}
</div>
);
}
// debounce.js
export const debounce = (callback, delay) => {
let timerId;
return (...args) => {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
callback(...args);
}, delay);
};
};
위 코드가 나의 완성본이고, 아래는 내가 이 코드를 구현하기 위해서 겪은 과정이다!
같은 문제를 겪는 사람들에게 도움이 되었으면 좋겠다!
# 2. 내가 겪은 에러
초기에 로컬 캐시 스토리지 저장하는 코드는 아래와 같았다.
코드로 캐시 스토리지 사용하려는데 계속 에러가 생김.
clone is not a function...
import { AxiosError } from "axios";
import { axiosInstance } from "./axiosInstance";
const searchKeyword = async (keyword) => {
try {
const cacheName = "result_cache";
const url = `https://api.clinicaltrialskorea.com/api/v1/search-conditions/?name=${keyword}`;
const response = await axiosInstance.get("/search-conditions", {
params: { name: keyword },
});
console.log(response);
const cacheStorage = await caches.open(cacheName);
await cacheStorage.put(url, response.clone());
return response;
} catch (error) {
if (error instanceof AxiosError) {
return error.response;
}
console.error("Error while searching for keyword:", error);
return { data: [] }; // 빈 배열로 감싸서 반환
}
};
const searchApi = {
searchKeyword,
};
export default searchApi;
# 2-1. 에러 해결
해당 에러를 해결하기 위해 끊임없는 구글링에도 불구하고 답을 얻지 못했다.
그리고 chat gpt의 도움을 받았다.
gpt로부터 답을 얻고 수정된 코드는 아래와 같은데 , api응답 데이터 객체를 생성하고 JSON.srringfy를 통해서 문자열로 저장한 후에 put을 통해 캐시 스토리지에 저장한다!
import { AxiosError } from "axios";
import { axiosInstance } from "./axiosInstance";
const searchKeyword = async (keyword) => {
try {
const cacheName = "result_cache";
const url = `<url 문자열 보안문제로 못보여줍니당>?name=${keyword}`;
const response = await axiosInstance.get("/search-conditions", {
params: { name: keyword },
});
const cacheStorage = await caches.open(cacheName);
const data = await response.data;
const clonedResponse = new Response(JSON.stringify(data));
await cacheStorage.put(url, clonedResponse);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return error.response;
}
console.error("Error while searching for keyword:", error);
return { data: [] }; // 빈 배열로 감싸서 반환
}
};
const searchApi = {
searchKeyword,
};
export default searchApi;
# 3. 문제 발생
잘 돌아가는줄 알았는데!!
저장된 캐시를 보니 이상하다... 인코딩 문제가 있는 것 같은데,, 원래 이렇게 저장되나요..?
아시는 분들은 댓글을 통해서 알려주시면 너무너무 감사하겠습니다!!
일단 저장된 캐시를 아래 코드로 읽어와봤는데 데이터를 잘 읽어온다. 읽어오는 것은 문제가 없는 것 같다...(내 추측)
// 캐시된 데이타 읽어오는 함수
export async function getCachedData(cacheName, url) {
try {
const cacheStorage = await caches.open(cacheName);
const cachedResponse = await cacheStorage.match(url);
if (!cachedResponse || !cachedResponse.ok) {
return false;
}
return await cachedResponse.json();
} catch (error) {
console.error("Error while getting data from cache:", error);
return false;
}
}
# 4. 최종 캐시 스토리지 설정 코드
맨 위에서는 cacheName을 고정된 문자열로 저장했는데, 이렇게 저장하면 캐시네임이 동일하므로 캐싱이 제대로 이루어지지 않았다!
따라서 최종적으로 캐싱 스토리지 구현 코드는 아래와 같다!
//searchAPI.js
import { AxiosError } from "axios";
import { axiosInstance } from "./axiosInstance";
import { getCachedData, setCachedData } from "../utils/cache";
const searchKeyword = async (keyword) => {
try {
const cacheName = `cache_${keyword}`;
const url = `https://api.clinicaltrialskorea.com/api/v1/search-conditions/?name=${keyword}`;
let cachedData = await getCachedData(cacheName, url);
if (cachedData) {
return cachedData;
}
const response = await axiosInstance.get("/search-conditions", {
params: { name: keyword },
});
await setCachedData(cacheName, url, response);
console.info("calling api");
return response;
} catch (error) {
if (error instanceof AxiosError) {
return error.response;
}
console.error("Error while searching for keyword:", error);
return { data: [] }; // 빈 배열로 감싸서 반환
}
};
const searchApi = {
searchKeyword,
};
export default searchApi;
//cache.js
export async function setCachedData(cacheName, url, response) {
const cacheStorage = await caches.open(cacheName);
const init = {
headers: {
"Content-Type": "application/json, application/json; charset=utf-8",
"content-length": "2",
},
};
const clonedResponse = new Response(JSON.stringify(response), init);
await cacheStorage.put(url, clonedResponse);
return;
}
export async function getCachedData(cacheName, url) {
try {
const cacheStorage = await caches.open(cacheName);
const cachedResponse = await cacheStorage.match(url);
if (!cachedResponse || !cachedResponse.ok) {
return false;
}
return await cachedResponse.json();
} catch (error) {
console.error("Error while getting data from cache:", error);
return false;
}
}
//SearchBox.jsx
import React, { useState } from "react";
import searchApi from "../api/seachAPI";
import ResultList from "./ResultList";
export default function SearchBox() {
const [searchText, setSearchText] = useState("");
const [result, setResult] = useState([]);
// fix: 검색창에 입력 텍스트 없을 때 호출 막아야함
const handleChange = async (e) => {
setSearchText(e.target.value);
if (searchText.length > 0) {
const res = await searchApi.searchKeyword(e.target.value);
setResult(res.data);
}
};
return (
<div className="flex flex-col w-72 m-auto mt-10">
<div className="flex rounded-md overflow-hidden">
<input
className="p-3 flex-grow"
type="text"
value={searchText}
onChange={handleChange}
/>
<button className="bg-blue-600 text-white p-3">검색</button>
</div>
{searchText.length > 0 ? <ResultList result={result} /> : null}
</div>
);
}
'React' 카테고리의 다른 글
[React] Netlify를 통해 서버리스 만들기 및 netlify: command not found 해결방법(엄청난 에러의 연속..) (0) | 2023.05.17 |
---|---|
[React] onKeyDown 이벤트 중복으로 2번 실행되는 문제 및 해결방법 (4) | 2023.05.10 |