Skip to content

FRONTENDSCHOOL5/final-05-Mandle_mandle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧤 만들만들 🧤

프로젝트 배너 이미지

함께 만드는 즐거움으로, 일상을 가득 채우다 🍏

🔗 배포 링크 👩‍🏫 강사 계정 👩‍👧‍👦 수강생 계정
https://mandle-mandle.netlify.app/ ID: [email protected]
PW: Mandletest2!
ID: [email protected]
PW: dodo123

0. 목차

  1. 프로젝트 소개
  2. 기술 및 개발환경
  3. 협업 방식
  4. 폴더구조
  5. 기능 UI
  6. 피드백
  7. 주요 기능
  8. 성능개선
  9. 팀원 소개

1. 프로젝트 소개

  • 만들만들은 원데이 클래스 서비스를 제공하는 참여형 소셜 커머스 플랫폼 입니다.
  • 클래스 예약기능과 홍보를 지원해 강사들이 자유롭게 자신의 역량을 발휘하고 창의적인 활동을 할 수 있는 공간을 제공합니다.
  • 강사와 수강생이 서로 소통하고, 수강생이 자신의 관심 분야나 취미를 발전시키는 동시에 다른 사람들과 교류하며 함께 즐길 수 있는 SNS 공간을 제공합니다.
  • 역량을 나누고, 함께 성장하며, 자신의 능력을 발전시키는 공간을 통해 우리는 사용자들의 라이프 스타일을 더욱 풍요롭게 만들고자 합니다.

2. 기술 및 개발환경

[기술 스택]

사용 기술            
패키지  
포맷터  
협업      
디자인      
IDE  

[기술스택 상세]

  • Recoil을 택한 이유

    • React의 UseState훅과 비슷하게 동작하고 직관적이면서 간단한 구조를 가져 코드의 양을 줄일 수 있었습니다.
    • 대규모 데이터를 다루는 것이 아니고 상대적으로 적은 코드를 작성하는 Recoil이 Redux에 비해 유리하다 생각하였습니다.
    • 상태 관리 라이브러리를 사용해 본 경험의 필요성을 느껴 recoil로 상태관리하기로 결정하였습니다.
    • 유저정보의 지속성을 위해 사용한 recoil-Persist
      • recoil은 기본적으로 새로고침 또는 컴포넌트 언마운트 시 상태가 초기화되지만, recoil-persist를 사용하면서 세션 스토리지에 저장하여 지속성을 유지하였습니다.

  • Custom Hook 사용

    1. useProfileInput
    • 프로필 입력 폼에 관련된 상태 밑 이벤트 처리 로직을 관리하는데 사용하였습니다.
    • 프로필 입력 폼 컴포넌트에서 해당 훅을 호출하여 상태 및 이벤트를 관리하였습니다.
    1. useTextareaResizeHook
    • textarea 요소의 크기를 내용에 맞게 자동으로 조절하는데 사용하였습니다.

  • axios 라이브러리 활용

    • 비동기로 HTTP 통신을 할 수 있어 return을 promise 객체로 해주기 때문에 response 데이터를 쉽게 다루기 위해 사용하였습니다.
    • Promise 기반의 API를 제공하여 비동기적인 방식으로 HTTP 요청을 처리하였습니다.

  • 중첩 라우트 처리를 위한 route-Outlet

    • 일반적으로 부모 라우트 컴포넌트 내에서 사용되며, 중첩된 자식 라우트 컴포넌트의 출력을 표시하는데 사용하였습니다.
    • 중첩된 뷰를 구성하고 다양한 경로에 따라 적절한 컴포넌트를 렌더링 할 수 있었습니다.

[코딩 컨벤션]

Prettier & GlobalStyle
Prettier 컨벤션

{
  "printWidth": 80, // 한줄에 80자 처리
  "singleQuote": true, // 홑따옴표로 처리
  "jsxSingleQuote": true,
  "tabWidth": 2, //들여쓰기 2칸으로 지정
  "semi": true,
  "trailingComma": "all",
  "bracketSpacing": true,
  "arrowParens": "always"
}


