yamoo9 / likelion-FEQA

질문/답변 — 프론트엔드 스쿨, 멋사
29 stars 9 forks source link

[LAB-3] useEffect 와 렌더링 문제 (하위 컴포넌트 리-렌더링 이슈) #299

Closed choinamechoi closed 1 year ago

choinamechoi commented 1 year ago

질문 작성자

최영범

문제 상황

문제 요약 : useEffect에 조건부 렌더링이 되도록 코드를 작성했다. 그러나 리렌더링 시 조건이 바뀌지 않았음에도 useEffect가 실행이 되어 useEffect안의 함수가 불필요하게 실행이 된다.

문제 상세 :

image

-목록 보기 버튼을 눌렀을 때 렌더링 되는 화면 과 콘솔 image

image

원하는 흐름

  1. Main.jsx 가 렌더링 된다.
  2. 목록보기 버튼 누른다.
  3. Search.jsx 렌더링 ( useEffect의 조건인 searchText가 수정되지 않았으니 useEffect가 실행 안됨)

문제가 되는 흐름

  1. Main.jsx 가 렌더링 된다.
  2. 목록보기 버튼 누른다.
  3. Search.jsx 렌더링
  4. kakaomap.jsx의 useEffect가 실행.
  5. useEffect안의 함수가 불필요하게 실행.

문제 추가 설명 image

처음 앱을 켰을때 useEffect는 마운트 될때 실행이되니 콘솔이 1,2,3번까지 찍히는것은 이해가됩니다.

그러나 이후에 useEffect의 조건인 searchText를 수정하지않은 상태에서 목록보기 버튼을 누르면 4,5 콘솔 까지만 실행이 되고 6번은 조건이 변하지 않았으니 실행이 안되어야 되는것이 아닌가요?

추측 : 목록보기 버튼을 누르면 포탈로 설정된 search.jsx 가 렌더링 되도록 코드를 작성했는데 포탈이 마운트,언마운트 되고 Main.jsx 안의 내용(포탈의 마운트,언마운트)이 바뀌니 Main.jsx도 다시 마운트가 되어 useEffect가 실행이 된다.??

프로젝트 저장소 URL

https://github.com/choinamechoi/car-zip/tree/feature/%2313

