[TIL] 내일배움캠프 React 과정 2023.01.27

Today I Learned

  • vercel로 프로젝트 배포하기
  • custom hook 공부하기

 


isOpen 데이터를 전역적으로 사용하기

// 현재 시간, 요일에 따라 영업 중, 영업 종료 구분
const handleIsOpen = () => {
  // 현재 시간, 현재 요일
  const { currentTime, day } = getCurrentTime();

  // 주말인 경우
  if (day === 0 || day === 7) {
    return currentTime >= weekendOpenTime && currentTime <= weekendCloseTime;

    // 평일인 경우
  } else {
    return currentTime >= weekdayOpenTime && currentTime <= weekdayCloseTime;
  }
};

handleIsOpen이라는 함수를 실행하면 현재 시간과 요일을 이용해서 서점이 현재 영업 중인지를 boolean으로 출력하고, 이 값을 각 서점마다 isOpen이라는 키에 담고 싶었는데 이걸 어떻게 하면 전역적으로 사용할 수 있을지 고민을 많이 했다.

 

처음에 했던 생각은 handleIsOpen 함수를 실행할 때마다 setState로 isOpen에 boolean을 넣는 것이었는데 map 안에서 각 서점마다 setState가 다르게 되지 않고 결과가 전부 하나로 통일되어 버렸다. 

 

그래서 다른 방법이 뭐가 있을까... 고민하다가 isOpen을 전역 데이터로 만드는 게 아니라 isOpen을 전역 데이터에 넣어버리자는 아이디어가 떠올랐다. 처음에는 useEffect로 서점의 운영 정보가 담긴 DB라는 전역 state에 isOpen을 넣는 방법을 생각했는데 이렇게 하면 useEffect가 실행돼서 isOpen 값이 담기기 전에는 isOpen이라는 요소가 존재하지 않아서 에러가 발생할까 봐 걱정이 되었다.

 

// 전역 DB
export const dbState: any = atom<IdbState[]>({
  key: 'dbState',
  default: data.map((item) => ({
    ...item,
    isOpen: handleIsOpen(),
  })),
});

내 고민을 얘기했더니 다른 팀원분이 isOpen을 아예 전역 state의 dafault 값으로 넣자는 아이디어를 주셔서, 서점의 운영 정보가 담긴 데이터를 전역으로 사용하기 위해 recoil로 만든 DB라는 전역 state에 default 값을 넣을 때 기존 서점 정보 데이터를 가공해서 isOpen까지 넣어버렸다. 

 

이렇게 하면 렌더링을 할 때마다 각 서점의 데이터마다 현재 시간에 따라 isOpen이 boolean으로 들어가기 때문에 이 요소를 이용해서 리스트나 마커 오버레이에서 현재 서점이 영업 중인지를 실시간으로 보여줄 수 있게 되었다.

 

 

custom hook 사용해 보기

이번에 처음으로 프로젝트가 여유롭게 마무리되었기 때문에 코드를 리팩터링 할 수 있는 시간이 있었다. 그래서 다른 팀원 분이 custom hook에 대해서 거의 강의를 해주셨는데 너무 배운 점이 많았다! 

 

나는 커스텀훅을 코드의 재사용성을 높이기 위해서만 사용한다고 생각했는데 코드의 가독성을 높이기 위해서도 사용할 수 있는 거였다. 코드가 200줄 넘어가면 구조를 파악하기가 어려운데 커스텀훅을 이용하면 컴포넌트 내부가 깔끔해지기 때문에 한눈에 이 컴포넌트의 역할과 로직을 파악하기 좋을 거 같다. 다만 너무 모든 것을 커스텀훅으로 만들려고 하지는 말고 커스텀훅이 필요한 경우를 잘 구분해야 할 거 같다.

 

// useMap.ts
import { useNavigate } from 'react-router-dom';
import { useCallback, useState } from 'react';

const { kakao } = window;

