Next.js + Typescript + Firebase Stock SNS Service
테스트 계정
ID : [email protected]
PW : test1234!
구분 | 스택 & 라이브러리 |
---|---|
언어 | |
메인 라이브러리 | |
기타 라이브러리 | |
패키지 관리 | |
배포 |
기술 스택 선정 이유
기술 스택 | 선정 이유 |
---|---|
Next.js | - 각 피드의 게시물과 유저의 개인 프로필 페이지에 서버 사이드 렌더링(SSR)를 적용하여 초기 로딩 속도 개선과 검색 엔진 최적화(SEO)에 유리하게 하였습니다. - 프레임워크 안에 내장된 라우팅 시스템을 통해 프로젝트 구조화에 도움이 되었습니다. - Next/image, 코드 스플리팅, 프리페칭 등의 최적화 기능을 활용하여 성능을 향상시킬 수 있었습니다. |
TailwindCss | - 유틸리티 클래스 기반의 CSS 프레임워크로, 빠른 UI 개발과 일관된 디자인 시스템 구축에 효과적입니다. - 반응형 디자인을 위한 클래스를 제공하여 모바일 친화적인 개발이 가능했습니다. - CSS-in-JS 방식은 런타임에 스타일을 생성하고 적용하므로 서버 측 렌더링 시 추가 오버헤드가 발생할 수 있음. Tailwind CSS를 사용하면 별도의 JavaScript 런타임이 필요하지 않아 서버 부담을 줄일 수 있었습니다. |
Tanstack Query(React Query) | - 데이터 fetching, 캐싱, 동기화를 처리할 수 있어 파이어베이스와의 연동이 수월하였습니다. - useQuery나 useMutation과 같은 선언적이고 직관적인 API를 제공하여 코드 가독성과 유지보수성을 높일 수 있었습니다. - enabled라는 옵션을 사용하여 해당 인자가 존재하지 않는 경우에는 fetch를 실행하지 않도록 하여 불필요한 요청을 줄일 수 있었습니다. |
React Hook Form | - 폼 상태 관리와 유효성 검사를 간편하게 처리할 수 있어 코드의 양을 줄일 수 있었습니다. - 비제어 컴포넌트로 이루어져 있어 불필요한 렌더링 최소화로 성능 최적화를 할 수 있었습니다. - 회원가입, 로그인, 게시글 작성 폼에 사용하여 개발 생산성을 향상시킬 수 있었습니다. |
Shadcn/UI | - 이 프로젝트에서는 Shadcn/UI의 다양한 컴포넌트를 활용하여 모달, 버튼, 스켈레톤 UI, 드롭다운 등의 UI 요소를 일관되고 세련되게 구현했습니다. |
Firebase (Authentication, Firestore, storage) | - 백엔드 인프라를 별도로 구축하지 않고도 인증, 데이터베이스, 스토리지 등의 기능을 제공하여 빠른 개발이 가능하기에 채택하였습니다. |
폴더 구조
📦
├─ .eslintignore
├─ .eslintrc.json
├─ .gitignore
├─ .husky
│ └─ pre-commit
├─ .prettierignore
├─ .prettierrc.cjs
├─ .vscode
│ └─ settings.json
├─ README.md
├─ app
│ ├─ (afterLogin)
│ │ ├─ [userId]
│ │ │ └─ post
│ │ │ └─ [postId]
│ │ │ ├─ _components
│ │ │ │ ├─ CommentList.tsx
│ │ │ │ ├─ SinglePost.tsx
│ │ │ │ └─ WriteComment.tsx
│ │ │ ├─ _hooks
│ │ │ │ ├─ useGetCommentList.ts
│ │ │ │ └─ useGetSinglePost.ts
│ │ │ ├─ _services
│ │ │ │ ├─ getCommentList.ts
│ │ │ │ └─ getPost.ts
│ │ │ ├─ edit
│ │ │ │ ├─ _components
│ │ │ │ │ └─ EditForm.tsx
│ │ │ │ ├─ _hooks
│ │ │ │ │ ├─ useEditForm.ts
│ │ │ │ │ └─ useEditPost.ts
│ │ │ │ └─ page.tsx
│ │ │ └─ page.tsx
│ │ ├─ _components
│ │ │ ├─ BackButton.tsx
│ │ │ ├─ CommentCount.tsx
│ │ │ ├─ Header.tsx
│ │ │ ├─ LikeCount.tsx
│ │ │ ├─ NavigationBar.tsx
│ │ │ ├─ NavigationItem.tsx
│ │ │ ├─ PostCard.tsx
│ │ │ ├─ PostContent.tsx
│ │ │ ├─ PostCreatedAt.tsx
│ │ │ ├─ PostImages.tsx
│ │ │ ├─ PostSetting.tsx
│ │ │ ├─ PostUserImage.tsx
│ │ │ ├─ PostUserNickName.tsx
│ │ │ ├─ RQProvider.tsx
│ │ │ └─ RedirectToLogin.tsx
│ │ ├─ home
│ │ │ ├─ _components
│ │ │ │ ├─ FollowPostList.tsx
│ │ │ │ ├─ PostList.tsx
│ │ │ │ ├─ Tab.tsx
│ │ │ │ └─ TabDecider.tsx
│ │ │ ├─ _hooks
│ │ │ │ ├─ useFollowPostList.ts
│ │ │ │ ├─ useInfinitePostList.ts
│ │ │ │ ├─ usePostSetting.ts
│ │ │ │ └─ useToggleLike.ts
│ │ │ ├─ _services
│ │ │ │ ├─ deletePost.ts
│ │ │ │ ├─ getFollowingPostList.ts
│ │ │ │ ├─ getLike.ts
│ │ │ │ ├─ getPostList.ts
│ │ │ │ ├─ likePost.ts
│ │ │ │ ├─ unLikePost.ts
│ │ │ │ ├─ updatePost.ts
│ │ │ │ └─ writePost.ts
│ │ │ └─ page.tsx
│ │ ├─ layout.tsx
│ │ ├─ messages
│ │ │ ├─ [userId]
│ │ │ │ ├─ _components
│ │ │ │ │ ├─ MessageForm.tsx
│ │ │ │ │ ├─ MessageList.tsx
│ │ │ │ │ └─ UserInfo.tsx
│ │ │ │ ├─ _hooks
│ │ │ │ │ ├─ useEndScroll.ts
│ │ │ │ │ ├─ useMessageForm.ts
│ │ │ │ │ ├─ useMessageList.ts
│ │ │ │ │ ├─ useRoomId.ts
│ │ │ │ │ └─ useSendMessage.ts
│ │ │ │ ├─ _services
│ │ │ │ │ └─ sendMessage.ts
│ │ │ │ └─ page.tsx
│ │ │ ├─ _components
│ │ │ │ ├─ MessageRoom.tsx
│ │ │ │ └─ MessageRoomList.tsx
│ │ │ ├─ _hooks
│ │ │ │ └─ useMessageRooms.ts
│ │ │ ├─ _services
│ │ │ │ ├─ getMessageRooms.ts
│ │ │ │ └─ getRoomId.ts
│ │ │ └─ page.tsx
│ │ ├─ post
│ │ │ ├─ _components
│ │ │ │ └─ PostForm.tsx
│ │ │ ├─ _hooks
│ │ │ │ ├─ usePostForm.ts
│ │ │ │ └─ useWritePost.ts
│ │ │ └─ page.tsx
│ │ ├─ search
│ │ │ └─ page.tsx
│ │ └─ users
│ │ └─ [userId]
│ │ ├─ _components
│ │ │ ├─ FollowButton.tsx
│ │ │ ├─ FollowCard.tsx
│ │ │ ├─ FollowModal.tsx
│ │ │ ├─ LogoutButton.tsx
│ │ │ ├─ MessageButton.tsx
│ │ │ └─ UserProfile.tsx
│ │ ├─ _hooks
│ │ │ ├─ useFollow.ts
│ │ │ ├─ useGetFollowData.ts
│ │ │ └─ useGetUserData.ts
│ │ ├─ _services
│ │ │ ├─ follow.ts
│ │ │ ├─ getFollowData.ts
│ │ │ ├─ getUser.ts
│ │ │ ├─ logout.ts
│ │ │ └─ unFollow.ts
│ │ └─ page.tsx
│ ├─ (beforeLogin)
│ │ ├─ _components
│ │ │ └─ RedirectToHome.tsx
│ │ ├─ layout.tsx
│ │ ├─ login
│ │ │ ├─ _components
│ │ │ │ └─ LoginForm.tsx
│ │ │ ├─ _hooks
│ │ │ │ └─ useLoginForm.ts
│ │ │ └─ page.tsx
│ │ ├─ page.tsx
│ │ └─ signup
│ │ ├─ _components
│ │ │ └─ SignUpForm.tsx
│ │ ├─ _hooks
│ │ │ └─ useSignUpForm.ts
│ │ ├─ _services
│ │ │ ├─ saveUserData.ts
│ │ │ └─ signUp.ts
│ │ └─ page.tsx
│ ├─ _hooks
│ │ ├─ useInfiniteScroll.ts
│ │ ├─ useOnAuth.ts
│ │ └─ usePreviewImage.ts
│ ├─ _services
│ │ └─ handleUpload.ts
│ ├─ _store
│ │ ├─ useFollowModal.ts
│ │ ├─ usePost.ts
│ │ └─ useTab.ts
│ ├─ _types
│ │ ├─ follow.ts
│ │ ├─ like.ts
│ │ ├─ message.ts
│ │ ├─ navigation.ts
│ │ ├─ post.ts
│ │ └─ user.ts
│ ├─ _utils
│ │ ├─ formatDateTime.ts
│ │ └─ hashUid.ts
│ ├─ favicon.ico
│ ├─ firebase.js
│ ├─ layout.tsx
│ ├─ loading.tsx
│ └─ not-found.tsx
├─ components.json
├─ components
│ └─ ui
│ ├─ SubmitButton.tsx
│ ├─ alert-dialog.tsx
│ ├─ alert.tsx
│ ├─ avatar.tsx
│ ├─ button.tsx
│ ├─ dialog.tsx
│ ├─ dropdown-menu.tsx
│ ├─ input.tsx
│ ├─ label.tsx
│ ├─ loader.tsx
│ ├─ skeleton.tsx
│ └─ textarea.tsx
├─ lib
│ └─ utils.ts
├─ next.config.mjs
├─ package-lock.json
├─ package.json
├─ postcss.config.mjs
├─ public
│ ├─ icon.svg
│ ├─ logo.png
│ ├─ next.svg
│ └─ vercel.svg
├─ styles
│ └─ globals.css
├─ tailwind.config.ts
└─ tsconfig.json
-
Firebase Authentication을 통한 회원가입 및 로그인/로그아웃 기능 구현
-
비밀번호 보안 가이드라인 준수
-
회원가입 필수 요소: 이름, 닉네임, 프로필 이미지, 인사말
회원가입 | 로그인 |
---|---|
-
게시글 CRUD 기능 구현 (Create, Read, Update, Delete)
-
게시글 조회 시 useInfiniteQuery 기능을 사용한 무한 스크롤 페이지네이션 적용
-
게시글 상세 페이지 모달로 구현 및 서버 사이드 렌더링 적용
-
이미지 파일 첨부 및 Firebase Cloud Storage 활용
피드 리스트 | 피드 작성 |
---|---|
피드 수정 | 피드 삭제 |
---|---|
-
사용자는 모든 게시글에 좋아요 가능
-
댓글 조회, 생성, 수정, 삭제 기능 구현
-
댓글 조회 시 useInfiniteQuery 기능을 사용한 무한 스크롤 페이징 적용
피드 좋아요 | 댓글 작성 |
---|---|
-
사용자 간 팔로우/언팔로우 관계 설정 및 수정
-
팔로우/팔로잉 수 표시 및 유저 리스트 제공
-
팔로우한 사용자의 게시글만 볼 수 있는 페이지 제공
유저 프로필 | 팔로우 / 언팔로우 | 팔로우 / 팔로잉 리스트 |
---|---|---|
-
Firebase를 활용한 1:1 실시간 채팅 구현
-
채팅 메시지 보내기 및 받기 기능
-
채팅 목록 페이지 제공
채팅 |
---|
좋아요 버튼 클릭 시 의도하지 않은 페이지 이동 문제
- 피드의 좋아요 버튼을 클릭할 때, 좋아요 그능은 정상적으로 동작하지만 동시에 해당 피드의 상세 페이지로 이동하는 문제 발생
- 좋아요 버튼을 감싸고 있는 부모 요소(div)에 피드 상세 페이지로 이동하는 onClick 이벤트가 설정되어 있었음
- 좋아요 버튼 클릭 시, 이벤트 버블링으로 인해 부모 요소의 onClick 이벤트까지 실행되어 의도하지 않은 페이지 이동이 발생했습니다.
- 좋아요 버튼의 onClick 이벤트 핸들러 내에서
event.stopPropagation()
을 호출하여 이벤트 버블링을 막음 - 이를 통해 좋아요 버튼 클릭 시, 해당 이벤트가 부모 요소로 전파되는 것을 막을 수 있었음
const onToggleLike = (event: MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
setLiked(prev => !prev);
if (liked) {
mutationUnLike.mutate();
} else {
mutationLike.mutate();
}
};
좋아요 카운트 동기화 문제
- 메인 피드리스트에 있는 피드의 좋아요 버튼을 클릭 시 정상작동하지만, 해당 피드의 상세 페이지로 이동하면 좋아요 카운트가 동기화되지 않는 문제 발생
LikeCount
컴포넌트의 좋아요 카운트 값과PostCard
의 좋아요 카운트 값이 연동되지 않아 좋아요 개수가 실시간으로 업데이트되지 않음LikeCount
컴포넌트 내부에서 좋아요 카운트값을 별도로 관리하고 있어서,PostCard
컴포넌트의 좋아요 카운트 값과 동기화되지 않음
- 좋아요 카운트 값을
LikeCount
컴포넌트에서 관리하지 않고,PostCard
컴포넌트에서 props로 전달하도록 변경 - 좋아요
useMutation
의onSuccess
콜백에서queryClient.invalidateQueries
를 호출하여 해당 피드와 관련된 쿼리를 무효화하고 최신 데이터를 가져오도록하여 해결
const mutationLike = useMutation({
mutationFn: () => likePost({ userId, postId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['post'] });
queryClient.invalidateQueries({ queryKey: ['like', userId] });
},
onError: error => {
console.error('Error like post:', error);
},
});
- 고려사항 : 특정 피드의 좋아요 상태만을 업데이트 했는데 invalidateQueries로 인해 메인 피드의 목록들 전체가 초기화되어 불필요한 초기화가 생기는 것 같아 더 나은 최적화 방법에 대해 고려해야 될 필요가 있음
throttling 함수가 의도한 대로 동작하지 않는 문제
- 무한스크롤 적용과정에서
lodash
의throttle
함수를 사용했지만, 예상한 대로 동작하지 않음
2000ms로 설정한 시간 간격으로 함수 호출이 제한되지 않고, 딜레이 없이 자주 호출됨
const throttledFetchNextPage = throttle(fetchNextPage, 2000);
-
throttle
함수는 호출될 때마다 새로운 함수 인스턴스를 생성함. -
React 컴포넌트가 리렌더링되면 컴포넌트 내부의 모든 코드가 다시 실행이 되기 때문에,
throttle(fetchNextPage, 2000)
도 다시 호출이 되어 새로운throttle
함수가 생성됨. -
이로 인해,
throttle
함수가 의도한 대로 함수 호출을 제한하지 못하게 되어 매번 새로운throttledFetchNextPage
함수가 생성되어 기존의throttle
상태가 초기화 됨
-
useCallback
을 사용하여 문제 해결 -
useCallback
훅을 사용하여throttle
함수를 메모이제이션 하면,fetchNextPage
가 변경되지 않는 한 동일한 함수 참조를 유지하게 되어throttle
의 호출 제한 기능이 의도한 대로 동작함
const throttledFetchNextPage = useCallback(throttle(fetchNextPage, 2000), [fetchNextPage]);