[리팩토링] - JinCha 프로젝트 뜯어 고치기

😧 갑자기?

맞다. 리팩토링은 처음이다. 프로젝트가 끝날 때마다 리팩토링은 꼭 해야지 하고 있다가 이제 와서야 시작했다. 

프로젝트 선정은 간단했다. 첫 리팩토링인 점을 고려해 수정할게 많아보이는 프로젝트로 선정했다. 

진챠 프로젝트 회고록

 

또한 이전에 약속한게 있으니 약속도 지킬겸 겸사겸사 시작했다.

지표에서는 드라마틱한 효과가 있었던 것은 아니었다. 하지만 최적화를 진행하며 겪은 내용들을 까먹지 않기 위해 기록 할 것이다. 

 

리팩토링 할 부분

목표는 성능 최적화다. 이것을 지표로 확인 할 수 있다면 좋겠다고 생각해서 Lighthouse의 성능 지표를 최대한 끌어올리는 것을 목표로 하였다. 

 

내가 가장 바꾸고 싶은 지표는 Performance, Accessible, SEO 총 3가지다. 

 

1. React.lazy()

렌더링을 최적화하기 위해 이리저리 찾아보다 발견했다. React.lazy의 가장 큰 장점은 "지연 로딩" 이다. 

나는 궁금했다.

"어라? 로딩이 지연되면 더 않좋은거 아닌가?"

 

리액트는 대표적인 SPA(Single Page Application)이다. 그래서 첫 로딩을 할 때 브라우저는 JS에 포함된 모든 코드를 읽고 준비가 다 된 상태에서 렌더링을 한다. 그래서 첫 로딩 시에는 좀 시간이 걸리겠지만 이후 페이지를 전환할 때 로딩이 없다는 장점이 있다. 하지만 UX 측면에서는 첫 로딩이 오래거리는 것은 치명적이다. 1초가 늘어날 때마다 페이지 이탈률은 엄청나게 올라간다. 그래서 사용하는게 React.lazy 이다.

 

React.lazy는 동적으로  컴포넌트를 렌더링 할 수 있다. 이게 무슨 말이냐면, 현재 사용자가 필요로 하는 컴포넌트와 필요하지 않는 컴포넌트를 동적으로 추가하거나 삭제할 수 있다는 뜻이다. 이렇게 되면 첫 로딩시 필요하지 않는 컴포넌트들은 렌더링 하지 않을 수 있기 때문에 그만큼 로딩 시간이 줄어들게 된다. 

그럼 React.lazy는 어디에 작성해야 할까? React 공식 문서에는 Route부분에서 적용시키길 권장하고 있다. 개발자가 직접 사용자 경험을 해치지 않으면서 코드 분할 하기에는 쉽지 않기 때문이다. 

 

나의 코드

import React, { Suspense, lazy } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

// React.lazy 적용
const MainPage = lazy(() => import("../pages/MainPage"));
const LoginPage = lazy(() => import("../pages/LoginPage"));
const SignupPage = lazy(() => import("../pages/SignupPage"));
const MovieDetail = lazy(() => import("../pages/MovieDetail"));
const MovieListCounty = lazy(() => import("../pages/MovieListCounty"));

export default function Router() {
  return (
    <>
      <BrowserRouter>
        <Suspense>
          <Routes>
            <Route path="/" element={<MainPage />} />
            <Route path="login" element={<LoginPage />} />
            <Route path="signup" element={<SignupPage />} />
            <Route path="/movie/:movieName/:openDate" element={<MovieDetail />} />
            <Route path="/movieList" element={<MovieListCounty />} /> {/* 국가별 영화 페이지 */}
          </Routes>
        </Suspense>
      </BrowserRouter>
    </>
  );
}

 

적용시키는 것은 어렵지 않았다. 각각의 주소를 import로 받아와서 lazy에 감싸주면 된다. 

 

결과

생각보다 큰 차이는 없었다. 아무래도 규모가 큰 프로젝트가 아니다 보니 렌더링 속도나 Lighthouse의 Performance 지표면에서 유의미한 차이는 없었다. 그리고 첫 렌더링 시에 모든 코드를 불러오지 않기 때문에 페이지 이동을 할 때 약간의 로딩이 걸린다는 단점이 있었다. 이것또한 프로젝트 규모를 생각해서 다음엔 사용할지 안할지 고려해보면 좋을 것 같다.

 

