리액트 팝업 깜빡거림 현상 개선하기

김진영·2023년 5월 8일
0

개인 프로젝트

목록 보기
7/7
post-thumbnail

Mapbox 지도 API를 활용해 개인 프로젝트를 진행했습니다.
지도 상 특정 위치에 핀을 꽂고 해당 위치 정보를 활용해 애플리케이션 아이템을 구현하는 과정은 몹시 즐거웠습니다.
하지만 무엇보다 프로젝트를 마치고 가장 기억에 남는 점은 팝업이 나타나면서 깜빡거리는 현상을 개선한 트러블슈팅 과정입니다. 🚀
문제가 발생하는 과정을 논리적으로 정리하고 어느 부분을 손 봐야 하는지 진단하는 과정이 인상적이었기 때문입니다.

🚧 이슈

지도 상 특정 위치를 더블 클릭하면 해당 위치에 대한 정보를 입력하는 팝업이 나타납니다.
팝업이 정상적으로 나타났으면 좋았겠지만... 깜빡거리면서 등장하는 문제가 발생했습니다. 😭
이는 유저 경험에 치명적일 수 있기 때문에 문제 해결을 위해 고민하는 시간을 가졌습니다.

문제 정의

팝업이 깜빡이면서 등장하는 문제를 해결하기 위해 우선 문제 상황을 정의했습니다.
문제 상황을 아래와 같이 3가지 단계로 정리했습니다.

  1. 화면을 더블 클릭
  2. 팝업이 나타났다가 바로 사라지는 듯한 현상이 발생
  3. 팝업이 화면 중앙에 다시 등장

코드 리뷰

값 정리

문제 상황을 구체적으로 살펴보기 위해 코드 상 4가지 값에 주목했습니다.

  • 첫째, 새로운 좌표 정보를 나타내는 newCoordinate 상태값
  • 둘째, 지도를 더블 클릭하면 동작하는 핸들러 함수
  • 셋째, newCoordinate 상태값이 업데이트 되면 호출되는 useEffect
  • 넷째, newCoordinate 값 여부에 따라 발생하는 조건부 렌더링
function App() {
  // 지도 뷰 상태 정보
  const [viewState, setViewState] = useState<ViewState | null>({
    bearing: 0,
    longitude: 2.294694,
    latitude: 48.858093,
    padding: {
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
    },
    pitch: 0,
    zoom: 4,
  });
  
  ... 💻 MORE STATE ...
  
  // ✅ 새로운 좌표 정보
  const [newCoordinate, setNewCoordinate] = useState<NewCoordinate | null>(
    null
  );

  // ✅ 지도 더블 클릭 이벤트 핸들러
  const handleMapDbClick = useCallback(
    (event: mapboxgl.MapLayerMouseEvent) => {
      event.preventDefault();
      if (!currentUser) {
        signinAlertRef.current?.showModal();
      } else {
        const { lng, lat } = event.lngLat;
        setCurrentPin(null);
        setNewCoordinate({ lng, lat });
      }
    },
    [currentUser]
  );
  
  // ✅ 새로운 좌표의 상태값(newCoordinate )이 업데이트 되면 실행되는 useEFfect 훅
  useEffect(() => {
    if (!newCoordinate) {
      return;
    } else {
      setViewState(prev => ({
        ...prev!,
        longitude: newCoordinate.lng,
        latitude: newCoordinate.lat,
      }));
    }
  }, [newCoordinate]);

  return (
    <Map
      {...viewState}
      cursor="pointer"
      style={{ width: "100vw", height: "100vh" }}
      mapStyle="mapbox://styles/mapbox/streets-v9"
      onMove={event => setViewState(event.viewState)}
      onDblClick={handleMapDbClick}
      mapboxAccessToken={process.env.REACT_APP_MAPBOX_ACCESS_TOKEN}
    >
    ... 💻 MORE CODE ...
    
      // ✅ 새로운 좌표의 상태값(newCoordinate) 여부에 따라 조건부 렌더링
      {newCoordinate && (
        <NewPinPlaceInfo
          currentUser={currentUser}
          newCoordinate={newCoordinate}
          handlePins={setPins}
          handleNewCoordinate={setNewCoordinate}
        />
      )}
      
    ... 💻 MORE CODE ...
    </Map>
  );
}

export default App;

과정 정리

다음으로는 4가지 값이 상호작용하는 과정을 구체적으로 살펴봤습니다.
결과적으로, 문제의 원인은 업데이트 되는 좌표(newCoordinate)의 상태값이었습니다.

이제 문제가 되는 로직을 구체적으로 살펴보겠습니다.

  1. 더블 클릭하면서 더블 클릭 이벤트 핸들러(handleMapDbClick)가 호출됩니다.
    • 좌표의 (newCoordinate) 상태값이 업데이트 됩니다.
  2. 컴포넌트의 상태가 업데이트 됐기 때문에 해당 컴포넌트가 리렌더링 됩니다.
    • 함수형 컴포넌트의 리턴문도 다시 실행됩니다.
    • 더블 클릭한 위치에 팝업(NewPinPlaceInfo)이 나타납니다.
      • 리턴문 내 'newCoordinate &&' 이후 코드
    • ⭐️ 아직 '지도 상 더블 클릭한 위치'가 화면 중앙으로 이동하지 않은 상태입니다.
  3. 컴포넌트 렌더링이 완료된 이후에 newCoordinate이 업데이트 됐기 때문에 useEffect 훅이 호출됩니다.
    • viewState 값이 업데이트 되면서 컴포넌트가 다시 한번 리렌더링 됩니다.
    • ⭐️ 이번에는 '지도 상 더블 클릭한 위치'가 화면 중앙으로 이동한 상태입니다.

