개인 프로젝트를 진행하던 중, 다음과 같은 에러를 마주했다. 문제가 발생한 컴포넌트는 DetailContainer.tsx
, DetailBottom.tsx
, PlaceInfo.tsx
이고, 부모 -> 자식 순이다.
import React, { useState, useEffect } from "react";
import styles from "./DetailContainer.module.scss";
import DetailTop from "./top/DetailTop";
import DetailBottom from "./bottom/DetailBottom";
import type { pfDetail } from "@/models/detail.client";
interface DetailContainerProps {
pfId: string;
}
const DetailContainer: React.FC<DetailContainerProps> = ({ pfId }) => {
const [detail, setDetail] = useState<pfDetail>({
prfnm: { _text: "" },
prfpdfrom: { _text: "" },
prfpdto: { _text: "" },
fcltynm: { _text: "" },
poster: { _text: "" },
prfstate: { _text: "" },
prfruntime: { _text: "" },
prfage: { _text: "" },
pcseguidance: { _text: "" },
mt10id: { _text: ""},
dtguidance: { _text: "" },
styurls: {
styurl: { _text: "" },
},
relates: {
relate: {
relatenm: { _text: "" },
relateurl: { _text: "" },
},
},
});
useEffect(() => {
async function fetchDetailData() {
try {
const data = await fetch(`http://localhost:3000/detail/${pfId}`).then(
(res) => res.json()
);
setDetail(data);
} catch (error) {
console.log("공연 상세 데이터 불러오기 실패", error);
}
}
fetchDetailData();
}, [pfId]);
const {
prfnm,
prfpdfrom,
prfpdto,
fcltynm,
poster,
prfstate,
prfruntime,
prfage,
pcseguidance,
mt10id,
dtguidance,
styurls,
relates,
} = detail;
const topData = {
prfnm,
prfpdfrom,
prfpdto,
fcltynm,
poster,
prfstate,
prfruntime,
prfage,
pcseguidance,
relates,
};
const bottomData = { fcltynm, dtguidance, styurls, mt10id };
return (
<div className={styles["detail-background"]}>
<div className="container">
{topData && <DetailTop data={topData} />}
{bottomData && <DetailBottom data={bottomData} />}
</div>
</div>
);
};
export default DetailContainer;
import React, { useState } from "react";
import styles from "./DetailBottom.module.scss";
import type { TextNode } from "@/models/common.client";
import PerformanceInfo from "./PerformanceInfo";
import PlaceInfo from "./PlaceInfo";
interface DetailBottomPropsObj {
fcltynm: TextNode;
dtguidance: TextNode;
styurls: {
styurl: TextNode | TextNode[];
};
mt10id: TextNode;
}
interface DetailBottomProps {
data: DetailBottomPropsObj;
}
const DetailBottom: React.FC<DetailBottomProps> = ({ data }) => {
const [selectedTab, setSelectedTab] = useState("performance-info");
const { fcltynm, dtguidance, styurls, mt10id } = data;
const performanceInfoData = { dtguidance, styurls };
const placeInfoData = { fcltynm, mt10id };
console.log(placeInfoData);
return (
<div className={styles["bottom-info"]}>
<div className={styles["bottom-info__header"]}>
<div
className={`${styles["bottom-info__tab"]} ${
selectedTab === "performance-info" &&
styles["bottom-info__tab--active"]
}`}
onClick={() => setSelectedTab("performance-info")}
>
공연 정보
</div>
<div
className={`${styles["bottom-info__tab"]} ${
selectedTab === "place-info" && styles["bottom-info__tab--active"]
}`}
onClick={() => setSelectedTab("place-info")}
>
장소 정보
</div>
</div>
<div
style={{
display: selectedTab === "performance-info" ? "block" : "none",
}}
>
{performanceInfoData && <PerformanceInfo data={performanceInfoData} />}
</div>
<div
style={{
display: selectedTab === "place-info" ? "block" : "none",
}}
>
{placeInfoData && <PlaceInfo data={placeInfoData} />}
</div>
</div>
);
};
export default DetailBottom;
import React, { useState, useEffect, useRef } from "react";
import styles from "./PlaceInfo.module.scss";
import locationIcon from "@/assets/filter/location.svg";
import type { TextNode } from "@/models/common.client";
import supabase from "@/apis/supabase-client";
interface PlaceInfoPropsObj {
fcltynm: TextNode;
mt10id: TextNode;
}
interface PlaceInfoProps {
data: PlaceInfoPropsObj;
}
const PlaceInfo: React.FC<PlaceInfoProps> = ({ data }) => {
const [address, setAddress] = useState("");
const [hasLocation, setHasLocation] = useState(false);
const mapRef = useRef<HTMLDivElement>(null);
const { fcltynm, mt10id } = data;
interface LocationType {
address: string;
latitude: number;
longitude: number;
}
useEffect(() => {
// DB로부터 위치 정보를 가져오는 함수
const fetchLocationData = async (): Promise<void | LocationType> => {
console.log("mt10id:", mt10id._text);
const { data, error } = await supabase
.from("concert_halls")
.select("address, latitude, longitude")
.eq("code", mt10id._text)
.limit(1)
.single();
if (error) {
console.log("DB concert_halls 에서 위치 데이터 가져오기 실패", error);
} else {
console.log("data:", data);
return data;
}
};
const drawMap = (lat: number, lng: number) => {
const { naver } = window;
if (mapRef.current && naver) {
const location = new naver.maps.LatLng(lat, lng);
const map = new naver.maps.Map(mapRef.current, {
center: location,
zoom: 17,
});
new naver.maps.Marker({
position: location,
map,
});
}
};
const fetchLocationAndDrawMap = async () => {
const locationObj = await fetchLocationData();
if (locationObj) {
const { address, latitude, longitude } = locationObj;
setHasLocation(true);
setAddress(address);
drawMap(latitude, longitude);
} else {
setHasLocation(false);
}
};
fetchLocationAndDrawMap();
}, [mt10id._text]);
return (
<div className={styles["place-info"]}>
<section className={styles["hall"]}>
<p className={styles["hall__title"]}>공연장 정보</p>
<div className={styles["hall__detail"]}>
<img className={styles["icon"]} src={locationIcon} alt="" />
<div className={styles["hall__detail-text"]}>
<div className={styles["hall__detail-name"]}>{fcltynm._text}</div>
<div className={styles["hall__detail-address"]}>{address}</div>
</div>
</div>
</section>
<section className={styles["location"]}>
<p className={styles["location__title"]}>위치</p>
<div
ref={mapRef}
className={`${styles["location__map"]} ${
!hasLocation && styles["location__map--no-location"]
}`}
>
{!hasLocation && "위치 정보가 제공되지 않습니다."}
</div>
</section>
</div>
);
};
export default PlaceInfo;
DetailContainer.tsx
에서 공연 상세 데이터를 받아온 후, 해당 데이터에 포함되어 있는 공연장코드 mt10id
를 PlaceInfo.tsx
에 전달하여 DB에 있는 위치 데이터를 가져와야 하는 상황이었다.
그런데 에러 메시지를 보면PlaceInfo.tsx
에서 mt10id
를 제대로 읽어오지 못하고 있었다.
useEffect
의 실행 순서 문제였다. 나는 함수 컴포넌트의 호출 순서가 부모 -> 자식 순이기 때문에, useEffect
또한 부모에서 먼저 실행될 것이라 생각했다. 하지만 그와 반대로 useEffect
의 실행 순서는 자식 -> 부모 순이었다.
간단한 예제를 만들어 확인해보면 자식의 useEffect
가 먼저 실행된다.
import React, { useEffect } from "react";
const Child: React.FC = () => {
useEffect(() => {
console.log("Child useEffect!");
}, []);
console.log("Child!");
return (
<div></div>
);
}
const Parent: React.FC = () => {
useEffect(() => {
console.log("Parent useEffect!");
}, []);
console.log("Parent!");
return (
<div>
<Child />
</div>
);
};
export default Parent;
부모 컴포넌트인 DetailContainer.tsx
에서 비동기 호출을 통해 받아온 데이터를 바탕으로 자식 컴포넌트인 PlaceInfo.tsx
에서 DB의 데이터를 검색하여 받아와야 하는데, 자식의 useEffect
가 먼저 실행되다 보니 mt10id
가 비어 있는 상태로 검색을 했기 때문에 에러가 발생한 것이었다.
구글링을 하던 중 나와 유사한 문제를 겪은 사례가 있어 참고하였다.
Suspense Lazy를 사용했다고 한다.