noy3928 / BookDiver-FE

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Diver(책 리뷰 공유 sns)📘

안녕하세요! 인상깊은 책의 구절을 사람들과 공유하고, 책으로 소통할 수 있는 sns 다이버입니다.
책 읽는 것을 좋아하고
다른사람과 읽은 책에 대해 이야기 나눌 수 있는 플랫폼이 없어서 아쉬웠던 분들을 위한 서비스입니다.

시연영상 보러가기



목차

  1. 팀원 소개 및 제작 기간
  2. 프로젝트 소개
  3. 사용 기술
  4. 협업 과정
  5. 신경 쓴 부분
  6. 기능소개/실행화면


팀원 소개 및 제작 기간

  • 2021년 7월 23일 ~ 9월 3일 (7주)
  • 7인 1조 팀프로젝트
    • 프론트엔드 (React) : 노예찬, 이현주
    • 백엔드 (Node.js) : 김승빈, 권오빈
    • 디자이너 : 조민정, 김언용, 곽은주


프로젝트 소개

ezgif com-gif-maker


  • 책을 좋아하는 사람들이 읽은 책의 구절을 공유하고 책을 추천하면서
  • 책을 중심으로 소통이 이루어지는 sns 서비스 입니다.
    시연영상 보러가기


사용 기술

Front-end

  • React
  • javascript

Back-end

deploy

  • AWS S3
  • Route 53
  • Cloud front
  • https


협업 과정

  • 전체 팁 협업 : notion

  • 협업

    • Figma, zeplin을 통하여 디자이너와 협업
    • swagger을 이용하여 백엔드와 협업
  • 구현
    : 모든 작업은 다음과 같은 과정을 거쳤습니다. 깃허브 리드미

  • 전략 우리는 깃 플로우 전략을 사용했습니다.


  • 각자의 origin develop branch 에서 작업했습니다.
  • 각각 기능마다 feature 브랜치를 생성한 후 작업을 진행했습니다.
  • pull request를 통해 upstreammerge하였으며 ,
  • rebase를 통해 커밋내역을 linear하게 유지할 수 있었습니다.
  • 그 결과, 어느 부분에서 어떤 코드가 추가 되고 오류가 났는지 쉽게 확인 할 수 있어 디버깅시 효율이 높아졌습니다.


✨ 신경 쓴 부분


1. 최적화

어떻게 특별한 기능을 넣을까? 보다
'어떻게 하면 최적화할 수 있을까?'를 고민했습니다.


1)무한스크롤 구현

  • 메인페이지에서 한번에 모든 게시물을 불러올 때, 렌더링 속도가 느려질 것을 고려하여 무한스크롤 기능을 구현하였습니다.
  • 기존의 스크롤 이벤트를 계산하는 방식으로 무한 스크롤을 계산할 경우, 불필요한 scroll 이벤트가 발생하고, Dom 요소의 reflow로 인해 브라우저 성능이 저하되는 것을 발견했습니다.
  • 때문에 이벤트에 최적화된 intersection observer api를 활용하여, 무한스크롤을 구현했습니다.


2)이미지 lazy loading

  • 메인페이지에서 무한 스크롤을 통해 데이터 요청을 끊어하는 것을 넘어서, 초기 렌더링 속도를 높일 수 있는 방법을 찾고 고민했습니다.
    그 결과, 이미지에 레이지 로딩을 적용할 경우 초기 렌더링 속도를 향상시킬 수 있다는 사실을 알게되었습니다.
  • 무한스크롤과 마찬가지로 intersection observer api를 활용해, 화면에 0.1 비율로 나타날 때, 이미지를 불러오도록 했습니다.
  • data-src에 img url을 기록해두고, 해당 이미지가 화면에 0.1비율 나타날 때, 이미지의 src에 url값을 넣어주었습니다.
// 이미지 관찰 ref
  const observeImage = useRef(null) 
...
// 관찰시 실행할 함수
  const showImage = ([entry], observer) => {
    if (!entry.isIntersecting) {
      return
    }
    const imageUrl = [entry][0].target.dataset.src //dataset을 통해 data-src를 가지고 오기
    observeImage.current.src = imageUrl // 이미지에 src를 넣어주기
    observer.unobserve(entry.target) // 함수가 실행될 때, 관찰을 끝내기.
}

//옵저버 인스턴스 선언
  useEffect(() => {
      const observer = new IntersectionObserver(showImage, {threshold: 0.1}); //메인이미지 관찰
      observer.observe(observeImage.current)
    return () => {
      observeImage.disconnect();}
    },[])

...
   <Image
    alt="Feed_img"
    data-src={image}
    ref={observeImage}
    onClick={() => {
    goToReviewDetail();
    }}
/>


3)이미지 압축

  • s3의 저장공간을 최적화하고, get 요청시 이미지를 불러오는 속도를 높이기 위해 압축하여 서버에 전송했습니다.
  • 이를 위해 Browser Image Compression 라이브러리를 사용했습니다.
  • 라이브러리는 주간 다운로드수와, 최신 업데이트일을 확인한 후 선정했습니다.

