-
프로젝트 기간 : 2022.12.9. ~ 2023.1.4.
-
배포 URL : 🔗 멍하냥
-
Test ID / PW : [email protected] / test1111
- '멍하냥'은 반려동물들의 일상을 공유하는 SNS 플랫폼입니다.
- '멍하냥'이라는 이름은 "뭐해?"라고 묻는 말에 강아지와 고양이를 나타내는 '멍'과 '냥'을 붙여 반려동물들의 안부를 묻는 의미를 담았습니다.
- 사용자들은 자신의 반려동물 사진을 기록하고 자랑할 수 있습니다.
- 다른 계정을 팔로우해서 팔로우한 사용자들의 게시물을 피드에서 한 번에 볼 수 있고, 댓글과 좋아요를 통해 서로 소통을 할 수 있습니다.
안녕하세요, 저희는 4명의 Front-end 개발자로 구성된 13oosters팀입니다.
13이 영어 알파벳의 'B'와 비슷한 모양을 활용해 '부스터즈'라는 팀명을 지었습니다. boost는 신장시키다/북돋우다/격려/촉진 등의 의미를 가지고 있고, booster는 촉진제/추진로켓 등의 의미를 가지고 있습니다. 이번 프로젝트가 저희 성장의 촉진제가 되었으면 하는 마음에서 작명했습니다. 🚀🔥
(멋쟁이사자처럼 프론트엔드 스쿨 3기 프로젝트 13팀입니다.)
👑 이준근 | 🎨 김소영 | 💻 이준엽 | 📜 최현지 |
---|---|---|---|
blog: jxxunnn github: Jxxunnn |
github: TommyKim97 | blog: yubmun github: yubmun |
github: h12j21-star |
- GitHub Projects : 진행상황을 page별로 나누어 GitHub Issues에서 각자 맡은 업무를 이슈 템플릿에 체크리스트 형식으로 공유했습니다.
- GitHub Wiki : 회의와 컨벤션을 기록하고 요점노트를 기록하여 공유하였습니다.
- Figma : 동시 접속하여 함께 UI, 색상 디자인 상의를 진행했습니다.
Discord
,Gather Town
: 원활한 의사소통을 위해 디스코드와 게더타운에서 영상 및 음성 통화를 적극 활용했습니다.
페이지 별로 기능을 담당하여 프로젝트를 진행하고자 🔗 Git Flow 방식 을 사용했습니다.
페이지 별 브랜치를 만들고 각자 작업 브랜치를 따로 생성하여, 페이지 브랜치로 PR 및 Merge를 진행합니다.
- feat: 새로운 기능 구현
- fix: 오류 수정
- docs: 문서 수정 (예 : readme.md, json 파일 등 수정/ 문서 관련 라이브러리 설치 등)
- design: 마크업 및 style 작업
- style: 코드에 변화가 없는 수정 (예 : prettier, 세미콜론 등)
- refactor: 코드 리팩토링
- comment: 주석 추가
- chore: 빌드 부분 혹은 패키지 매니저 수정사항
- rename: 파일 혹은 폴더명 수정 or 옮기기
- remove: 파일 삭제
통일성 있는 코드 작성을 위해 다양한 🔗 코드 컨벤션 을 정해 사용했습니다.
{
"printWidth": 80,
"singleQuote": false,
"jsxSingleQuote": false,
"tabWidth": 2,
"semi": true,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}
{
"extends": ["react-app", "react-app/jest", "naver" "prettier"],
"rules": {
"no-console": 1,
"react-hooks/exhaustive-deps": 0,
"no-unused-expression": false
}
}
각자 맡은 업무를 이슈 템플릿에 체크리스트 형식으로 공유했습니다.
- 팀 노션에 동시 접속하여 🔗 주간 회의를 진행하고, GitHub Wiki에 회의 내용을 정리해 업로드했습니다.
- 프로젝트에서 사용하는 Styled-Components의 공식문서를 번역하며 🔗 공부 내용을 기록하여 공유하였습니다.
- 게더 타운의 가상 팀 공간을 활용하여 협업을 진행했습니다. 페어 프로그래밍을 진행하거나 전체 상의가 필요한 작업을 할 때, 게더 타운의 화면 공유와 음성 통화 기능과 VSCode의 Live Share 확장 기능을 적극 활용했습니다.
- 로그인
- 회원가입
- 유효성 검사
- 토큰 검증
- 게시글 업로드
- 무한 스크롤
- 유저 검색
- 게시글 수정, 삭제
- 댓글 게시, 삭제
- 게시글/댓글 신고
- 로그아웃
- 프로필 수정
- 팔로우 / 팔로잉
- 그리드, 리스트형 게시글
- assets/ : 이미지, 아이콘
- components/ : 컴포넌트
- components/common/ : 공통
- pages/ : 생성한 컴포넌트들을 조합해 각 페이지 구현
- style/ : globalstyle, 공통 스타일
- utils/ : util 함수
📦 project-sns-react🐶😺
.eslintrc.json
.github
│ ├─ ISSUE_TEMPLATE
│ └─ PULL_REQUEST_TEMPLATE
├─ .gitignore
├─ .gitmessage.txt
├─ .prettierignore
├─ .prettierrc.json
├─ README.md
├─ package-lock.json
├─ package.json
├─ public
│ └─ index.html
└─ src
├─ App.js
├─ assets
│ └─ image
├─ components
│ ├─ common
│ │ ├─ Card.js
│ │ ├─ Cards.js
│ │ ├─ Logo.js
│ │ ├─ Modal.js
│ │ ├─ Notice.js
│ │ ├─ ProfileSetting.js
│ │ └─ SplashScreen.js
│ ├─ follow
│ │ ├─ FollowersCard.js
│ │ ├─ FollowersCards.js
│ │ ├─ FollowingsCard.js
│ │ └─ FollowingsCards.js
│ ├─ home
│ │ ├─ EmptyFeed.js
│ │ ├─ Feeds.js
│ │ ├─ Loading.js
│ │ └─ PostImage.js
│ ├─ login
│ │ ├─ Form.js
│ │ ├─ Login.js
│ │ └─ Welcome.js
│ ├─ post
│ │ ├─ Comment.js
│ │ ├─ Comments.js
│ │ ├─ Detail.js
│ │ ├─ Dialog.js
│ │ └─ Writing.js
│ ├─ profile
│ │ ├─ GridCard.js
│ │ ├─ GridCards.js
│ │ ├─ MyProfileButton.js
│ │ ├─ ProfileInformation.js
│ │ └─ SortButtons.js
│ ├─ search
│ │ ├─ Result.js
│ │ └─ Results.js
│ ├─ style
│ │ ├─ Button.js
│ │ ├─ GlobalStyle.js
│ │ ├─ Header.js
│ │ ├─ NavBar.js
│ │ ├─ PageLayout.js
│ │ ├─ follow
│ │ │ ├─ FollowButton.js
│ │ │ └─ FollowCancelButton.js
│ │ ├─ form
│ │ │ ├─ ErrorMessageP.js
│ │ │ ├─ LoginButton.js
│ │ │ ├─ LoginForm.js
│ │ │ ├─ LoginInput.js
│ │ │ ├─ SignUpButton.js
│ │ │ └─ TitleH2.js
│ │ └─ profile
│ │ ├─ FollowButton.js
│ │ ├─ FollowCountDiv.js
│ │ ├─ FollowCountP.js
│ │ └─ FollowCountSpan.js
│ └─ upload
│ ├─ ImageUpload.js
│ ├─ Posting.js
│ └─ TextUpload.js
├─ hooks
│ ├─ useFetch.js
│ └─ useInput.js
├─ index.css
├─ index.js
├─ pages
│ ├─ ErrorPage.js
│ ├─ FollowersPage.js
│ ├─ FollowingsPage.js
│ ├─ HomePage.js
│ ├─ LoginPage.js
│ ├─ PostPage.js
│ ├─ ProfileEditPage.js
│ ├─ ProfilePage.js
│ ├─ SearchPage.js
│ └─ UploadPage.js
├─ reportWebVitals.js
├─ routes
│ └─ Router.js
└─ utils
├─ api.js
├─ postData.js
└─ validate.js
- 상세 기능 설명은 각 페이지별 링크 연결해두었습니다.
🔗 splash | 🔗 로그인 페이지 | 🔗 회원가입 페이지 |
---|---|---|
🔗 홈 페이지 | 🔗 검색 페이지 |
---|---|
🔗 마이 프로필 페이지 | 🔗 유저 프로필 페이지 | 🔗 팔로워 페이지 & 팔로잉 페이지 |
---|---|---|
🔗 프로필 수정 페이지 | 🔗 로그아웃 모달 |
---|---|
🔗 게시글 작성 페이지 | 🔗 게시글 상세 페이지 | 🔗 홈 페이지 모달 |
---|---|---|
🔗 게시글 수정 / 신고 모달 | 🔗 댓글 삭제 / 신고 모달 |
---|---|
axios로 서버와 통신하는 모든 부분에서 서버 주소와 코드가 반복되어 이를 줄이기 위해 custom axios
를 사용하였습니다.
const BASE_URL = "url";
const API = axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "application/json",
},
});
API.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
Promise.reject(error);
},
);
페이지 마다 존재하는 header의 구성이 비슷하여 공통 컴포넌트
로 만들었고, 각 페이지에 type을 지정하여 각 페이지 별로 header를 구분하였습니다.
export default function Header({
type,
setKeyword,
startTransition,
setProfileModal,
profileModal,
}) {
const UI = {
logo: (
<HeaderUI>
<button>
<img src={topLogoImage} alt="멍하냥" />
</button>
</HeaderUI>
),
search: (
),
profile: (
),
post: (
),
followers: (
),
followings: (
),
};
return <HeaderWrap>{UI[type]}</HeaderWrap>;
}
로그인 페이지 / 회원가입 페이지에서 유효성 검사를 react-hook-form
라이브러리를 사용했습니다. 입력 값 존재 여부, 포커스 등의 유틸을 지원하기 때문에 기능 구현의 리소스를 절약할 수 있었습니다.
const {
register,
setFocus,
handleSubmit,
formState: { isSubmitting, errors },
watch,
} = useForm({ mode: "onChange" });
const checkIsValue = (e) => {
e.target.value && watch("email") && watch("password")
? setIsValue(true)
: setIsValue(false);
};
const handleInput = (e) => {
setUserData({ ...userData, [e.target.name]: e.target.value });
checkIsValue(e);
};
...
검색 페이지에서는 Blocking rendering을 해결하여 성능을 최적화하는 것이 중요하다고 생각했습니다.
이를 위해 useTransition
훅을 사용하였습니다.
- useTransition은 상태변화의 우선순위를 지정해줍니다.
- 화면을 업데이트 하는 중에도 검색 input의 우선순위를 높여 입력이 끊기는 상황을 줄여줍니다.
- isPending의 boolean값을 이용하여 로딩중 ui를 띄어 ux를 향상 시켜줍니다.
export default function SearchPage() {
const [keyword, setKeyword] = useState(undefined);
const [isPending, startTransition] = useTransition();
return (
<PageLayout paddingValue={0}>
<h1 className="sr-only">검색 페이지</h1>
<Header
setKeyword={setKeyword}
startTransition={startTransition}
type="search"
/>
<Results keyword={keyword} isPending={isPending} />
<NavBar type="검색" />
</PageLayout>
);
}
...
search: (
<>
<h2 className="sr-only">검색창</h2>
<HeaderUI>
<button onClick={() => navigate(-1)}>
<img src={backImage} alt="뒤로 가기" />
</button>
<SearchDiv>
<HeaderInput
onKeyUp={(e) => {
startTransition(() => {
setKeyword(e.target.value);
});
}}
placeholder="계정 검색"
/>
<button
onClick={(e) => {
e.target.closest("div").childNodes[0].value = "";
setKeyword("");
//
}}
type="button"
>
<img src={cancelImage} alt="취소 버튼" />
</button>
</SearchDiv>
</HeaderUI>
</>
)
- 이번 프로젝트를 통해 학습하고 성장하자는 공동의 목표로 의미있는 과정을 남긴 것 같아 뿌듯합니다. 개발하는 재미를 다시금 느끼게 해준 팀원들 감사합니다.
- 정말 멋지고 좋은 팀원 분들을 만나서 많은 것들을 배울 수 있었던 감사한 시간이었습니다. 함께 프로젝트 하게 되어 영광이었고, 같이 즐거웠던 일화들도 소중한 기억으로 남을 것 같습니다. 🚀💗
- 여러모로 부족한 상태로 프로젝트를 시작했지만, 팀원분들의 유쾌한 응원과 기술 구현에 대한 지식공유를 해주셔서 여러 경험들을 할 수 있는 행복한 시간이었습니다. 정말 고생많으셨습니다~!
- 이번 프로젝트를 통해 자신감을 많이 얻었고 협업과정과 커뮤니케이션을 배울 수 있었습니다. 다들 정말 감사하고 수고하셨습니다~!💜