Skip to content

[FE] 산책기능 (작성중)

Woody edited this page Sep 27, 2024 · 13 revisions

🗺 지도 구현하기

https://developer.mozilla.org/ko/docs/Web/API/Geolocation_API/Using_the_Geolocation_API

📱 터치 화면 스크롤

  • scroll-snap-type: x mandatory;

  • -webkit-overflow-scrolling: touch;

const StyledSlideWrapper = styled.div`
  margin-top: 1rem;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  width: 100%;
  height: 80%;
`;

✨ CSS Positioning: lefttransform

left 속성과 transform 속성을 사용하여 요소를 수평으로 중앙에 위치시키는 방법을 정리해보겠습니다.

시각적 예시

세 가지 다른 CSS 설정의 결과를 볼 수 있습니다.

  1. 파란 박스: left: 0
  2. 빨간 박스: left: 50%
  3. 초록 박스: left: 50%transform: translateX(-50%)

상세 설명

1. left: 0

  • 요소를 부모 컨테이너의 왼쪽 끝에 위치시킵니다.
  • 예상대로 작동하며, 요소의 왼쪽 가장자리가 부모의 왼쪽 끝과 일치합니다.

2. left: 50%

  • 요소의 왼쪽 가장자리를 부모 컨테이너의 중앙에 위치시킵니다.
  • 하지만 요소의 중심은 중앙선보다 오른쪽에 있게 됩니다. (이 부분을 캐치를 못하여 중앙에 위치시킨다고 생각했습니다.)

3. left: 50%transform: translateX(-50%)

  • 요소의 중심을 부모 컨테이너의 중앙선과 정확히 일치시킵니다.
  • left: 50%로 요소의 왼쪽 가장자리를 중앙에 위치시킨 후,
  • transform: translateX(-50%)로 요소를 왼쪽으로 자신의 너비의 절반만큼 이동시킵니다.

코드 예시

.parent {
  position: relative;
  width: 300px;
  height: 200px;
}

.left-0 {
  position: absolute;
  left: 0;
}

.left-50-percent {
  position: absolute;
  left: 50%;
}

