[React Performance Optimization] Login

김병진·2021년 3월 24일
0

들어가기 전에

스마트팜 웹사이트를 올해 크게 개편했습니다. 개편 내용은 아래와 같습니다.

BeforeAfter
LanguageJavascriptTypescript
FrontendReact, ReduxReact, Redux
BackendExpressNestJS

피상적으로 위와 같이 바뀌었지만 내부적으로 Mysql schema, Api 등 많이 바뀌었습니다.
Mysql도 다른 Nosql로 바꾸려 했지만 아래와 같은 이유로 바꾸지 않았습니다.

  1. 엄격한 스키마가 필요합니다.
    • 기기들이 서로 통신하고 데이터를 주고 받는 IoT 특성상 맞지 않는 구조의 데이터를 가지고 데이터베이스에 접근하면 오류가 일어나야 잠재적 문제를 방지할 수 있습니다.
  2. 관계가 필요합니다.
    • 어떤 유저가 어떤 기기를 작동했는지, 어떤 센서가 데이터를 저장했는지 등 모든 데이터가 서로 관계를 맺고 있습니다.

그렇다고 Mysql이 만능이 아닌 건 인정할 수 밖에 없습니다. json과 array 타입의 데이터를 저장할 수 없어 JSON.stringify을 이용해 문자열로 바꿔 저장할 수 밖에 없었습니다. 아직 이에 대한 해결책을 생각해내지 못했습니다. 아직 저가 Nosql을 제대로 공부하지 않아서 생긴 문제일 수 있습니다. 그래서 다음 프로젝트는 Nosql을 이용하여 진행해볼까 합니다.

이제 저가 개편한 내용을 정리하고자 합니다. 각 컴포넌트를 프로파일링 해보고 속도나 코드의 간결함 등을 정리해볼 계획입니다.

Login Component

위와 같은 로그인 컴포넌트를 가지고 이름과 비밀번호를 작성하는 과정으로 테스트를 진행해보겠습니다.(gif 프로그램이 box-shaow 때문인지 화면을 잘 인식하지 못하고 있습니다.)

RESULT


* 최적화 전 프로파일링


* 최적화 후 프로파일링


위 두 프로파일링은 0.1ms 시간 이하는 모두 제외하였습니다. 두 프로파일링 성능을 아래의 표를 이용해 비교해보도록 하겠습니다.

BeforeAfterComp.
Commits14 steps6 stepsx0.42
Largest Time7ms0.5msx14

성능 차이는 엄청 납니다. 커밋은 14개에서 6개로 반 이상 줄었고, 가장 오랜 시간 렌더링한 커밋을 비교해보면 커밋 렌더링 시간은 14배 차이가 납니다.

HOW

  1. Component Structure
  2. React.memo & useCallback
  3. Remove unecessary useState

WHY

  1. Component Structure

우선 로그인 컴포넌트의 구조에 대해 알아보겠습니다.

  • Before Component Structure
|___ View
    |___ Login
    |___ CustomDialog
  • After Component Structure
|___ View.tsx
    |___ LoginInput
    |___ Login
    |___ CustomDialog

이전 컴포넌트 구조는 구글링하면 나오는 기본 예제 중 하나였습니다. 간단한 변화지만 성능이 꽤 좋게 향상됐습니다. 단지 usename input과 password input을 login component와 독립시켰을 뿐입니다. 최소한의 렌더링을 위해 구조를 많이 바꿨습니다. 리렌더링을 최소화하기 위해 아래의 리렌더링 조건을 꼭 염두에 두고 구조를 짜면 좋을 듯 합니다.

  • Props의 변경
  • State의 변경
  • forceUpdate()
  • 부모 컴포넌트의 렌더링
    ...

이전 구조는 로그인 뷰에 다 때려박아 넣어놨기 때문에 이름 및 비밀번호 input에 글자 하나만 추가해도 로그인 뷰 전체가 리렌더링 되는 구조였습니다. 뷰 전체가 리렌더링된다는 것은 inputs부터 customDialog까지 모두 리렌더링 되고 있었습니다. 개편 이후, 이름과 비밀번호 작성 시 result gif에서 봤듯이 input만 리렌더링 되고 나머지는 리렌더링 되지 않고 있습니다.

  1. React.memo & useCallback

customDialog 처럼 특정 조건에만 렌더링 되는 경우에 React.memo와 useCallback을 사용하면 좋습니다. React.memo는 자식 컴포넌트를 래핑하여 들어오는 props를 기억하고 props가 바뀌는 경우만 리렌더링합니다. useCallback은 부모 컴포넌트에서 자식 컴포넌트로 보내는 함수에 적용하여 재사용할 수 있도록 합니다. 자세한 사항은 React.memo & useCallback로 가시면 조금 더 깊게 이해하실 수 있습니다.

Login.tsx 컴포넌트에 아래와 같이 작성 가능합니다.

const handleDialogClose = useCallback(() => {
    setOpen(false);
  }, []);
  ...생략
  <CustomDialog
        open={open}
        handleClose={handleDialogClose}
        title={Errors.SIGNIN_FAILURE_TITLE}
        description={Errors.SIGNIN_FAILURE_DESC} />

위와 같이 deps에 빈 배열을 두게 되면 첫 마운트에 선언되고 리렌더링되어도 재선언되지 않습니다. CustomDialog.tsx 에서 는 아래와 같이 React.memo로 래핑하여 함수를 재사용할 수 있습니다.

export default React.memo(CustomDialog);
  1. Remove unecessary useState

우리는 너무 습관적으로 useState를 사용하는 것 같습니다. useState를 이용하면 scope가 const나 let보다 넓어 편리하다는 장점(이 점은 단점이 될 수 있습니다.)도 있지만 state가 변경될 때마다 리렌더링을 해야한다는 단점이 있습니다. 위와 같이 LoginInput을 Login 컴포넌트 밖으로 빼놓은 상황이라면, 부모 컴포넌트에서 굳이 자식 컴포넌트에게 내려주는 변수를 state로 관리할 필요가 없다고 생각합니다. 그 결과 아래와 같은 코드로 작성 가능합니다.

Before

const [login, setLogin] = React.useState({
        name: "",
        pw: ""
    });

const handleChange = target => (e) => {
  setLogin({ ...login, [target]: e.target.value })
}

After

interface LogIn {
  username: string;
  password: string;
}

const login: LogIn = { username: '', password: '' } as LogIn;

const handleLoginChange = <T extends keyof LogIn> (type: T) => 
  <U extends BaseSyntheticEvent> (e: U): void => {
    login[type] = e.target.value
  }

Conclusion

첫 게시글로는 로그인 컴포넌트의 성능 최적화에 대해 알아보았습니다. 성능 최적화를 통해 얼마나 큰 성능 향상이 있었는지, 어떻게 했는지 등을 알아보았습니다. 다음 번에는 또 다른 컴포넌트로 성능을 최적화해보겠습니다.

profile
아이디어를 구현할 수 있는 개발자가 목표입니다.

0개의 댓글