2. Custom Hook

리액트의 꽃이라고 표현되는 custom hook이다.

내 코드를 보면 중복되는 로직이 많았다. 좀만 더 생각하면 잘 묶을 수 있을 거 같은데 프로젝트 기간상 어쩔 수 없이 그냥 제출했다. 이번엔 이 코드를 Custom-hook을 이용해 어떻게 줄였는지에 대해 설명해 보겠다. 

먼저 대략적인 로직은 아래와 같다.

 

API 요청은 두번을 한다. 

A api : 영화 정보는 다양하게 많지만 포스터 이미지가 없음

B api : 영화 정보는 부족하지만 포스터 이미지는 있음

1. A api에 axios 요청을 한 후 데이터를 movieList(변수)에 담는다.
2. 곧바로 B api에 axios 요청을 한다. (이때 movieList에 담긴 데이터 중 "영화 제목"과 "개봉일"을 B api 요청인자로      포함시킨다.)
3. B api에서는 포스터 이미지만 필요하기 때문에 리턴값으로 poster url을 리턴한다. 
4. movieList에 Poster url을 추가한다.

 

첫번째 리팩토링

- 일별/주간 박스오피스에서 영화 정보와 포스터를 같이 불러오는 axios를 하나의 hook으로 만듬

- 일별/주간 박스오피스를 구분하기 위해서 훅으로 값을 넘길 때 isDaily라는 인자를 같이 넘기도록 유도함.

두번째 리팩토링

- 국가별 영화 리스트도 같은 로직이기 때문에 custom hook을 같이 진행함.

- isDaily는 일별/주간 박스오피스 밖에 구분을 못하기 때문에 커스텀 훅으로 보내는 인자를 다시 구성함.

- isDaily 대신 url과 어떤 영화 리스트인지 구분하는 문자열을 같이 보냄.

- custom hook에서는 switch case문을 이용해 조건을 나눠서 영화 리스트를 추출하고 리덕스에 포스터 정보까지 저장.

 

결과

 

약 150줄의 불필요한 코드를 제거할 수 있었다. custom hook을 만들면서 코드 최적화를 처음 해봤는데 단순히 제작을 할 때보다 머리를 더 많이 쓴 느낌이 들었다. 코드를 어디서부터 어디까지 묶어서 custom hook을 만들지 고민하는 과정도 꽤 생각을 많이 했다. 영화 정보를 가져오는 axios만 쪼갤지 아님 포스터 url까지 모두 가져오는 과정을 한번에 묶을지 등등 고민을 했다. 하지만 이전보다 반복되는 코드가 줄었고 Lighthouse의 Performance도 1점 올라 행복했다. 앞으로 많이 써야 겠다고 생각했다.

 

 

3. 이미지 최적화

Lighthouse를 분석하면서 가장 상단에 많이 뜬 것이 이미지와 관련된 내용이었다. 

사이트 특성상 이미지를 많이 요청하고 불러와야 하기 때문에 그런것 같았다. 나도 이점을 알고 이미지 최적화에 대해 알아보았다. 방법은 아래와 같았다.

 

1. png,jpg 같은 확장자 대신 webp로 바꿔서 사용을 하면 이미지 용량이 현저하게 줄어든다.

2. css의 background-image 대신 HTML의 <img> 태그를 사용하자.

3. 중요 이미지는 fetchpriority로 빠르게 로드하자.

   - LCP의 일부로 첫 로드 시에 가져와야 할 특정 이미지가 있다고 가정하자. 브라우저는 다른 이미지들 보다 이 이미지를       우선적으로 처리한다.

4. 지연로딩(lazy loading)과 async decoding으로 성능 높이기

   - 둘다 LCP이미지가 아닌 경우에 사용할 수 있다. 중요 이미지가 아닌경우 LCP의 성능을 높이기 위해서 뷰 포트에 없는       이미지의 로딩을 좀 늦추는 방법이다. 

5. S3에 이미지를 저장 후 사용하기

 