.centered {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

transform이 필요한가?

left 속성만으로는 요소의 크기를 고려하지 않습니다. transform: translateX(-50%)를 추가로 사용하면 요소의 크기에 관계없이 정확한 중앙 정렬을 할 수 있습니다.

성능 고려사항

  • transform은 GPU 가속을 받아 성능상 이점이 있습니다.
  • position: absoluteleft를 사용하여 대략적인 위치를 잡고, transform으로 미세 조정을 하는 방식이 효율적입니다.

이 방식을 사용하면 레이아웃 재계산(리플로우)을 최소화하면서 정확한 중앙 정렬을 달성할 수 있습니다.

⏱ 스톱워치 구현

스톱워치를 구현할 때 주의할 점은 React의 상태 업데이트와 비동기 로직의 상호작용입니다.
StrictMode로 인해 발생할 수 있는 문제도 고려해야 하며, 타이머의 중복 실행과 메모리 누수를 방지하는 코드 작성이 중요합니다

🚨 1. StrictMode로 인한 타이머 중복 실행 문제

stopwatch-issue

  • 문제 설명 React의 StrictMode는 개발 모드에서 컴포넌트를 두 번 렌더링함으로써 잠재적인 버그를 찾을 수 있게 해줍니다.
    하지만 타이머(setTimeout 또는 setInterval)를 사용하는 경우, 타이머가 두 번 설정되어 시간이 두 배로 빠르게 흐르는 문제가 발생했습니다.

  • 해결 방법

    • 타이머 중복 방지: useRef를 사용해 타이머 ID를 관리하고, 중복 설정을 방지하였습니다.
    • useCallback을 사용하여 함수 메모이제이션: 타이머를 설정하는 함수(startTimer 등)를 useCallback으로 감싸 재정의되지 않게 하여 useEffect가 함수의 최신 버전을 참조할 수 있게 하였습니다.
useEffect(() => {
  if (status === 'start' && !timeoutRef.current) {
    startTimer();
  } else if (status === 'pause') {
    stopTimer();
  } else if (status === 'stop') {
    stopTimer();
    setTime(0);
  }

  return () => stopTimer(); // 컴포넌트 언마운트 시 타이머 정리
}, [status]);

const startTimer = useCallback(() => {
  timeoutRef.current = window.setTimeout(() => {
    setTime(prev => prev + 1);
    startTimer();
  }, 1000);
}, []);

2. 상태 관리와 타이머 로직의 비동기성 문제

  • 문제 설명 타이머의 상태를 setTimeout이나 setInterval로 관리할 때, 상태 업데이트가 비동기로 이루어지기 때문에 의도치 않게 여러 타이머가 실행될 수 있습니다. 또한, 상태 업데이트가 즉시 반영되지 않아 시간이 두 배로 흐르거나 중지되지 않는 현상이 발생할 수 있습니다.

  • 해결 방법

    • 타이머 설정을 useRef로 관리: 타이머의 ID를 useRef로 관리하여 매 렌더링 시 타이머 상태를 유지할 수 있게 합니다.
    • 상태 변경 시 타이머를 즉시 정리: 타이머가 재설정되기 전에 clearTimeout 또는 clearInterval로 이전 타이머를 정리합니다. timeoutRef.current 도 null처리를 해줘야 재시작시 startTimer()를 호출할 수 있습니다.
useEffect(() => {
    if (status === 'start' && !timeoutRef.current) {
      startTimer();
      return;
    }

    if (status === 'pause') {
      stopTimer();
      timeoutRef.current = null;
      return;
    }

    if (status === 'stop') {
      stopTimer();
      setTime(0);
      timeoutRef.current = null;
    }

    return () => stopTimer();
  }, [status, startTimer, stopTimer]);

image startTimer, stopTimer를 의존성 배열에 추가해줘야합니다.


3. 시간 형식 변환 문제

  • 문제 설명 스톱워치에서 초 단위로 기록된 시간을 시, 분, 초 단위로 변환해야 할 때, 수동으로 이를 계산하는 로직이 번거로웠습니다.

  • 해결 방법

    • 헬퍼 함수로 시간 변환 로직을 분리: 시간 형식 변환을 헬퍼 함수로 분리하여 코드의 가독성을 높이고 재사용성을 증가시켰습니다.
export function getTimeFormatString(time: number): string {
  const hour = Math.floor(time / 3600).toString().padStart(2, '0');
  const min = Math.floor((time % 3600) / 60).toString().padStart(2, '0');
  const sec = (time % 60).toString().padStart(2, '0');

  return `${hour}:${min}:${sec}`;
}

4. 컴포넌트 언마운트 시 타이머 정리

  • 문제 설명 컴포넌트가 언마운트될 때, 타이머가 여전히 실행 중이라면 메모리 누수가 발생하거나 예기치 않은 동작이 일어날 수 있습니다.

  • 해결 방법

    • useEffect에서 반환 함수로 타이머 정리: useEffect에서 반환되는 함수로 타이머를 명시적으로 정리하여 컴포넌트가 언마운트될 때 타이머가 중지되도록 하였습니다.
useEffect(() => {
  return () => stopTimer(); // 컴포넌트 언마운트 시 타이머 정리
}, []);

📍 커스텀 오버레이 적용하기

참조 공식 문서

🚨 닫기 버튼이 제대로 실행되지 않을 때

image

기존 방식 (HTML 문자열 방식에서의 문제점):

기존 코드에서 closeOverlay 함수는 오버레이의 content에 포함된 HTML 문자열 내에서 onclick 속성으로 호출되었습니다.

const customContents = `
  <div class="wrap">
    <div class="close" onclick="closeOverlay()" title="닫기">
      <img src=${closeIcon}>
    </div>
  </div>`;
  • 문제:

    • 이 방식에서 closeOverlay 함수는 HTML 문자열 내에서 호출되는데, 브라우저는 HTML 문자열로부터 해당 함수가 정의된 위치를 바로 찾지 못합니다. 그 이유는 문자열로 작성된 함수는 전역 스코프에서 접근 가능해야 하기 때문입니다.
    • 그러나 closeOverlayinitMap 함수 안에 지역적으로 정의된 함수였으므로, HTML 문자열 내에서 onclick으로는 함수에 접근할 수 없고, ReferenceError: closeOverlay is not defined 에러가 발생했습니다.
  • 문제 요약:

    • HTML 문자열 방식으로 함수를 사용하려면 전역 스코프에 함수를 선언해야 합니다. 하지만, 이는 네임스페이스 오염과 다른 함수들과의 충돌 가능성을 높일 수 있습니다.

수정한 방식 (DOM 요소 방식에서의 차이점):

수정된 코드에서는 closeOverlay를 HTML 문자열 내에서 직접 호출하지 않고, DOM 요소를 직접 생성한 후 이벤트 리스너를 추가했습니다.

const closeButton = document.createElement('img');
closeButton.src = closeIcon;
closeButton.style.cssText = 'width: 0.8rem; height: 0.8rem; margin-left: 0.2rem; cursor: pointer;';

// 닫기 버튼에 클릭 이벤트를 추가
closeButton.addEventListener('click', () => {
  overlay.setMap(null); // 이벤트 리스너로 함수 실행
});
  • 차이점:
    • HTML 문자열 내에서 onclick으로 직접 호출하지 않고, DOM 요소를 생성한 후 해당 요소에 이벤트 리스너를 등록했습니다.
    • 이 방식에서는 함수가 스코프 문제 없이 이벤트 리스너 내부에서 호출되므로, 함수 참조에 문제가 생기지 않습니다.

정리:

변경된 점함수 참조 방식입니다:

  1. 기존 방식에서는 HTML 문자열 내에서 onclick="closeOverlay()"처럼 함수 호출이 이루어졌습니다. 이때 전역 스코프에서 함수를 찾지 못해 에러가 발생했습니다.
  2. 수정된 방식에서는 DOM 요소를 생성한 후 addEventListener로 이벤트 리스너를 등록함으로써, 해당 함수가 스코프 내에서 정상적으로 호출될 수 있게 처리했습니다.

결론

  • 기존 방식은 전역 스코프에서 함수 참조 문제로 인해 closeOverlay가 호출되지 않았습니다.
  • 수정 방식은 DOM 요소에 직접 이벤트 리스너를 추가하여, 함수 참조와 스코프 문제를 해결하고 closeOverlay 함수가 정상적으로 실행되도록 처리한 방식입니다.

따라서 함수 참조 방식의 차이로 인해, closeOverlay 함수가 정상적으로 실행되었습니다.

closeOverlay실행 --- ## 📍 커스텀 오버레이 위치 정렬하기

🛠 커스텀 오버레이의 xAnchoryAnchor로 위치 정렬하기

**kakao.maps.CustomOverlay**에서 오버레이의 위치를 세밀하게 조정하려면 xAnchoryAnchor를 사용할 수 있습니다. 이 두 값은 오버레이의 기준점을 조정해, 오버레이가 마커나 지도 위에 어느 방향으로 배치될지 결정합니다.

1. xAnchoryAnchor의 기본 개념

  • xAnchor: 오버레이의 가로축 기준점입니다.

    • xAnchor: 0이면, 오버레이의 왼쪽 끝이 마커의 좌표와 일치합니다.
    • xAnchor: 0.5는 오버레이의 가운데가 마커와 일치하는 기본값입니다.
    • xAnchor: 1이면, 오버레이의 오른쪽 끝이 마커와 일치합니다.
  • yAnchor: 오버레이의 세로축 기준점입니다.

    • yAnchor: 0이면, 오버레이의 위쪽 끝이 마커의 좌표와 일치합니다.
    • yAnchor: 0.5는 오버레이의 가운데가 마커와 일치하는 기본값입니다.
    • yAnchor: 1이면, 오버레이의 아래쪽 끝이 마커의 좌표와 일치합니다.
    • yAnchor의 값이 1보다 크면 오버레이가 마커보다 더 위쪽에 위치하게 됩니다.

공식 문서에 따르면, xAnchoryAnchor의 값에 따라 오버레이가 마커를 기준으로 어느 방향에 배치될지 조정할 수 있습니다.

// 커스텀 오버레이를 생성합니다
var mapCustomOverlay = new kakao.maps.CustomOverlay({
    position: position,
    content: content,
    xAnchor: 0.5, // 커스텀 오버레이의 x축 위치입니다. 0.5가 기본값으로 가운데 정렬입니다.
    yAnchor: 1.1  // 커스텀 오버레이의 y축 위치입니다. 1.1이면 마커의 위쪽에 위치하게 됩니다.
});

3. 적용 결과

image

// 마커 이미지 생성
const imageSize = new kakao.maps.Size(65, 65);
const imageOption = { offset: new kakao.maps.Point(27, 69) };
const markerImage = new kakao.maps.MarkerImage(mapMarkerImage, imageSize, imageOption);

// 마커 위치
const markerPosition = new kakao.maps.LatLng(currentLocation[0], currentLocation[1]);

const newMarker = new kakao.maps.Marker({
  position: markerPosition,
  image: markerImage,
  map: mapInstance,
});

// 커스텀 오버레이 콘텐츠 생성
const customContents = `
  <div class="wrap" style="padding: 10px; background: white; border-radius: 10px;">
    <div class="info">
      <div class="title">커스텀 오버레이</div>
      <div class="desc">설명 텍스트</div>
    </div>
  </div>
`;

// 커스텀 오버레이 생성
const overlay = new kakao.maps.CustomOverlay({
            content: customContents,
            map: mapInstance,
            position: newMarker.getPosition(),
            xAnchor: 0,
            yAnchor: 2,
          });

// 마커 클릭 시 오버레이 표시
kakao.maps.event.addListener(newMarker, 'click', function () {
  overlay.setMap(mapInstance);
});

4. 커스텀 오버레이 위치 정렬 요약

image

  • xAnchor: 오버레이의 가로 기준점으로, 0일 때는 왼쪽 끝, 0.5는 중앙, 1은 오른쪽 끝이 기준점이 됩니다.
  • yAnchor: 오버레이의 세로 기준점으로, 0일 때는 위쪽 끝, 0.5는 중앙, 1은 아래쪽 끝이 기준점이 됩니다. 1 이상의 값은 마커 위로 더 이동합니다.
  • 목표: 마커 위에 오버레이를 띄우고 싶다면, yAnchor를 1보다 큰 값으로 설정하면 됩니다.

이미지 캡쳐하기

image image image image