[feature/#13 브랜치]

.env 파일이 필요하시면 제가 예전에 디스코드 채팅으로 한번 보냈습니다!

감사합니다.

yamoo9 commented 1 year ago

문제 분석

React DevTools → Profiler 패널에서 렌더링 이슈를 진단해보면 KakaoMap 컴포넌트가 리-렌더링 된 이유를 알 수 있습니다. 상위 컴포넌트 Main이 리-렌더링 되었기 때문에 하위 컴포넌트 또한 리-렌더링 된 것입니다. 🤔

상위 컴포넌트의 return 으로 반환되는 JSX 구문에 포함된 하위 컴포넌트는 이전과 다른 데이터로 인식 되므로 리-렌더링 됩니다.

문제 해결

React.memo를 사용해 KakaoMap 컴포넌트가 리-렌더링 되지 않도록 설정해야 합니다.

그러면 아래 영상처럼 상위 컴포넌트가 리-렌더링 된다 하더라도 Props가 변경되지 않는 이상 메모된 하위 컴포넌트의 불필요한 리-렌더링을 막을 수 있습니다.

문제 해결을 위해 KakaoMap 컴포넌트 코드를 아래와 같이 수정하세요.

import { memo, useRef, useEffect, useState } from 'react';
// ...

const KakaoMap = memo(function KakaoMap(props) {
  // ...
});

export default KakaoMap;
choinamechoi commented 1 year ago

답변 감사합니다.

한가지 이해가 안가는게 kakaomap.jsx의 useEffect의 deps에 searchText 라는 변수를 넣어두었는데 ,

이 searchText는 recoil로 관리를 하고 있습니다.

main.jsx 가 리-렌더링이 되어 kakaomap.jsx가 리-렌더링이 된다고 하여도 searchText는 변하지 않았으니 useEffect는 실행이 안되어야 하는것이 아닌가요?

yamoo9 commented 1 year ago

답변 감사합니다.

한가지 이해가 안가는게 kakaomap.jsx의 useEffect의 deps에 searchText 라는 변수를 넣어두었는데 ,

이 searchText는 recoil로 관리를 하고 있습니다.

main.jsx 가 리-렌더링이 되어 kakaomap.jsx가 리-렌더링이 된다고 하여도 searchText는 변하지 않았으니 useEffect는 실행이 안되어야 하는것이 아닌가요?

문제 재검토

KakaoMap 함수 컴포넌트 내부에 Main 함수 컴포넌트가 포함되는 것이 문제 원인입니다. 이는 명백히 잘못된 작성 방법입니다. 컴포넌트 내부에 컴포넌트가 정의된 경우, 컴포넌트 리-렌더링 과정에서 내부에 포함된 컴포넌트는 다시 정의 되므로 마운트 과정을 다시 거치게 됩니다. 즉, useEffect는 필연적으로 실행됩니다.

그러므로 아래와 같이 컴포넌트를 중첩하지 않고 분리해 구성하는 올바른 방법을 사용해야 합니다.

KakaoMap.jsx ```jsx /* KakaoMap ----------------------------------------------------------------- */ function KakaoMap(props) { const [state, setState] = useState({ center: { lat: 36.013434, lng: 129.349478, }, isPanto: false, errMsg: null, isLoading: true, isBottomSheetOpen: false, currentMarker: null, }); return
; } /* Main --------------------------------------------------------------------- */ const Main = ({ state, setState }) => { const [draggable, setDraggable] = useState(true); const [locationData, setLocationData] = useState([]); const searchText = useRecoilValue(searchTextState); const [listState, setListState] = useRecoilState(parkinglotListState); useEffect(() => { searchPlace(searchText); ParkingFeeMarker; console.log('kakaomap.jsx 의 useEffect'); }, [searchText]); const positions = []; locationData.forEach((obj) => { positions.push({ title: obj.prkplceNm, latlng: { lat: obj.latitude, lng: obj.longitude }, fee: obj.basicCharge, basicTime: obj.basicTime, prkplceNo: obj.prkplceNo, prkplceSe: obj.prkplceSe, }); }); const mapRef = useRef(); const ParkingFeeMarker = (props) => { const handlingClickOverlay = () => { props.onClick(); }; return (
{+props.fee === 0 ? '무료' : props.fee}
); }; const zoomIn = () => { const map = mapRef.current; map.setLevel(map.getLevel() - 1); }; const zoomOut = () => { const map = mapRef.current; map.setLevel(map.getLevel() + 1); }; const moveToCurrentLocation = () => { if (navigator.geolocation) { // GeoLocation을 이용해서 접속 위치를 얻어옵니다 navigator.geolocation.getCurrentPosition( (position) => { const currentLat = position.coords.latitude; const currentLng = position.coords.longitude; // 현재 위치로 지도 중심 이동 const map = mapRef.current; map.setCenter(new window.kakao.maps.LatLng(currentLat, currentLng)); }, (err) => { setState((prev) => ({ ...prev, errMsg: err.message, isLoading: false, })); } ); } else { // HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다 setState((prev) => ({ ...prev, errMsg: 'geolocation을 사용할수 없어요..', isLoading: false, })); } }; function searchPlace(keyword = '서울 광화문') { const places = new window.kakao.maps.services.Places(); const callback = function (result, status) { const map = mapRef.current; if (status === kakao.maps.services.Status.OK) { const currentLat = Number(result[0].y); const currentLng = Number(result[0].x); SearchAreaScope(currentLat, currentLng).then((res) => { setListState(res); setLocationData(res); }); map.setCenter(new window.kakao.maps.LatLng(currentLat, currentLng)); } }; places.keywordSearch(keyword, callback); } return ( <>
› {positions.map((position, index) => ( {}} > { if (state.currentMarker && state.currentMarker.prkplceNo === position.prkplceNo) { // 현재 클릭된 마커와 같은 마커를 클릭하면 바텀시트 닫기 setState((prev) => ({ ...prev, isBottomSheetOpen: false, currentMarker: null, })); } else { setState((prev) => ({ ...prev, center: { lat: position.latlng.lat, lng: position.latlng.lng }, isPanto: true, isBottomSheetOpen: true, currentMarker: position, selectedParkingLot: { title: position.title, fee: position.fee, basicTime: position.basicTime, prkplceNo: position.prkplceNo, prkplceSe: position.prkplceSe, }, })); } }} /> ))}
확대 축소
{state.isBottomSheetOpen && ( )} ); }; export default KakaoMap; ```


아래 영상은 React.memo를 사용하지 않은 코드로, 제대로 컴포넌트를 작성할 경우 useEffect 콜백이 발생하지 않는 것을 보여 줍니다.

choinamechoi commented 1 year ago

와..

감사합니다 이런 문제가 있었군요!!!