GlobalStyled 커벤션

 :root{
    --main-color: #036635 ;
    --sub-color:#B1D4C3;
    --font-color: #000000;
    --sub-font-color: #767676;;
    --border-color: #DBDBDB ;
    --background-color: #F2F2F2;
    --error-color: #EB5757;
    --font-xl : 20px;
    --font-lg : 16px;
    --font-md : 14px;
    --font-sm : 12px;
  }

    :root {
    --font--bold: 700;
    --font--semibold: 500;
    --font-regular: 400;;
  }

[ Git-flow ]

기본적으로 5가지 브랜치를 활용하는 Git-flow 전략이 일방적이나, 프로젝트 규모에 맞춰 3가지로 축소하여 사용했습니다.

  • main : 최종 배포하기 위한 브랜치
  • develop : 기능 구현, 버그 수정과 같은 기능을 합쳐 확인하기 위한 브랜치
  • feature : 세부 기능 작업들을 위한 브랜치
    • 각 브랜치의 이름은 feat/세부기능으로 이름 지어 어떠한 기능의 브랜치인지를 알 수 있도록 했습니다.
    • push 완료 후에는 해당 브랜치를 develop으로 merge하여 역할 별로 진행한 기능을 확인하며 구현했습니다.

[Git/Commit 컨벤션]

커밋 유형
커밋 유형 커밋 메세지 의미
Feat 새로운 기능 추가
🐛 Fix 버그 & 에러 수정
📝 Docs 리드미 등 문서 수정, 라이브러리 설치
🎨 Style 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
🖌 Design UI 디자인 변경
🔨 Refactor 코드 리팩토링
🤔 Test 테스트 코드, 리팩토링 테스트 코드 추가
Chore 빌드 업무 수정, 패키지 매니저 수정
🗒 Rename 파일명 혹은 폴더명 수정, 위치 옮기기
🔥 Remove 파일 삭제
커밋 메시지(제목 /본문 /숫자)
git commit -m "[✨Feat] 로그인 기능 구현 #13
// 커밋 구분 , 제목, 이슈 번호


3. 협업 방식

  • 🗣 스크럼

    스크럼을 매일 오전 9시와 오후 5시에 진행하여 각 구성원의 진행 상황과 겪은 문제, 해결 방안을 공유하였습니다. image

  • 📹 라이브 쉐어

    라이브 쉐어 프로그램을 사용하여 오류나 어려움이 있는 부분을 함께 해결하고 기술적 중요도가 높은 파트에서는 함께 작업함으로써 전체 코드에 대한 각 구성원들의 이해도를 높일 수 있었습니다.

  • 🥢 페어 프로그래밍

    공통으로 담당한 컴포넌트 작업 시 페어 프로그래밍 협업 방식을 사용해 팀원 모두가 코드 전체 흐름을 이해하고 기술적 완성도 높은 코드를 작성하도록 커뮤니케이션 기술을 적극적으로 사용할 수 있는 기회를 만들었습니다.

  • 🗂 깃플로우 전략

    main - develop - feature/(페이지명)
    

    깃플로우 전략을 활용 작업할 페이지 별로 브랜치를 구분하여 협업의 능률을 높였습니다. 충돌없이 맡은 역할 페이지 작업을 수행할 수 있었습니다.

  • 📈 깃헙 프로젝트

    깃허브 이슈깃헙 프로젝트 기능으로 전체 프로젝트의 일정과 목적 별로 구분해 구현해야 하는 기능들을 정리할 수 있었고 원활한 커뮤니케이션으로 작업 속도와 안정성을 동시에 얻을 수 있도록 했습니다. image image


4. 폴더구조

📂만들만들 폴더트리 📦 final-05-Mandle_mandle ├─ 📄 .gitignore
├─ 📂 .vscode
├─ 📄 .eslintrc.json
├─ 📄 .prettier.json
├─ 📄 settings.json
├─ 📄 README.md
├─ 📄 package-lock.json
├─ 📄 package.json
├─ 📂 public
│ └─ 📂 src
│ ├─ 📄 App.js
├─ 📂 Hooks
├─ 📂 Store
├─ 📂 api
├─ 📂 assets
│ ├─ 📂 font
│ └─ 📂 img
│ └─ 📂 temp
├─ 📂 componenent
│ └─ 📂 Common
├─ 📄 index.js
├─ 📂 pages
│ ├─ 📂 Chatting
│ ├─ 📂 Class
│ ├─ 📂 Home
│ ├─ 📂 Loadinng
│ ├─ 📂 Loging
│ ├─ 📂 NotFound
│ ├─ 📂 Posting
│ ├─ 📂 Profile
│ ├─ 📂 Signup
│ └─ 📂 Splash
└─ 📂 styles