컴포넌트가 리렌더링 되면서, 즉 팝업이 화면에 두 차례 등장하면서 팝업이 깜빡이는 것이었습니다.

👷🏻‍♂️ 트러블슈팅

How?

문제를 해결하기 위해 새로운 상태값 showNewPin을 추가했습니다.

const [showNewPin, setShowNewPin] = useState(false);

새롭게 추가한 상태값은 조건부 렌더링을 위해 사용됩니다.
수정된 조건부 렌더링 방식에 의해 팝업은 화면에 한 번만 등장합니다.
(팝업을 보여주기 위해서는 showNewPin 값을 true로 바꿔야 합니다.)

새로운 코드

기존 코드에서 아래 2가지 부분을 수정했습니다.

  • 새로운 상태(showNewPin) 추가
  • return 문 조건부 렌더링 코드 추가
function App() {
  // 새로운 핀 좌표 정보
  const [newCoordinate, setNewCoordinate] = useState<NewCoordinate | null>(
    null
  );
  // 🟠 새롭게 추가한 상태
  const [showNewPin, setShowNewPin] = useState(false);

  ... 💻 MORE CODE ...

  const handleMapDbClick = useCallback(
    (event: mapboxgl.MapLayerMouseEvent) => {
      event.preventDefault();
      if (!currentUser) {
        signinAlertRef.current?.showModal();
      } else {
        const { lng, lat } = event.lngLat;
        setCurrentPin(null);
        setNewCoordinate({ lng, lat });
      }
    },
    [currentUser]
  );
  
  ... 💻 MORE CODE ...

  useEffect(() => {
    if (!newCoordinate) {
      return;
    } else {
      setShowNewPin(true);
      setViewState(prev => ({
        ...prev!,
        longitude: newCoordinate.lng,
        latitude: newCoordinate.lat,
      }));
    }
  }, [newCoordinate]);

  return (
    <Map
      {...viewState}
      cursor="pointer"
      style={{ width: "100vw", height: "100vh" }}
      mapStyle="mapbox://styles/mapbox/streets-v9"
      onMove={event => setViewState(event.viewState)}
      onDblClick={handleMapDbClick}
      mapboxAccessToken={process.env.REACT_APP_MAPBOX_ACCESS_TOKEN}
    >
      ... 💻 MORE CODE ...

      // 🟠 수정한 부분
      {newCoordinate && showNewPin && (
        <NewPinPlaceInfo
          currentUser={currentUser}
          newCoordinate={newCoordinate}
          handlePins={setPins}
          handleNewCoordinate={setNewCoordinate}
          handleShowNewPin={setShowNewPin}
        />
      )}
      
      ... 💻 MORE CODE ...
    </Map>
  );
}

export default App;

과정 정리

새로운 부분을 추가하면서 기존의 과정을 수정했습니다.
1번 과정까지는 이전과 동일합니다.

  1. 더블 클릭하면서 더블 클릭 이벤트 핸들러(handleMapDbClick)가 호출됩니다.
    • 좌표의 (newCoordinate) 상태값이 업데이트 됩니다.
  2. 컴포넌트의 상태가 업데이트 됐기 때문에 해당 컴포넌트가 리렌더링 됩니다.
    • 함수형 컴포넌트의 리턴문도 다시 실행됩니다.
    • ⭐️ 더블 클릭한 위치에 팝업(NewPinPlaceInfo)이 나타나지 않습니다.
      • 추가한 상태값을 '조건부 렌더링'의 조건값으로 추가했기 때문입니다.
      • 현재까지 showNewPin의 상태값은 false입니다.
  3. 컴포넌트 렌더링이 완료된 이후에 newCoordinate이 업데이트 됐기 때문에 useEffect 훅이 호출됩니다.
    • viewState 상태값을 업데이트 합니다.
    • 이때 showNewPin 상태값도 true로 업데이트 합니다.
    • 결과적으로 batch update가 발생하면서 컴포넌트가 리렌더링 됩니다.

showNewPin 상태를 추가하면서 팝업을 화면에 한 번만 렌더링 되게 로직을 수정했습니다.

🗺 결과

팝업이 등장하면서 더이상 깜빡이지 않는 것을 확인할 수 있습니다!! 👏🏻👏🏻🎉
문제를 해결하는 과정이 깔끔하고 신속했습니다.
이번의 경험을 잊지 않고 추후 발생하는 트러블슈팅 과정에서 참고해야겠습니다. 😆

추가로적으로, 해당 이슈를 해결하면서
1) useEffect 훅의 동작 방식을 다시 한번 자세하게 살펴보는,
2) useState 대신 useRef를 활용하는 등 애플리케이션 성능 개선에 대해 고민하는
의미있는 시간 또한 가질 수 있었습니다.

profile
기록해서 남길래요

0개의 댓글