리뷰작성사진

4)번들 최적화 및 파일 최적화

  • 브라우저 성능을 높이기 위해서 공부하는 중, 번들 파일을 잘게 쪼개는 것이 초기 렌더링 속도를 높여준다는 것을 알게 되었습니다.
  • 때문에 코드 스플릿팅을 통해서, 큰 번들을 잘게 쪼개는 작업을 진행했습니다.
  • bundle-analyzer를 통해 번들을 분석했고, 용량이 큰 파일들을 확인 후 개선해나가는 중입니다.
    lottie 라이브러리가 용량이 크다는 것을 확인 후, 더 가벼운 lottie-web-light 버전으로 바꾸었고, lodash의 용량이 큰 것을 확인 후, 해당 라이브러리를 삭제하고 직접 구현 가능한 부분은 직접 구현하도록 하였습니다.
    lottie 개개의 파일이 크다는 것을 확인했고, 압축을 할 수 있는 방법을 찾아 개선해나갈 예정입니다.
성능개선 적용 목록
  • 사용자 업로드 이미지 파일 압축
  • 코드 스플릿팅을 통해 컴포넌트를 레이지 로딩하고, 이를 통해 번들 최적화 실행.
  • 웹폰트 import해오는 것을 삭제 후, 경량화 폰트 다운로드 후 설정 진행중.
  • 이미지 레이지 로딩
  • 무한 스크롤 구현
  • default 이미지 파일들 압축 후, 다시 적용.
  • cloudfront gzip 적용
  • lodash 라이브러리 삭제 후, 기능 직접 구현 (디바운스)
  • 로티 라이브러리 파일 크기가 큰 것을 확인 후, light버전으로 바꾸기.


이런 내용들을 적용한 후에, 3G환경에서 메인페이지 초기 랜더링 속도를 21초에서 6초로 줄일 수 있었습니다.



5)결과

  • 결과적으로, 이미지가 많은 메인 페이지의 랜더링 속도를 3G환경에서 측정했을 때 21초가 걸리던 시간을 6초까지 무려 3.5배 향상시킬 수 있었습니다.
    메인페이지 뿐만 아니라, 다른 페이지에서도 지속적으로 속도를 개선해나갈 계획입니다.
느낀점
프로젝트 진행시, 웹성능을 높이기 위한 시도를 한 것은 이번이 처음이었습니다. 이 시도를 통해서, 느낀점이 많았습니다. 어떤 요소들이 웹성능을 저하시키는지 알게 되었고, 다음부터는 시작단계에서부터 어떤 요소들을 고려해야 웹성능에 최적화될 수 있는지, 조금은 볼 수 있는 눈이 생긴 것 같습니다.


6)React.memo를 활용하여 리렌더링 방지

  • 불필요하게 리렌더링 되는 컴포넌트를 방지하기 위하여 컴포넌트를 세분화하고
  • React.memo를 활용하였습니다.
  • 그 결과, 아래 사진에서 보시는 것과 같이, <전> 사진에서는 불필요한 렌더링이 일어나고 있지만, <후> 사진에서는 불필요한 랜더링이 일어나지 않고 있습니다.


const ChoicedBook = React.memo(()=>{
    return(
      <BookChoice
      onClick={() => {
        dispatch(permitActions.showModal(true));
      }}
    >
      <img src={add_button} alt="add btn" />
      <Text >리뷰할 책 선택하기</Text>
    </BookChoice>
    )
  })


2. 사용자 중심

어떻게 특별한 기능을 넣을까? 보다
'어떻게 하면 UX중심적인 개발을 할 수 있을까?'를 고민했습니다.

1)사용자 게시글 읽음 전송

  • 사용자에게 맞춤형 피드를 구성하기 위해서, 사용자가 해당 게시글을 읽었다는 데이터를 서버에 보내줄 필요가 있었습니다.
    이것을 어떻게 구현할지 고민하던 중, intersection observer를 활용해 화면에 게시물이 나타나면 해당 게시물의 아이디를 서버에 보내주면 되겠다고 생각했습니다.
  • 이것을 구현할 때, 생긴 2가지 어려움이 있었습니다. 1.useRef를 화면의 모든 게시물에 붙이는 방법 2.화면에 나타난 게시물의 아이디 값을 구하는 방법.
  • 1.useRef를 모든 게시물에 붙이는 방법 : 일반적인 방법으로는 반복문을 돌려서 ref를 컴포넌트마다 붙일 수가 없었습니다. 그래서 검색도 해보고 찾아본 결과, useState를 활용하면 ref를 반복문을 통해서 생성할 수 있음을 알게되었습니다. 이것을 아래와 같이 구현하였습니다.
  const ReviewCount = reviewList.length; //리뷰의 갯수
  const [elRefs,setElRefs] = useState([]); //ref를 생성할 useState

  //게시물 하나당 ref를 붙이기 위한 작업
  useEffect(() => {
//의존성 배열을 통해, 리뷰의 갯수가 변화될 때마다 다시 ref를 생성한다. 해당 idx에 ref가 있으면 생성하지 않는다. 
    setElRefs(elRefs => (
      Array(ReviewCount).fill().map((_,i) => elRefs[i] || createRef())
    ))
  },[ReviewCount])
  • 2.화면에 나타난 게시물의 아이디값을 구하는 방법 : 아이디를 구할 방법이 잘 생각나지 않아 고민을 하다, data- 속성을 사용하면 쉽게 구할 수 있겠다는 생각이 들었습니다. 떄문에 아래와 같이 구현하였습니다.