5. 기능 UI

  • Splash, 회원가입, 로그인

Splash 회원가입 프로필 설정 로그인
  • 홈피드, 검색

홈피드(팔로잉O) 홈피드(팔로잉X) 검색
  • 게시글

게시글 등록 게시글 수정 게시글 삭제
  • 클래스

클래스 피드 클래스 상세 클래스 예약하기
클래스 등록 클래스 삭제
  • 댓글

댓글 등록 & 좋아요 댓글 삭제 댓글 신고
  • 마이 프로필

강사 프로필 수강생 프로필 게시물 앨범형
찜한 클래스 예약 클래스
프로필 수정 팔로잉, 팔로우
  • 다른 유저 프로필

채팅, 팔로우, 공유 다른 강사 프로필 다른 수강생 프로필
  • 채팅, NotFound(404), 로그아웃

채팅 NotFound(404) 로그아웃

6. 피드백


7. 주요 기능

클래스 상세 페이지에 있는 찜하기 기능은 해당 클래스를 찜하여 수강생 본인 프로필에서 찜한 클래스 목록을 볼 수 있으며 또한 찜하기를 취소하여 찜한 클래스를 관리할 수 있습니다.
클래스 예약 페이지는 수강생이 원하는 클래스를 날짜와 시간을 정하여 예약하는 페이지입니다. 날짜와 시간을 모두 선택해야만 예약이 가능하며, 만약 동일한 날짜에 이미 예약한 클래스가 있을 경우 중복 예약이 불가능합니다. 예약한 클래스 정보는 수강생 본인 프로필로 이동하여 예약한 클래스 정보를 확인할 수 있습니다.

수강후기 작성 페이지는 사용자가 자신이 수강한 클래스의 이미지, 이름, 예약한 날짜, 시간 정보를 드롭다운으로 선택하여 해당 클래스에 대한 후기 및 경험, 사진을 공유하는 페이지입니다. 로그인한 사용자가 강사 계정인 경우, 페이지는 수강후기 작성이 아닌 공지 및 홍보를 위한 페이지로 변경됩니다. 강사는 자신이 등록한 클래스의 이미지, 이름, 클래스 태그, 그리고 가격 정보를 받아와 공지를 작성할 수 있게 됩니다. 중요한 점은 페이지가 사용자의 로그인 계정 유형(수강생 또는 강사)을 감지하고, 이에 따라 서로 다른 정보 를 렌더링한다는 것입니다.


UX를 고려한 조건부 모달, 토스트 메시지, 스켈레톤 UI 구현

조건부 모달
조건부 모달 조건부 모달2
    export default function Modal({
    onClick,
    setModalOpen,
    setAlertModalOpen,
    text,
    type,
  }) {
    const handleOverlayClick = (event) => {
      if (event.target === event.currentTarget) {
        setModalOpen(false);
      }
    };
    const handleAlertModalOpen = () => {
      setModalOpen(false);
      setAlertModalOpen(type);
    };
  
    return (
      <ModalWrap onClick={handleOverlayClick}>
        <ModalBox>
          <div></div>
          <ul>
            <li>
              <button onClick={handleAlertModalOpen}>{text}</button>
            </li>
            {(type === 'delete' || type === 'class') && (
              <li>
                <button onClick={type && onClick}>
                  {type === 'delete'
                    ? '수정'
                    : type === 'class'
                    ? '클래스 상세 페이지로 이동'
                    : null}
                </button>
              </li>
            )}
          </ul>
        </ModalBox>
      </ModalWrap>
    );
  }
     function MiniClassList({ classItem, page, token, setClassUpdated }) {
     const [alertModalOpen, setAlertModalOpen] = useState(false);
     const [isModalOpen, setModalOpen] = useState(false);
     const navigate = useNavigate();
   
     const handleDeleteSubmit = async () => {
       const response = await DeleteClass(classItem.id, token); // Call the API component
       if (response) {
         setAlertModalOpen(false);
         alert(`해당 게시글이 삭제되었습니다.`);
   
         setClassUpdated(true); // 새로고침(상태변경으로 바꿀 예정)
       }
     };
   
     const handleMoveClassDetail = () => {
       navigate(`/class/detail/${classItem.id}`);
     };
   
     return (
       <>
         {isModalOpen && (
           <Modal
             setModalOpen={setModalOpen}
             setAlertModalOpen={setAlertModalOpen}
             onClick={handleMoveClassDetail}
             type='class'
             text='삭제'
           />
         )}
         {alertModalOpen && (
           <ModalAlert
             setAlertModalOpen={setAlertModalOpen}
             onClick={handleDeleteSubmit}
           />
         )}
       </>
     );
   }
   
   export default MiniClassList;

