4월 한 달 동안 베타 프로젝트를 세팅하고 배포까지 마무리했다.
✏️ 프로젝트 요구사항
- 웹 기반 서비스
- 웹 UI 내에서 카메라를 통해 사진 촬영
- 카카오 맵이나 구글맵이 아닌 자체 제작한 지도 이미지 위에 핀으로 위치를 표시
- 특정 위치에 대한 리뷰 작성 및 좋아요 기능
- 사용자 활동 분석을 위한 Mixpanel 연동
🤔 웹에서 카메라 촬영이 된다고?
처음에는 웹 UI안에서 카메라 촬영을 할 수 있는 건가 했다..?
찾아보니 가능했고 나는 react-webcam을 활용해서 구현했다.
이 라이브러리는 내부적으로 <video> 태그를 사용해 카메라 화면을 실시간으로 보여준다.
구체적으로는 브라우저가 제공하는 표준 Web API인 navigator.mediaDevices.getUserMedia()를 호출해서 사용자의 카메라 스트림(stream)을 가져온다. 그리고 이 stream을 video 태그의 srcObject에 할당하면 브라우저는 video 태그를 통해 실시간으로 카메라 화면을 사용자에게 보여줄 수 있게 되는 것이다.
⚠️ 주의할 점
localhost 또는 https에서만 작동한다. (즉, http에서는 카메라 접근 자체가 안된다!!)
이러한 제한은 getUserMedia()는 사용자의 민감한 장치에 접근하기 때문에 브라우저가 보안이 확보된 환경에서만 동작을 허용한다고 한다.
그래서 http로 접근한 경우 https로 자동 리다이렉트 되도록 CloudFront에서 설정해 줬다.
defaultBehavior: {
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
...
}
🗺️ react-zoom-pan-pinch로 지도 만들기
내가 구현해야 했던 요구사항은 아래와 같았다.
- 줌인 / 줌아웃
- 지도 위에 핀을 꽂고 이때 핀은 확대/축소될 때 크기가 변하지 않아야 함
- 핀을 클릭하면 특정 위치로 줌인되고 자동으로 포커스 되어야 함
이 요구사항을 만족하는 라이브러리로 react-zoom-pan-pinch가 가장 적합했다.
다만 한 가지 이슈가 있었는데,
초기 로딩 시 핀의 크기가 과하게 크게 보이는 문제가 있었다.
이는 transform 값이 적용되기 전, 스타일 초기화가 제대로 되지 않아서 생긴 현상이었다.
직접 수정하고 PR도 올렸는데 아직 승인은 안 됐다 🥲
나는 이 이슈를 해결하기 위해 KeepScale 컴포넌트 부분만 따로 만들어서 사용했다.
import React, { useEffect, useRef } from "react";
import { useTransformContext } from "react-zoom-pan-pinch";
import { mergeRefs } from "../_utils/mergeRefs";
export const KeepScale = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const localRef = useRef<HTMLDivElement>(null);
const context = useTransformContext();
useEffect(() => {
const applyScale = (scale: number) => {
if (localRef.current) {
localRef.current.style.transform = context.handleTransformStyles(
0,
0,
1 / scale
);
}
};
requestAnimationFrame(() => {
applyScale(context.transformState.scale);
});
return context.onChange((ctx) => {
applyScale(ctx.instance.transformState.scale);
});
}, [context]);
return <div {...props} ref={mergeRefs([localRef, ref])} />;
});
🗂️ Parallel Routes
바텀 시트 관리는 parallel routes를 활용해서 구현했다.
아래와 같이 바텀시트의 상태를 관리하는 경우 한계가 있다.
const [isOpen, setIsOpen] = useState(false);
- 새로고침 시 바텀시트가 닫히며 상태가 초기화 됨
- 모달/바텀시트 상태관리를 위한 별도의 state가 필요해 복잡함
- 브라우저 뒤로 가기/앞으로 가기로 이전 상태를 복원할 수 없음
물론 query params에 직접 모달 상태를 추가해서 상태관리를 할 수도 있다.
그러면 새로고침 시 바텀시트 상태가 유지되고 별도의 state가 필요 없으며 브라우저 히스토리도 유지할 수 있다.
하지만 이 query params을 관리해야 하므로 코드의 복잡도가 높아질 수 있다.
Parallel routes를 사용하면 아래와 같은 장점이 있다!
- 모달/바텀 시트 상태관리를 위한 state가 필요 없음
- url에 바텀시트가 상태가 유지되어 새로고침을 해도 상태 유지
- 브라우저 히스토리와 연동되므로 뒤로 가기/앞으로 가기로 상태 복원이 가능
- 메인 페이지와 독립적으로 라우팅과 렌더링 되어 유지보수가 쉬움
- 중첩된 모달/바텀시트의 경우 관리가 용이함
- 필요한 부분(변경된 slot)에 대해서만 리렌더링이 발생함
File-system conventions: Parallel Routes | Next.js
Simultaneously render one or more pages in the same view that can be navigated independently. A pattern for highly dynamic applications.
nextjs.org
🎼 vaul (bottomsheet 라이브러리)
바텀 시트를 직접 구현할까 하다가 시간 제약이 있어 라이브러리를 활용했다.
라이브러리를 쓰면서 버그가 있길래 수정해서 PR도 올렸다(PR에 대한 승인은 되었는데 merge는 아직이다...!)
🚀 배포
Next.js를 output: 'export'로 정적 사이트로 빌드하고, AWS CDK를 사용해 정적 웹사이트로 배포했다.
전체적인 배포 과정은 아래와 같다.
- 정적 빌드 생성
- Next.js를
output: 'export'모드로 설정한 후next build로 정적 파일을/out디렉토리에 빌드한다.
- Next.js를
cdk deploy로 S3 버킷, CloudFront, 도메인 레코드, 인증서 배포- S3: 정적 파일을 저장할 버킷 생성
- CloudFront: S3를 연결한 CDN 생성
HTTP 요청은 자동으로 HTTPS로 리다이렉트 되도록 설정했다. 브라우저에서 카메라 API 같은 기능이 HTTPS 환경에서만 동작하기 때문이다. - Route 53: 도메인을 연결해 사용자 요청을 CloudFront로 라우팅
- ACM: HTTPS를 위한 인증서 연결