//옵저버가 관찰될 때, 실행할 함수 => 해당 게시물의 아이디 값을 서버에 보내서, 사용자가 이 게시물을 '읽었다'는 것을 체크해주기 
  const sendIsRead = async([entry], observer) => {
    if (!entry.isIntersecting) {
      return
    }
    const showedReviewId = [entry][0].target.dataset.id
    observer.unobserve(entry.target) // 함수가 실행될 때, 관찰을 끝내기.
    dispatch(reviewActions.checkIsRead(showedReviewId)) //관찰한 게시물의 아이디를 보내기
}

...
  useEffect(() => {
  let observer

  //ref요소가 존재하고, 페이지의 로딩이 끝나면 옵저버 인스턴스를 생성하기. 
    if(elRefs[0] && !is_loading){
    // 절반반 읽어도, 게시물 읽음을 보내기 
    observer = new IntersectionObserver(sendIsRead, {threshold: 0.5});
    reviewList.forEach((_, idx) => {
      //리뷰의 갯수만큼 생성된 ref에 옵저버를 붙이기
      observer.observe(elRefs[idx].current)
    });
  }
  //화면을 나갈때 옵저버의 연결을 해제하기. 
  return () => observer?.disconnect();
  },[elRefs])


...
<CartWrapper ref={setRef}  data-id={_id}>
...


2)스크롤 위치 기억


  • 사용자가 다른 페이지로 갔다가 돌아왔을때 보던 게시물로 돌아오는 것이 사용자 경험적으로 편할 것이라고 생각했습니다.
  • 스크롤 위치를 리덕스에 저장했습니다.
  • 스크롤 위치가 바뀔때마다 액션을 불러오는 것이 비효율적이라 생각하여
  • 디바운스를 사용하여 사용자가 스크롤을 멈추었을 시 리덕스에 위치를 저장해두었습니다.

스크롤 위치 저장


  
  let timer;
  const scroll = (e)=>{
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function() {
      dispatch(reviewActions.saveCurrentScroll(e.target.scrollTop))
    }, 500);

  }

3) 책 검색 자동완성

  • 네이버에 저장된 책들을 검색할 수 있습니다.
  • 사용자가 검색어를 입력하면 0.2초후에 서버에 요청을 보냅니다(디바운스 이용).
  • 서버에서 보내주는 책의 제목을 보여줍니다.
  • 책의 제목을 클릭하면 해당 책들이 보여집니다.


  let timer;
  const search = ()=>{
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function() {
      dispatch(searchActions.getSearchBooksSV(text.current.value))
      setAutoComplete(true)
    }, 200);


기능소개/ 실행 화면

좋아요기능 책검색기능 이미지 검색기능 팔로우기반 피드업데이트
북마크 기능 팔로우 기능 팔로우기반 알림기능 북 컬렉션 만들기
해시태그 기반 추천도서 레벨 시스템 기반 칭호관리 소셜 로그인 추천해시태그 기능
댓글 기능(수정,삭제)


dependencies


  • "@material-ui/core": "^4.12.3"
  • "@material-ui/icons": "^4.11.2"
  • "@testing-library/jest-dom": "^5.11.4"
  • "@testing-library/react": "^11.1.0"
  • "@testing-library/user-event": "^12.1.10"
  • "axios": "^0.21.1"
  • "browser-image-compression": "^1.0.14"
  • "connected-react-router": "6.8.0"
  • "global": "^4.4.0"
  • "history": "4.10.1"
  • "immer": "^9.0.5"
  • "jwt-decode": "^3.1.2"
  • "lottie-web-light": "^1.1.0"
  • "react": "^17.0.2"
  • "react-dom": "^17.0.2"
  • "react-ga": "^3.3.0"
  • "react-intersection-observer": "^8.32.0"
  • "react-kakao-login": "^2.1.0"
  • "react-redux": "^7.2.4"
  • "react-router-dom": "^5.2.0"
  • "react-scripts": "^4.0.3"
  • "redux": "^4.1.0"
  • "redux-actions": "^2.6.5"
  • "redux-logger": "^3.0.6"
  • "redux-thunk": "^2.3.0"
  • "sass": "^1.37.5"
  • "socket.io-client": "^4.1.3"
  • "styled-components": "^5.3.0"
  • "swiper": "^6.8.1"
  • "web-vitals": "^1.0.1"

About


Languages

Language:JavaScript 97.9%Language:CSS 1.6%Language:HTML 0.4%