Closed jellyjoji closed 9 months ago
페이지가 변경되면 이전 페이지의 모든 컴포넌트는 언마운트(unmount) 됩니다. 즉, 모두 사라집니다. 덩달아 컴포넌트의 상태도 모두 사라지므로 다시 페이지로 접근했을 때 모든 상태는 초기화(Reset)됩니다.
CreateRoom 에서 Location 페이지로 이동하는 순간 CreateRoom 페이지에서 사용자가 입력된 모든 정보는 사라집니다. 컴포넌트가 언마운트되었기 때문입니다. 반대로 Location 페이지에서 사용자가 선택한 주소 정보도 같은 이유로 사라집니다. 이유는 동일합니다.
페이지가 전환되어도 상태가 온전하게 유지되려면 컴포넌트 레벨에서 관리하는 것이 아니라 앱에서 상태를 관리하여야 합니다. 쉽게 말해 전역(global) 상태로서 관리해야 합니다.
React Context API 또는 Zustand 라이브러리를 사용해서 페이지가 전환되더라도 상태가 유지되도록 구현해야 합니다. 팀에서 선택한 방법으로 앱 상태 관리 코드를 작성하세요.
Context API를 사용하는 방법을 이해할 수 있게 코드 예시를 작성해보겠습니다.
답변드린 아래 파일에 작성된 주석을 참고해보세요.
App.jsx
import { createContext, useState } from 'react';
// 앱에서 사용될 공통 상태 관리를 위한 컨텍스트 객체를 생성합니다.
export const AppContext = createContext();
function App() {
// "방 만들기" 폼 입력에 필요한 모든 상태를 관리합니다.
const [createRoomForm, setCreateRoomForm] = useState({
category:'',
title: '',
content: '',
price: 0,
pickup: null,
payment: '',
participateCounter: 0,
meetingPoint: '',
uploadImage: null,
status: '',
location: '',
});
// 공급할 앱 상태 값(value)를 작성합니다.
const appState = {
// 예: state
createRoomForm,
// 예: setState
updateCreateRoomForm: (key, value) => {
setCreateRoomForm((state) => {
return {
...state,
[key]: value,
};
});
},
};
return (
<AppContext.Provider value={appState}>
<HelmetProvider>
<div className="max-w-xl mx-auto mt-12 font-pretendard bg-purple-200">
{/* 모든 페이지에 앱 상태 공급 */}
<RouterProvider router={router} />
</div>
</HelmetProvider>
<Toaster />
</AppContext.Provider>
);
}
CreateRoom.jsx
import { AppContext } from '@/App';
function CreateRoom() {
// 이전에 관리하던 모든 상태는 이제 직접 관리하지 않고 App에서 관리합니다.
// CreateRoom 페이지 컴포넌트는 AppContext로부터 상태 값을 읽고 쓸 수 있습니다.
const { createRoomForm, updateCreateRoomForm } = useContext(AppContext);
// 방 만들기 폼 입력할 때마다 앱에 상태를 저장해야 합니다.
// 그래야 Location 페이지로 이동할 때 기억이 되겠죠.
// 그러므로 파일을 제외한 나머지는 사용자가 입력할 때 상태를 업데이트해야 합니다.
// useRef를 사용하는 것이 아니라, updateCreateRoomForm 함수를 사용하세요.
// ...
return (...);
}
Location.jsx
import { AppContext } from '@/App';
function Location() {
// 이전에 관리하던 모든 상태는 이제 직접 관리하지 않고 App에서 관리합니다.
// Location 페이지 컴포넌트는 AppContext로부터 상태 값을 읽고 쓸 수 있습니다.
const { updateCreateRoomForm } = useContext(AppContext);
useEffect(() => {
// Kakao Map API 코드
// ...
}, []);
const handleSetLocation = () => {
// ...
// Kakao Map API에서 사용자가 선택한 주소 정보 값을 상태에 업데이트
updateCreateRoomForm('location', '주소 정보');
};
// 사용자가 지도에서 장소를 선택한 후 다시 CreateRoom 페이지로 이동하는 기능이 필요합니다.
// ...
return (...);
}
Location.jsx
import arrowLeft from '@/assets/icons/arrowLeft.svg';
import { useContext, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import './Location.module.css';
import { AppContext } from '@/App';
import Button from "../../components/Button";
const { kakao } = window;
function Location() {
const { updateCreateRoomForm } = useContext(AppContext);
const [data, setData] = useState();
useEffect(() => {
const mapContainer = document.getElementById('map'), // 지도를 표시할 div
mapOption = {
center: new kakao.maps.LatLng(37.57157200866145, 126.9763416696016), // 지도의 중심좌표
level: 4,
};
const map = new kakao.maps.Map(mapContainer, mapOption);
const geocoder = new kakao.maps.services.Geocoder();
const marker = new kakao.maps.Marker(),
infowindow = new kakao.maps.InfoWindow({ zindex: 1 });
searchAddrFromCoords(map.getCenter(), displayCenterInfo);
kakao.maps.event.addListener(map, 'click', function (mouseEvent) {
searchDetailAddrFromCoords(mouseEvent.latLng, function (result, status) {
if (status === kakao.maps.services.Status.OK) {
let detailAddr = !!result[0].address.address_name
? result[0].address.address_name
: '위치정보를 불러올수없음';
setData(detailAddr);
const content = '<div className="bAddr">' + detailAddr + '</div>';
marker.setPosition(mouseEvent.latLng);
marker.setMap(map);
infowindow.setContent(content);
infowindow.open(map, marker);
}
});
});
kakao.maps.event.addListener(map, 'idle', function () {
searchAddrFromCoords(map.getCenter(), displayCenterInfo);
});
function searchAddrFromCoords(coords, callback) {
geocoder.coord2RegionCode(coords.getLng(), coords.getLat(), callback);
}
function searchDetailAddrFromCoords(coords, callback) {
geocoder.coord2Address(coords.getLng(), coords.getLat(), callback);
}
function displayCenterInfo(result, status) {
if (status === kakao.maps.services.Status.OK) {
const infoDiv = document.getElementById('centerAddr');
for (let i = 0; i < result.length; i++) {
if (result[i].region_type === 'H') {
infoDiv.innerHTML = result[i].address_name;
break;
}
}
}
}
}, []);
useEffect(() => {
updateCreateRoomForm('meetingPoint', data);
}, [data]);
return (
<div className="h-full">
<div className="relative h-12">
<p className="text-center py-3">지도에서 위치 확인</p>
<Link to="/createroom">
<img src={arrowLeft} alt="뒤로 가기" className="absolute top-3" />
</Link>
</div>
<div className="map_wrap">
<div id="map" className="w-full h-[420px]"></div>
<div className="hAddr flex">
<span id="centerAddr" className='p-4'>
{data}
</span>
<Link to="/createroom" className="bg-white w-full absolute max-w-xl bottom-0 p-4 drop-shadow-2xl">
<Button type="submit" className="activeButton lgFontButton w-full ">
이 위치로 설정
</Button>
</Link>
</div>
</div>
</div>
);
}
export default Location;
MeetingPoint.jsx
import arrow from '@/assets/icons/arrow.svg';
import { Link } from 'react-router-dom';
function MeetingPoint({ title, ...restProps }) {
return (
<>
<div className="flex justify-between w-full py-4 bg-white">
<label>{title}</label>
<Link to="/location">
<img src={arrow} alt="만날 장소 지도 이동" {...restProps} />
</Link>
</div>
</>
);
}
export default MeetingPoint;
CreateRoom.jsx
import { AppContext } from '@/App';
import { pb } from '@/api/pocketbase';
import Button from '@/components/Button';
import FormInput from '@/components/FormInput';
import CreateHeader from '@/layout/CreateHeader';
import CategoryDropdown from '@/parts/create/CategoryDropdown';
import ContentTextarea from '@/parts/create/ContentTextarea';
import DatePicker from '@/parts/create/DatePicker';
import FileUpload from '@/parts/create/FileUpload';
import MeetingPoint from '@/parts/create/MeetingPoint';
import ParticipateCounter from '@/parts/create/ParticipateCounter';
import PaymentToggleButton from '@/parts/create/PaymentToggleButton';
import Status from '@/parts/create/Status';
import { ClientResponseError } from 'pocketbase';
import { useContext, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import Creator from '@/parts/create/Creator';
function CreateRoom() {
const { createRoomForm, updateCreateRoomForm } = useContext(AppContext);
const formRef = useRef(null);
const categoryRef = useRef(null);
const titleRef = useRef(null);
const contentRef = useRef(null);
const priceRef = useRef(null);
const dateRef = useRef(null);
const paymentRef = useRef(null);
const ParticipateCounterRef = useRef(null);
const uploadImageRef = useRef(null);
const statusRef = useRef(null);
const handleCreate = async (e) => {
e.preventDefault();
const categoryValue = categoryRef.current.value;
const titleValue = titleRef.current.value;
const contentValue = contentRef.current.value;
const priceValue = priceRef.current.value;
const dateValue = dateRef.current.value;
const paymentValue = paymentRef.current.dataset.payment;
const ParticipateCounterValue = Number(
ParticipateCounterRef.current.textContent
);
const meetingPointValue = createRoomForm.meetingPoint;
const creatorValue = createRoomForm.creator.id;
const uploadImageValue = uploadImageRef.current.files[0];
const statusValue = statusRef.current.value;
const data = new FormData();
data.append('category', categoryValue);
data.append('title', titleValue);
data.append('content', contentValue);
data.append('price', priceValue);
data.append('pickup', dateValue);
data.append('payment', paymentValue);
data.append('participateNumber', ParticipateCounterValue);
data.append('meetingPoint', meetingPointValue);
data.append('creator', creatorValue);
data.append('participate', creatorValue);
if (uploadImageValue) {
data.append('uploadImage', uploadImageValue);
}
data.append("status", statusValue);
for (const [key, value] of data.entries()) {
console.log(key, value);
}
// return
try {
await pb.collection('products').create(data);
// navigate('/products');
} catch (error) {
if (!(error instanceof ClientResponseError)) {
console.error(error);
}
}
}
return (
<>
<Helmet>
<title>방만들기</title>
</Helmet>
<div >
<CreateHeader />
<form
encType="multipart/form-data"
ref={formRef}
onSubmit={handleCreate}
>
<div className="flex flex-col gap-4 p-4 relative"
>
<CategoryDropdown
ref={categoryRef}
title="카테고리"
className="w-full defaultInput"
/>
<FormInput
ref={titleRef}
type="text"
placeholder="상품명을 입력해주세요."
labelClassName="product name"
inputClassName="defaultInput w-full"
label="상품명"
/>
<FormInput
ref={priceRef}
type="number"
placeholder="0원"
labelClassName="product price"
inputClassName="defaultInput w-full"
label="상품 가격"
/>
<ContentTextarea
ref={contentRef}
title="내용"
placeholder="공구 모임 주요내용을 알려주세요."
className="w-full defaultInput"
labelClassName="product content"
/>
<DatePicker
ref={dateRef}
title="픽업 날짜"
className="w-full defaultInput"
labelClassName="date Picker"
/>
<Status
ref={statusRef}
title="상태"
className="w-full defaultInput "
labelClassName="status"
/>
<Creator />
<PaymentToggleButton
ref={paymentRef}
title="정산 방법"
labelClassName="payment"
/>
<ParticipateCounter ref={ParticipateCounterRef} title="인원" />
<MeetingPoint title="만날 장소" />
<FileUpload
ref={uploadImageRef}
title="파일 업로드"
className="bg-[#EBF8E8] p-4 rounded-lg text-primary-500"
/>
</div>
<div className="bg-white fixed bottom-0 max-w-xl w-full p-4 drop-shadow-2xl">
<Button type="submit" className="activeButton lgFontButton w-full ">
방 만들기
</Button>
</div>
</form>
</div>
</>
);
}
export default CreateRoom;
내용
kakao Map API 를 구현한 다음 component 로 독립적인 페이지인 Location.jsx 로 분리하는 작업을 하고있습니다. 만날 장소 등록하기를 클릭하면 새 페이지에서 열리는 방식으로 분리했을때 지도에서 새 페이지에서 지도를 눌러 얻은 결과값을 현재 작업 중인 createRoom page 로 불러와야합니다. 해당 결과값을 불러와야 하는 방식에 대해 논의드립니다.
참고 이미지 (선택)
아래와 같이 지도에 찍힌 주소 결과값을 아래와 같이 메인 createRoom 페이지의 form data 로 불러와서 입력하는 방식에 대해 질문 드립니다.
(참고) 사용한 kakao map api 첨부 https://apis.map.kakao.com/web/sample/coord2addr/
※ 댓글에 이슈 해결 완료 후 결과 또는 해결 과정 이미지 첨부