Skip to content

ncb6206/stockaca

Repository files navigation

Stockaca

Next.js + Typescript + Firebase Stock SNS Service

테스트 계정

ID : [email protected] 
PW : test1234!

📌 기술 스택 & 사용 라이브러리

구분 스택 & 라이브러리
언어
메인 라이브러리
기타 라이브러리 Static Badge
패키지 관리
배포
기술 스택 선정 이유
기술 스택 선정 이유
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로 전달하도록 변경
  • 좋아요 useMutationonSuccess 콜백에서 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 함수가 의도한 대로 동작하지 않는 문제

문제

  • 무한스크롤 적용과정에서 lodashthrottle함수를 사용했지만, 예상한 대로 동작하지 않음
    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]);

About

주식 정보 공유 SNS 웹서비스입니다

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published