SnapRoad는 혼자 혹은 원하는 그룹원들과 사용할 수 있는 여행 기록 서비스입니다.
Next와 React, TypeScript를 사용하여 개발하였으며 사진과 위치를 지정해 게시물을 등록하고 그룹 구성원들이 남긴 게시물에서 소통하고, 추억을 저장할 수 있는 서비스입니다!
Next JS
TypeScript
Zustand
Tanstack-query
Tailwind CSS
StoryBook
Supabase
Kakao Map API
Zod
React Hook Form
LightHouse
Sentery
각 mutate 대신 mutateAsync를 사용하여 비동기 뮤테이션 작업을 순차적으로 실행하도록 수정했습니다. mutateAsync는 Promise를 반환하므로 await를 사용해 각 뮤테이션이 완료될 때까지 기다릴 수 있습니다. 이렇게 하면 작업 순서가 보장되며, 비동기 작업이 의도대로 수행됩니다.
const submitPost = async () => {
try {
// 1. 포스트 생성
const postResult = await createPostMutation.mutateAsync(postData);
// 2. 포스트 생성 후, 이미지에 생성된 postId 업데이트
await updateImagesPostIdMutation.mutateAsync({
postId: postResult.data.post_id,
uploadSessionId,
});
// 3. 태그를 병렬로 저장
await Promise.all(
tags.map((tag) =>
saveTagsMutation.mutateAsync({ tag, postId: postResult.data.post_id, groupId })
)
);
// 작업이 성공적으로 끝난 후, 페이지 이동
router.push(`/group/${groupId}`);
} catch (error) {
console.error('폼 제출 중 오류 발생:', error);
}
};
카카오맵 api에서 클러스터 마커 스타일을 지원하는 방법
클러스터 시 마커의 사이즈가 10개 이하면 1번에서 설정한 스타일 배열의 0번째 스타일 적용 100개 이하면 1번째 스타일 적용하는 형식
// 마커 스타일 설정 방법
var styles = [{
width : '53px', height : '52px',
background: 'url(cluster_small.png) no-repeat',
color: '#fff',
textAlign: 'center',
lineHeight: '54px'
}, {
width : '73px', height : '72px',
background: 'url(cluster_large.png) no-repeat',
color: '#fff',
textAlign: 'center',
lineHeight: '74px'
}];
// 마커 스타일 지정 방법
clusterer.setStyles(styles);
// 클러스터 크기를 구분하는 값을 배열로 지정한다.
// 아래와 같이 구분값을 2개 지정하면 클러스터는
// 50보다 작은경우, 50보다 크거나 같고 100보다 작은경우, 100보다 크거나 같은경우, 이렇게 3개의 크기로 구분된다.
clusterer.setCalculator([ 50, 100 ]);
// 또는
// 클러스터 크기를 구분하는 값을 반환하는 함수를 지정할 수 있다.
// 함수 인자로는 클러스터가 포함하는 마커의 개수가 넘어온다.
// 반환값은 클러스터 사이즈 별 스타일 혹은 문구 배열의 인덱스 값이어야 한다.
clusterer.setCalculator(function( size ) {
var index;
// 클러스터에 포함된 마커의 개수가 50개 미만이면 리턴할 index값을 0으로 설정한다.
if ( size < 50 ) {
index = 0;
} else if ( size < 100 ) {
index = 1;
} else {
index = 2;
}
return index;
});
문제 분석
calculator
가 마커의 사이즈만 매개변수로 받아와서 적정 스타일을 각 클러스터 마커들에게 지정할 수 없는 문제
state
로 관리하기getBounds()
로 보여지는 맵의 영역 범위를 구하고 kakao.maps.LatLngBounds()
의 인자로 넣어 만든 viewport
를 만들고 이 viewport
에 클러스터 마커가 들어오면 마커 스타일 배열 state
에 클러스터 마커의 중심 좌표가 들어오면 그 스타일의 인덱스를 구해서 그 인덱스를 calculator
에 반환하여 클러스터 마커에 해당 클러스터 마커를 구성 마커의 이미지를 적용
CREATE OR REPLACE FUNCTION get_user_posts_by_user_id(input_user_id uuid)
RETURNS TABLE(
post_thumbnail_image text,
post_address text,
post_id uuid,
created_at timestamp with time zone,
group_id uuid
)
LANGUAGE plpgsql
AS $$
begin
return query
select p.post_thumbnail_image, p.post_address, p.post_id, p.created_at, p.group_id
from public.posts p
where p.group_id IN (
select ug.group_id
from public.user_group ug
where ug.user_id = input_user_id
)
order by random()
limit 5;
end;
$$;
async () => {
const { data } = await browserClient.auth.getUser();
let dataList: PostDataListType = [];
if (data.user?.id) {
const userId = data.user.id;
const { data: postDataList }: { data: PostDataListType } = await browserClient.rpc(
'get_user_posts_by_user_id',
{ input_user_id: userId },
);
if (postDataList?.length) {
const imgNameArray = postDataList.map((postData) => `${postData.group_id}/${postData.post_thumbnail_image}`);
const signedUrls = await getSignedImgUrls('tour_images', 60 * 60, imgNameArray);
if (signedUrls) {
dataList = postDataList.map((data, idx) => ({
...data,
post_thumbnail_image: signedUrls[idx].signedUrl,
}));
}
}
}
return dataList;
}
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
insert into public.profiles(
user_id,
user_email,
user_nickname
)values (
new.id,
new.email,
coalesce(new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'user_name',new.raw_user_meta_data->>'user_nickname')
);
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row
execute function public.handle_new_user();
LightHouse는 웹 성능 최적화, 접근성 확인, SEO 분석, PWA 지원 점검, 그리고 자동화된 문제 탐지를 통해 사용자 경험을 개선하고, 개발 중 발생할 수 있는 품질 문제를 효과적으로 진단 및 해결하기 위해서 사용하였습니다.
SnapRoad에서 SignedUrl을 사용한 이유
Storybook 도입기
카카오맵 api 클러스터 마커에 게시물 이미지 적용하기
Signed URL 과 캐싱