export const useMap = (
  mapContainer: any,
  setMarkerImage: any,
  markerImage: any,
  DB: any,
) => {
  const navigate = useNavigate();

  // map 객체를 저장할 state
  const [map, setMap] = useState<any>(null);
  const [markers, setMarkers] = useState<any[]>([]);

  // * 지도를 생성하는 함수
  const makeMap = useCallback(() => {
    // 지도를 생성할 때 필요한 기본 옵션
    let options = {
      center: new kakao.maps.LatLng(37.56839464, 126.9303023), // 지도의 중심 좌표
      level: 10, // 지도의 확대 수준
    };

    // 지도를 표시할 div와 지도 옵션으로 지도를 생성함
    const newMap = new kakao.maps.Map(mapContainer.current, options);

    // 지도 확대 축소를 제어할 수 있는 줌 컨트롤 생성
    const zoomControl = new kakao.maps.ZoomControl();
    newMap.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);

    // 마커 이미지 생성
    const imageSrc = require('../assets/images/marker.png');
    const imageSize = new kakao.maps.Size(28, 28);
    setMarkerImage(new kakao.maps.MarkerImage(imageSrc, imageSize));

    setMap(newMap);
  }, [mapContainer, setMarkerImage]);

  // * 마커를 생성하는 함수
  const makeMarkers = useCallback(() => {
    if (!markerImage) return;

    // 기존 마커 제거
    if (markers.length > 0) {
      markers.forEach((marker: any) => marker.setMap(null));
    }

    // 마커 표시하기
    const newMarkers: any[] = [];
    DB.forEach((store: IdbState) => {
      const marker = new kakao.maps.Marker({
        map: map, // 마커를 표시할 지도
        title: store.FCLTY_NM, // 마커의 타이틀, 마커에 마우스를 올리면 타이틀이 표시됨
        position: new kakao.maps.LatLng(store.FCLTY_LA, store.FCLTY_LO), // 마커를 표시할 위치(위도, 경도)
        image: markerImage, // 커스텀 마커 이미지 설정
        id: store.ESNTL_ID, // 마커에 ESNTL_ID를 id로 설정
      });

      // 마커 클릭시 해당 bookstoreId로 라우터 이동
      kakao.maps.event.addListener(marker, 'click', () =>
        navigate(`/map/${store.ESNTL_ID}`),
      );

      // 마커를 배열에 저장
      newMarkers.push(marker);
    });

    // 마커 배열을 state에 저장
    setMarkers(newMarkers);

    // eslint-disable-next-line
  }, [DB, map, markerImage]);

  return { makeMap, makeMarkers, map };
};
// useMap 사용하기
const { makeMap, makeMarkers, map } = useMap(
    mapContainer,
    setMarkerImage,
    markerImage,
    DB,
);

원래 페이지를 구성하는 컴포넌트 안에 있던 함수 두 개를 커스텀훅으로 뺐더니 이 함수 부분의 코드를 77줄->6줄로 줄일 수 있었다. 참고로 어떤 요소를 파라미터를 받고 어떤 것들을 return으로 내보낼지 헷갈릴 수 있기 때문에 신경 써서 코드를 리팩토링해야 한다!


회고

프로젝트 기능 구현과 배포까지 오늘 다 끝났기 때문에 주말 동안에는 발표 자료 정리랑 발표 준비도 하고, 코드도 좀 자잘하게 수정하려고 한다.

 

오늘 내배캠이 바꾸는 시간이라는 행사가 있었는데 연사 분들의 진솔한 이야기를 듣고 동기부여할 수 있는 시간이 되었다. 그중에서 SELF UNDERSTANDING이라는 단어가 기억에 남는데 실제로 내배캠은 취업이라는 목표에서 벗어나더라도 나를 알아갈 수 있는 좋은 기회였다. 

 

지금까지 나는 집에 있는 걸 좋아하고, 매일 만나던 친구들만 만나고, 새로운 사람들을 만나는 데 전혀 관심이 없었기 때문에 좀 정체되어 있었던 거 같다. 근데 캠프에 참가하며 정말 다양한 사람들을 만나고 점점 적응해 가면서 내가 몰랐던 나를 이해할 수 있는 시간이 되었다.

 

예를 들면 내가 자기주장이 강하고, 꽤나 솔직한 사람이라는 것. 사람들과 협업하고 대화도 많이 나누면서 깨달았는데 이걸 좀 고쳐야겠다는 생각도 했다 ㅋㅋ 한 가지에 꽂히면 시야가 좁아지는 경우가 있기 때문에 잠시 멈추고 다른 사람들의 의견을 경청할 줄 알아야 하며, 솔직하다는 이유로 속에 있는 말을 전부 다 내뱉다 보면 다른 사람들에게 상처를 줄 수도 있겠다는 생각이 들었다. 

 

아무튼... 성찰의 시간을 좀 가졌는데 결론은 좋은 동기들과 교류하고 내 할 일 열심히 하면서 많이 성장하고 싶다는 것이다. 아마 내 인생에서 이렇게 열정적으로 바쁘게 살아본 적은 없는 거 같다. 한때는 내가 열정이 전혀 없는 사람 같다는 생각도 했었는데 사실은 그 열정을 발휘할 대상이 없었던 거 같기도...