조건부 모달 기능은 Modal 컴포넌트에서 isModalOpen과 alertModalOpen이라는 두 개의 상태값을 통해 모달의 표시 여부를 제어하고 있습니다.

isModalOpen 상태값이 true일 때, Modal 컴포넌트가 렌더링되어 사용자에게 표시됩니다.
Modal 컴포넌트에서는 handleAlertModalOpen 함수가 호출되면, setModalOpen(false)를 통해 현재 모달을 닫고, setAlertModalOpen(type)를 통해 알림 모달을 열게 됩니다. 이 때 type은 알림 모달의 타입을 결정합니다.
알림 모달은 alertModalOpen 상태값이 true일 때 렌더링되어 사용자에게 표시됩니다. 이 때 alertModalOpen의 값은 알림 모달의 타입을 결정합니다.
알림 모달에서는 사용자가 확인 또는 취소 버튼을 클릭하면 setAlertModalOpen(false)를 호출해 알림 모달을 닫습니다.
Modal컴포넌트를 적용하고 있는 MiniClassList 컴포넌트에서 Modal과 ModalAlert 컴포넌트의 열림 상태를 제어하면서, 필요에 따라 각 모달에서 수행할 동작을 정의한 함수를 onClick prop으로 전달하여 모달의 동작을 제어합니다.

토스트 메시지
토스트 메시지 토스트 메시지2
  import { useState, useEffect } from 'react';
  import styled, { keyframes } from 'styled-components';
  
  export const Toast = ({ toastMessage, setToastMessage }) => {
    const [isFadeOut, setIsFadeOut] = useState(false);
    useEffect(() => {
      const timer = setTimeout(() => {
        setIsFadeOut(true);
      }, 2500);
  
      return () => {
        clearTimeout(timer);
      };
    }, [toastMessage]);
  
    useEffect(() => {
      if (isFadeOut) {
        const timer = setTimeout(() => {
          setToastMessage('');
          setIsFadeOut(false);
        }, 1000);
        return () => {
          clearTimeout(timer);
        };
      }
    }, [isFadeOut]);
  
    return <ToastWrap fadeOut={isFadeOut}>{toastMessage}</ToastWrap>;
  };
  
  const fadeIn = keyframes`
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  `;
  
  const fadeOut = keyframes`
    from {
      opacity: 1;
    }
    to {
      opacity: 0;
    }
  `;

토스트 메시지를 표시하고 일정 시간이 지나면 사라지게 하는 기능을 구현하였습니다. 구현 내용은 다음과 같습니다.

1. useState를 통해 isFadeOut이라는 상태를 생성하고 초기값을 false로 설정합니다. 이 상태는 토스트 메시지가 사라지는 애니메이션을 제어합니다.
2. 첫 번째 useEffect는 toastMessage가 변경될 때마다 실행됩니다. 이 useEffect 안에서는 타이머를 설정하여 2.5초 후에 isFadeOut 상태를 true로 변경합니다. 이로 인해 토스트 메시지가 사라지는 애니메이션이 시작됩니다.
3. 두 번째 useEffect는 isFadeOut 상태가 변경될 때마다 실행됩니다. isFadeOuttrue일 때, 즉 토스트 메시지가 사라지는 애니메이션이 시작되면 1초 후에 setToastMessage('')를 호출하여 토스트 메시지를 비우고, setIsFadeOut(false)를 호출하여 애니메이션 상태를 초기화합니다.
4. ToastWrap에서 fadeOut prop에 따라 서로 다른 애니메이션을 적용합니다. fadeOut prop이 true일 때는 fadeOut 애니메이션을, false일 때는 fadeIn 애니메이션을 적용합니다.
토스트 메시지가 사라진 후에는 toastMessage를 비우고 애니메이션 상태를 초기화하여, 호출마다 토스트 메시지가 계속해서 나타날 수 있도록 구현했습니다.