나는 여기서 1,5번 빼고는 다 활용을 해봤다. 이미지를 모두 img 태그로 적용시켜준 후 fetchpriority를 적용시키기 위해 가장 중요하다고 생각하는 img 태그에 적용시켰다. 나는 첫 로딩시 메인화면에 나오는 박스오피스 이미지에 적용을 시켰다. 

lazy loading과 async decoding은 하단의 "나라별 추천 영화" 박스 이미지에 적용을 시켰는데, 생각해보니 이 이미지들은 우선순위가 좀 밀릴 뿐이지 뷰 포트안에 포함되어있기 때문에 다시 삭제 했다. 

 

위의 내용들을 수정해도 로딩 개선에 큰 영향은 없었다. 아무래도 API 요청시 불러와지는 img의 용량이 큰게 가장 큰 문제였는데, 이미지의 확장자를 변경할 수는 없었다. 제공되는 API에서는 webp의 형식을 허용하지 않았기 때문이다. 따라서 이미지 최적화는 여기서 마무리 하기로 결정했다.

 

4. Skeleton UI

Skeleton ui는 성능 최적화 면에서 좋을 뿐 아니라 UX 측면에서도 많이 사용된다. 

기존 메인 페이지에 있는 케러셀은 데이터가 로드되는 동안 공백으로 남아 있었다. 심지어 axios 호출이 2번 일어나기 때문에 로딩 시간이 특히 더 길어서 공백시간이 길었다. 로딩이 다 완료 되면 이미지가 채워져 화면 레이아웃이 변경되는 현상이 발생했다. 이를 Layout Shift라고 하는데 페이지와 상호 작용을 시도하는 동안 콘텐츠가 이동할 수 있으므로 사용자에게 방해가 될 수 있다. 그래서 나는 이를 해결하기 위해 Skeleton ui를 사용하기로 결정했다.

 

Skeleton ui를 만들기 위해 여러 라이브러리가 있었지만 나는 라이브러리 사용을 최대한 줄이기 위해 직접 만들었다. 

만들기 위한 방법은 아래와 같다.

 

1. Skeleton ui를 만들고 싶은 자리의 HTML과 CSS를 똑같이 만든다.

2. Axios가 호출되는 동안만 나오도록 위치를 지정해준다.

 

 return (
    <>
      {todayMovieList.length === 0 && isLoading === true ? (
        <Carousel />   	// 로딩중 일 때
      ) : (
        <_customSwiper/>
      )
    </>
)

 

여기서 isLoading은 Axios 호출 중인지 확인하는 상태 값이고 todayMovieList는 일별 박스오피스 정보가 전역 변수에 저장되 있는지 아닌지 확인해주는 값이다.

 

5. 중복 코드 삭제

나는 국가별 영화 페이지를 각각 만들었었다. 이것도 거의 중복 코드가 대부분이었기 때문에 하나의 페이지에서 처리하도록 만들어 봤다. 이 컴포넌트는 아래와 같이 동작했다. 

 

1. 해당 국가의 영화 리스트를 api 요청한다. 

2. 전역 상태에 저장 후 영화 리스트를 화면에 뿌려준다.

 

먼저 각각 만들었던 router를 하나의 router로 수정해야 했다. 그래서 나는 useSearchParams를 사용하기로 했다. 

 

수정 전 코드
/KoreaMovie 

수정 후 코드
/movieList?country=Korea

 

쿼리스트링 방식을 사용해 value값에 따라 다르게 반응해줬다. value값에 따라 custom hook의 인자를 다르게 줘서 영화 리스트를 호출했다. 

 

그 결과 약 380줄의 코드를 줄일 수 있었다.

 

 

리팩토링 후

처음 해보는 과정이라 그런지 별거 아닌 최적화를 하는데도 시간이 꽤 오래걸렸다. 한번 최적화를 할 때마다 Lighthouse를 한번 돌리고 결과 보고를 반복했다. Lighthouse가 정확한 지표가 아니라는 것을 알지만 성능이 올라가는 모습을 볼 때마다 맞게 하고 있다고 생각이 들어 성취감이 있었다. 아직 하고 싶은 최적화 작업이 또 있다. 하지만 1차 리팩토링은 여기서 끝내고 다음에 시간있을 때 또 시도를 해보겠다. 아래는 최적화 후 달라진 결과다.


코드 : 약 -500줄

LCP : -1.4초

Performance : +10

SEO : +18