1. 개요
- 내용: 이번 프로젝트의 목적과 주요 기능(인증, 데이터 조회, 반응형 UI)을 간단히 소개한다
2. 기술 스택 및 초기 설정
- 내용: Vite, React, TypeScript, TanStack Query, Axios를 사용
- 코드: 8000번 포트의 백엔드 서버와 통신하기 위한 베이스 URL 설정
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'http://localhost:8000/v1',
headers: {
'Content-Type': 'application/json',
},
});
axiosInstance.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default axiosInstance;
3. TanStack Query를 활용한 효율적인 데이터 관리
- 내용: useQuery를 사용하여 서버 상태를 관리하고, 특히 정렬(최신순/오래된순) 기능을 처리
- queryKey에 sortOrder를 넣어 정렬이 바뀔 때마다 자동으로 패칭되는 로직
import { useQuery } from '@tanstack/react-query';
import client from '../api/client'; //
const fetchLPs = async (order: 'asc' | 'desc') => {
const { data } = await client.get(`/lps?order=${order}`);
return data.data.data;
};
export const useLPs = (sortOrder: 'asc' | 'desc') => {
return useQuery({
queryKey: ['lps', sortOrder],
queryFn: () => fetchLPs(sortOrder),
staleTime: 1000 * 60 * 5,
});
};
export const useLPDetail = (lpId: string) => {
return useQuery({
queryKey: ['lp', lpId],
queryFn: async () => {
const { data } = await client.get(`/lps/${lpId}`);
return data.data;
},
enabled: !!lpId,
});
};
4. 사용자 인터페이스(UI) 및 인터랙션 구현
- 내용: 목록 페이지의 격자 레이아웃과 카드의 호버 효과 구현
- 마우스를 올렸을 때 사진이 확대되고 검은 오버레이 위에 정보(제목, 업로드일, 좋아요)가 뜬다
import { useQuery } from '@tanstack/react-query';
import client from '../api/client'; //
const fetchLPs = async (order: 'asc' | 'desc') => {
const { data } = await client.get(`/lps?order=${order}`);
return data.data.data;
};
export const useLPs = (sortOrder: 'asc' | 'desc') => {
return useQuery({
queryKey: ['lps', sortOrder],
queryFn: () => fetchLPs(sortOrder),
staleTime: 1000 * 60 * 5,
});
};
export const useLPDetail = (lpId: string) => {
return useQuery({
queryKey: ['lp', lpId],
queryFn: async () => {
const { data } = await client.get(`/lps/${lpId}`);
return data.data;
},
enabled: !!lpId,
});
};
5. 반응형 레이아웃 및 내비게이션
- 내용: 창 크기에 따라 변하는 사이드바와 플로팅 버튼 라우팅을 다뤘다
6. 상세 페이지 및 보안(보호 라우트)
- 내용: 특정 LP의 상세 정보를 보여주고, 비로그인 사용자의 접근을 차단하는 로직
- useParams로 ID를 받고 useEffect로 토큰 존재 여부를 확인해 비로그인 사용자를 튕겨내는 '보호 라우트' 사용
7. 무한 스크롤(Infinite Scroll) 구현
- 내용: 대량의 데이터를 한 번에 가져오지 않고, 사용자의 스크롤에 맞춰 적절히 끊어 가져오는 기술
export const useInfiniteLPs = (sortOrder: 'asc' | 'desc') => {
return useInfiniteQuery({
queryKey: ['lps', 'infinite', sortOrder],
queryFn: async ({ pageParam = 0 }) => {
const { data } = await client.get(`/lps`, {
params: { order: sortOrder, cursor: pageParam },
});
return data.data;
},
getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
initialPageParam: 0,
});
};
- useInfiniteQuery를 사용해 getNextPageParam으로 서버의 cursor 값을 처리하는 로직은 무한 스크롤의 핵심
const ListPage = () => {
const [sortOrder, setSortOrder] = useState<'desc' | 'asc'>('desc');
const { ref, inView } = useInView();
const {
data,
isLoading,
isError,
error,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteLPs(sortOrder);
- react-intersection-observer를 사용하여 스크롤 하단 감지 시 fetchNextPage를 호출하는 트리거 로직
8. 지루함 없는 로딩: Shimmer 애니메이션 스켈레톤 UI
- 내용: 단순한 로딩 스피너 대신, 실제 콘텐츠와 유사한 형태의 스켈레톤을 노출하여 심리적 대기 시간을 줄인 과정
- opacity가 부드럽게 변하는 애니메이션 코드를 통해 디테일한 UI 구현
9. 상세 페이지 고도화: 댓글 시스템
- 내용: 단순 정보 조회를 넘어, 사용자와 소통하는 댓글창을 무한 스크롤로 구현하고 가독성을 높인 레이아웃
- 고양이 사진과 메타 정보가 상단에 배치되고, 그 아래로 댓글창과 댓글 목록이 자연스럽게 이어지는 개선된 가독성
const DetailPage = () => {
const { lpid } = useParams();
const navigate = useNavigate();
const [commentOrder, setCommentOrder] = useState<'desc' | 'asc'>('desc');
const [commentText, setCommentText] = useState('');
const { data: lp, isLoading: isLPLoading } = useLPDetail(lpid as string);
const {
data: commentData,
isLoading: isCommentLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteComments(lpid as string, commentOrder);
const { ref, inView } = useInView();
const accessToken = localStorage.getItem('accessToken');
useEffect(() => {
if (!accessToken) {
alert('로그인이 필요한 서비스입니다.');
navigate('/login');
}
}, [accessToken, navigate]);
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
if (isLPLoading) return <div style={{ padding: '50px', textAlign: 'center' }}>데이터 로딩 중...</div>;