스켈레톤 UI
홈-스켈레톤 클래스 스켈레톤 프로필 스켈레톤

데이터를 불러오는 동안 스켈레톤 UI를 표시함으로써 사용자에게 로딩 중임을 알려주는 기능을 구현하였습니다.
데이터 로딩이 끝나면 실제 데이터를 표시하는 방식으로 구현하여
데이터 로딩 시간 동안 사용자가 빈 화면을 보는 것을 방지하고, 사용자 경험을 개선하고자 하였습니다.
1. useState를 통해 loading이라는 상태를 생성하고 초기값을 true로 설정합니다. 이 상태는 데이터 로딩 상태를 나타냅니다.
2. 데이터를 불러온 후에는 setTimeout 함수를 이용하여 1초 후에 setLoading(false)를 호출하여 로딩 상태를 종료합니다.
3. 렌더링 부분에서 loading 상태에 따라 다른 UI를 표시합니다. loading이 true일 때는 PostSkeleton 컴포넌트를 통해 스켈레톤 UI를 표시하고, loading이 false일 때는 스켈레톤 UI가 사라진 후 실제 데이터를 표시합니다.


8. 성능개선

검색 디바운싱

  • 문제: 기존 계정 검색 시 input의 변화가 감지될 때 마다 API 요청이 매번 발생하여 불필요한 API 요청이 발생합니다. 또한, 사용자가 검색어 입력을 완료하지 않은 상태에서 결과가 표시되다보니 부정확한 결과가 표시되어 사용자 경험이 저하되는 문제도 있었습니다

  • 해결: lodash 라이브러리를 활용하여 계정 검색 시 디바운스를 구현했습니다. 이를 통해 사용자가 입력하는 동안 일정 시간 동안 검색 요청을 지연시켰습니다. 결과적으로 연속된 검색 요청을 방지하고, 사용자가 입력을 마친 후 단 한 번의 검색 요청만을 처리하도록 조정했습니다.

이미지 압축 적용

  • 문제: 대용량 이미지 파일을 웹 페이지에 업로드할 때, 해당 이미지들을 로딩하는 과정에서 화면에 용량이 작은 이미지보다 더 늦게 나타나는 문제가 발생했습니다. 위 문제로 인해 사용자가 이미지를 곧바로 확인하지 못하는 불편함을 겪을 수 있었습니다.

  • 해결: imageCompression 라이브러리를 활용하여 이미지 업로드 시 이미지를 압축하여 파일 크기를 줄였습니다. 이를 통해 대역폭과 데이터 부하를 감소시키고, 페이지 로딩 속도를 향상시켜 사용자가 웹 페이지에 접근시 이미지를 빠르게 로드하여 보다 원할하게 확인할 수 있도록 조정했습니다.

이미지 Lazy-loading 적용

  • 문제: 페이지 로딩 시 모든 이미지가 동시에 다운로드되어 초기 페이지 로딩 속도가 느렸습니다. 특히 대용량 이미지 파일이 많은 경우 사용자는 페이지가 로딩될 때까지 상당한 대기 시간을 경험했습니다.

  • 해결: lazy-loading을 통해 초기 페이지 로딩 시에는 원본 이미지 대신 저화질의 이미지를 먼저 보여준 뒤 원본 이미지 로드가 완료되는 시점에 다시 원본 이미지를 보여줌으로써 로딩시간을 단축시키고 사용자 경험을 향상 시켰습니다.

9. 팀원 소개

[만들5️⃣조]

안녕하세요! 저희는 4명의 Front-end 개발자로 구성된 '만들5️⃣ 조' 입니다.

(🦁멋쟁이사자처럼 프론트엔드스쿨 5기 프로젝트 5팀)

차다연 김예지 우경석 윤서준
Dayeon_profile_img Yegi_profile_img kyungseuk_profile_img seojun_profile_img
Da-Yeon yeji_kim 5647kr junny97
팀장 팀원 팀원 팀원

[역할 분담]

Releases

No releases published

Packages

No packages published

Languages