이전 포스팅에서 내 상황에서 해결할 수 있는 조합은
- Next js에서는 api router를 통해 proxy server와 같은 역할을 해주며,
CORS, 그리고 masking 하여 보안상이유로 사용 할 수 있는 Node기반
serverless Function이다.- Nextjs에서는 사용자가 어떤 url로 접근시 특정 사이트로 옮겨주는 자체 기능이 있는데
rewrite와 redirect 이다.
api router와 rewrite를 적절히 같이 사용하면 해결이 가능 할 것이라 적었다.
이제는 직접 해보면서 해결을 해보자.
api router에서 비동기 통신을 해야 하므로, axios를 사용 할 것인데 먼저, axios가 node.js에서 사용 할 수 있는지 찾아보았다.
req.body에 받아와야 하는 값은
LOCALDATA_020301_
: 동물병원의 query 값 중 하나LOCALDATA_020302_
: 동물약국의 query 값 중 하나LOCALDATA_020301(2)_${지역}
: 지역구LOCALDATA_020301(2)_${지역}/1/${범위}
: 데이터 몇 개 호출 할 범위(최대 1000개)한줄로 요약 하자면
animalHospitalAPI(`${LOCALDATA_020301_}${지역}/1/${범위}`)
animalPharamcyAPI(`${LOCALDATA_020302_}${지역}/1/${범위}`)
이렇게 되시겠다. 아래의 로직은 코드 전반이다.
//api/router~~
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from 'next';
const animalHospitalAPI = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL}`,
});
const animalPharamcyAPI = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY}`,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { api_type, query_type, area, endRange } = req.body;
const api_fn = {
animal_hospital: animalHospitalAPI,
animal_pharmacy: animalPharamcyAPI,
};
try {
const result = await api_fn[
api_type as "animal_hospital" | "animal_pharmacy"
](`${query_type}${area}/1/${endRange}/01`);
res.status(200).send({ data: result.data, query_string: query_type, area });
} catch (err) {
res.status(500).send(err);
}
}
저번 포스팅에서 동물병원&동물약국
API, 두 개의 API들을 동시에 호출 해야 하므로,
useQuery
+ Promise.all
조합으로 비동기 호출을 진행 했었는데, 그것을 수정해서 다시 만들었다.
LOCALDATA_020301_
: 동물병원의 query 값 중 하나LOCALDATA_020302_
: 동물약국의 query 값 중 하나/**
*
* @param api_query react-query에서 enabled와 지역구 query로 활용하는 인자 값입니다.
* @returns
*/
const DYNAMIC_API_QURIES = [
{ api_type: "animal_hospital", query_type: "LOCALDATA_020301_" },
{ api_type: "animal_pharmacy", query_type: "LOCALDATA_020302_" },
];
// process.env.NEXT_PUBLIC_SEOUL_API_URL은 api router 주소입니다.
export const ParalledQueriesAnimalMedicineAPI = async (
area: string | null,
endRange: string,
) => {
try {
const results = await Promise.all(
DYNAMIC_API_QURIES.map(async (query) => {
const result = await axios.post(
`${process.env.NEXT_PUBLIC_SEOUL_API_URL}`,
{
area,
api_type: query.api_type, // api router에서 조건부 함수 처리위해서
query_type: query.query_type, // 동물병원 || 동물약국 query
endRange,
},
);
return result.data;
}),
);
return results;
} catch (err) {
console.log(err, "map Error");
return []; // 성공 실패시 균일하게 해주기 위해서
}
};
비동기 함수를 만들어주고 useQuery로 감싸주었다.
const MixedContent = ({ endRange }: { endRange: string }) => {
const [query, setQuery] = useState<string | null>(null);
const { data, isLoading } = useQuery({
queryKey: [SEOUL_QUERY.MEDICINE, query],
queryFn: () => ParalledQueriesAnimalMedicineAPI(query, endRange),
enabled: !!query,
select: refineSeoulApiData, // 원하는 데이터로 가공
refetchOnWindowFocus: false,
staleTime: Infinity,
});
return (
<div style={{ display: "flex", gap: "20px" }}>
{SEOUL_LOCATION.map((Area) => (
<span
onClick={() => setQuery(Area.api_query)}
key={Area.location}
style={{
width: "80px",
backgroundColor: "wheat",
color: "black",
height: "80px",
textAlign: "center",
}}
>
{Area.location}
</span>
))}
<div style={{ alignSelf: "center", fontSize: "24px" }}>
데이터가 mixed content없이 불러와진{" "}
<span
style={{
textDecoration: "underline",
color: "bisque",
fontSize: "32px",
}}
>
{data ? data?.length : 0}
</span>{" "}
개
</div>
</div>
);
};
export default MixedContentError;
끝났으면 이제 Nextjs config를 건드려보자
const nextConfig = {
reactStrictMode: true,
async rewrites() {
return [
{
source: "/api/mixedError/mixedContents",
destination: `${process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL}`,
},
{
source: "/api/mixedError/mixedContents",
destination: `${process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY}`,
},
];
},
};
// 동물병원 API
process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL=http://openapi.seoul.go.kr:8088/secret-number/json/
// 동물약국 API
process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY=http://openapi.seoul.go.kr:8088/secret-number/json/
source는 내가 api router(proxy)로 요청을 보내면 destination을 env 경로로 맵핑해주세요 이다.
이렇게 한 후 build 한 후 vercel 배포 후 한 번 확인해 보자
cf) 참고로 destination의 주소가 똑같은데 아래를 확인하면 불편한 마음이 사라질것이다.
배포 후 확인을 해봤다.
성공했다.
/api/mixedError/mixedContents
로 경로를 요청했다면, 서버는 destination
의 경로로 매핑하기에
사실상 2개를 쓸 의미가 없다. 그러므로
결국 mixed content에러는 상반된 프로토콜을 사용하기에 일어나는 것이다는 점을 인지하고,
rewrite로
http로 맵핑해주는 것이기에, 하나의 url로 통일 및,
서울시 공공데이터의 도메인만 적어줘도 문제가 없다.
따라서 Refactoring을 진행 하였다.
async rewrites() {
return [
{
source: "/api/mixedError/mixedContents",
destination: `${process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL}`,
},
{
source: "/api/mixedError/mixedContents",
destination: `${process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY}`,
},
];
},
// nextjs.config.mjs
{
source: "/api/mixedError/mixedContents",
destination: `${process.env.NEXT_PUBLIC_SEOUL_API_URL}`,
},
// As-Is
process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL=http://openapi.seoul.go.kr:8088/secret-number/json/
// 동물약국 API
process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY=http://openapi.seoul.go.kr:8088/secret-number/json/
// To-Be
NEXT_PUBLIC_SEOUL_API_URL=http://openapi.seoul.go.kr:8088/
하나의 env로 바꿨는데,
2가지를 삭제해 주고, 아래와 같이 변경해주면 마음이 편안하다.
//api/mixed~~
//...생략
const animalHospitalAPI = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_HOSPITAL}`,
});
const animalPharamcyAPI = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_PHARAMCY}`,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { api_type, query_type, area, endRange } = req.body;
const api_fn = {
animal_hospital: animalHospitalAPI,
animal_pharmacy: animalPharamcyAPI,
};
try {
const result = await api_fn[
api_type as "animal_hospital" | "animal_pharmacy"
](`${query_type}${area}/1/${endRange}/01`);
//...생략
// animalHospitalAPI과 animalPharamcyAPI를 통합
const animalMedicineAPI = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_ANIMAL_MEDICINE}`,
});
const { query_type, area, endRange } = req.body; // api_type삭제
try {
// 조건으로 해주지 않아도 된다.
const result = await animalMedicineAPI(
`${query_type}${area}/1/${endRange}/01`,
);
/**
*
* @param api_query react-query에서 enabled와 지역구 query로 활용하는 인자 값입니다.
* @returns
*/
const DYNAMIC_API_QURIES = [
{ api_type: "animal_hospital", query_type: "LOCALDATA_020301_" },
{ api_type: "animal_pharmacy", query_type: "LOCALDATA_020302_" },
];
//... 생략
const result = await axios.post(
`${process.env.NEXT_PUBLIC_SEOUL_API_URL}`,
{
area,
api_type: query.api_type, // api router에서 조건부 함수 처리위해서
query_type: query.query_type, // 동물병원 || 동물약국 query
endRange,
},
);
const DYNAMIC_API_QURIES = [
{ query_type: "LOCALDATA_020301_" },
{ query_type: "LOCALDATA_020302_" },
];
//... 생략
const result = await axios.post(
`${process.env.NEXT_PUBLIC_MY_API_URL}`,
{
// api_type을 쓸 이유가 없어짐
area,
query_type: query.query_type, // 동물병원 || 동물약국 query
endRange,
},
);
//...생략
mixed content에러를 통해서
하나의 page에서 정말 많은 것을 배우고, 생각하고, 알아가면서, 문제를 해결해왔던 것같다.
지도 페이지에서 외부 API와 외부 라이브러리, 그리고 배포 후 문제까지 해결해가면서 정말 좋은 프로젝트 경험을 해온것 같다.
내가 작성한 코드는 블로그에 쓰기 위해 따로 작성한 코드이며 위의 블로그용 코드는 lunaxislu 여기서 확인이 가능합니다.
무언가 궁금하거나 잘못된 내용이 있으면 댓글 남겨주세요 !!
async 함수에서 try catch를 쓴 이유가 있나요? await then으로도 해결가능해보이는데