교육/원티드 프리온보딩 인턴십

[원티드 프리온보딩 프론트엔드 인턴십] Week 3-1. React Hook의 심층 활용 - 강의 후기

알파카털파카 2023. 1. 8. 23:23

Week 3-1. React Hook의 심층 활용

의존성 배열 / useEffect / React.memo / useCallback / useMemo / Context API

 

 

2주차 강의는 원티드 커리어 챌린지 로 대체되었다. 

커리어 챌린지는 2주 동안 진행된 전 직군 대상 강의였는데, 프리온보딩 코스와 맞추어 개발(프론트엔드) 파트를 들을 수 있게 진행됐다.

커리어 챌린지 강의 후기는 추후 다른 카테고리에서 작성할 예정이다.

3-1 강의 시간에는 1주차 팀별 미션 리뷰 시간을 갖고, 리액트 훅에 대해 배웠다.

 

 


 

 

1. 과제 리뷰

총 12개의 조에서 제출한 과제를 보고 개선하면 좋을 점을 알려주셨다.

이렇게 작성하면 안 된다든지 하는 피드백도 있었다.

강의를 듣다가 깜짝 놀랐는데, 처음으로 등장한 코드가 바로 우리 조, 그것도 내가 작성한 코드였기 때문이다...

코드에 이름이 써 있는 것은 아니지만 그래도 나는 알아볼 수 있었고 

굉장히 민망하고 부끄러웠다.......

정신을 차리고 다시 강의를 집중해서 들었는데 1:1 코칭 같은 느낌도 들고 오히려 이 부분은 잊어버리지 않게 될 것 같아 좋았다.

 

내가 작성한 코드는 팀원 분이 작성하신 로그인/회원가입 페이지에서

로그인/회원가입 모드를 전환할 수 있도록 하는 코드였다.

⬆️위가 나의 코드고,⬇️아래가 멘토님이 고쳐주신 코드다.

요지는 상수화를 활용해 가독성을 높이고, 유지보수성 향상시키자 는 것이다.

 

type RegisterMode = 'signUp' | 'signIn';

const Register = () => {
  const navigate = useNavigate();
  const [mode, setMode] = useState<RegisterMode>('sign_in');

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
  }

  const handleClickRegisterChange = () => {
    if (mode === 'signIn') {
      setMode('signUp');
      return;
    }
    setMode('signIn');
  }

  useEffect(() => {
    if (getAccessToken()) {
      navigate('/todo');
    }
  }, [navigate]);

  return (
    <Container>
      {mode === 'signUp' ? <Signup /> : <Signin />}
      <Button onClick={handleClickRegisterChange}>
        {
          mode === 'signIn' ? '회원가입 하러가기' : '로그인 하러가기'
        }
      </Button>
    </Container>
  )
}

 

const SIGN_IN = "SIGN_IN";
const SIGN_UP = "SIGN_UP";

type RegisterMode = typeof SIGN_IN | typeof SIGN_UP

const Register = () => {
  const navigate = useNavigate();
  const [mode, setMode] = useState<RegisterMode>(SIGN_IN);

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
  }

  const handleClickRegisterChange = () => {
    if (mode === SIGN_IN) {
      setMode(SIGN_UP);
      return;
    }
    setMode(SIGN_IN);
  }

  useEffect(() => {
    if (getAccessToken()) {
      navigate('/todo');
    }
  }, [navigate]);

  return (
    <Container>
      {mode === SIGN_UP ? <Signup /> : <Signin />}
      <Button onClick={handleClickRegisterChange}>
        {
          mode === SIGN_IN ? '회원가입 하러가기' : '로그인 하러가기'
        }
      </Button>
    </Container>
  )
}

export default Register;

 

삼항연산자와 조건문의 차이를 파악해서 상황에 맞게 사용하는 방법도 알려주셨다.

  • 값이 필요할 땐 삼항연산자 
  • 코드의 분기가 필요할 땐 조건문

 

이 외에도 아래의 사항을 참여 조의 코드를 예시로 들어 보여주셨다.

  • 파일 구조를 이해하고 있어야 함
    • 불필요 파일 삭제
    • 사용하지 않는 변수, import 제거
  • 상수화를 활용한 가독성, 유지보수성 향상
    • 상수는 항상 snake_case, uppercase
  • 삼항연산자와 조건문의 차이 파악해서 상황에 맞게 사용하기
    • 값이 필요할 땐 삼항연산자
    • 코드의 분기가 필요할 땐 조건문
  • 변수명
    • 세부 구현과 나타내고 싶은 값 구분
  • state의 최소집합 찾기
    • 불필요한 state가 있으면 안 됨
    • 리액트는 state가 변경되면 리렌더가 일어남
  • async await 동작 이해하고 사용하기
    • promise 처리 과정을 문법적으로 편하게 표현하기 위한 문법
    • async return → promise
    • await → promise resolve or reject
    • 무의미한 catch 활용 자제
    • 상황마다, 관심사마다 try, catch로 에러 처리를 하면 좋음 (console.log(e), dispatch({…)} 등)
  • 일관적인 포맷팅, 공백 적절하게 사용
  • 코드 작성 순서 주의 : 상수 선언, css 등
  • 스타일링 방식 혼용 지양

 

 

 

 

2. React 렌더링 최적화 & Advanced Hook

렌더링

 

렌더링이란, 화면에 특정한 요소를 그려내는 것이다.

리액트에서 렌더링이란, DOM요소를 계산하고 그려내는 것이다.

 

바닐라 JS에서는 DOM에 직접 접근하고 수정하는 명령형이고,

리액트, 뷰, 앵귤러에서는 ui를 선언하기만 하면 알아서 ui를 그려내도록 프레임워크를 이용한 선언형이다.

선언형은 화면이 어떻게 생길지 알 수 있어서 디버깅이 쉽다.

 

리액트에서 리렌더링이 되는 시점

state : ui와 연동되어야 하고, 변할 여지가 있는 데이터

데이터가 변경됐을 때 ui가 맞춰서 변화하기 위해 state변경 방법을 제한시킴(setState) & 이 함수가 호출될 때마다 리렌더링 되도록 설계

 

💡 state가 변하면 해당 컴포넌트를 포함한 하위 컴포넌트들은 모두 리렌더링 된다.

 

 

 

리액트의 렌더링 과정

 

리액트를 사용하는 개발자가 할 수 있는 최적화는

 

1. 기존 컴포넌트의 UI를 재사용할 지 확인한다.

2. 함수 컴포넌트: 컴포넌트 함수를 호출, 이 결과를 통해서 새로운 VirtualDOM을 생성한다.

 

UI가 실질적으로 변화 되었는지 안 되었는지를 매번 리액트가 렌더링 과정에서 일일이 모든 컴포넌트 트리를 순회하면서 검사하는 것은 비효율적이다.

따라서 리액트에서는 개발자에게 이 컴포넌트가 리렌더링이 되어야 할지 아닐지에 대한 여부를 표현할 수 있는 React.memo 함수를 제공하고 이를 통해 기존의 컴포넌트의 UI를 재사용할 지 판단하는 방법을 채택했다.

(재사용하려면 react.memo 사용)

 

memo는 함수이며,

컴포넌트를 인자로 받고 리턴값도 컴포넌트이다.

이런것을 고차 컴포넌트, HOC(Higher Order Component)라고 부른다.

 

 

 

 

자바스크립트 데이터 타입

 

원시형 : 다른 데이터 없이 해당 데이터 스스로 온전히 존재할 수 있는 형태, string, number, boolean, undefined, null 등

참조형 : 다른 데이터를 모아서 만들어진 타입

 

두 가지를 생각할 때 가장 큰 특징은 불변성

원시형 타입은 모두 불변하며 변경이 불가하다. ➡️ 불변성 : 비교가 쉬움

참조형 타입은 가변적으로 언제든, 어떤 형태로든 변경 가능하다. ➡️ 가변성 : 메모리 절약 가능, 결과 예상 힘듦, 객체간 비교가 어려움

 

React.memo는 기본적으로 props의 변화를 이전 props와 새로운 props를 shallow compare

 

 

 

Memoization

 

메모이제이션 : 특정한 값을 저장해뒀다가, 이후에 해당 값이 필요할 때 새롭게 계산해서 사용하는게 아니라 저장해둔 값을 활용하는 테크닉

리액트에서는 함수 컴포넌트에서 값을 memoization 할 수 있도록 useMemo, useCallback 등의 API(메소드)를 제공한다.

 

useMemo

useMemo는 리액트에서 을 memoization 할 수 있도록 해주는 함수이다.

주의점으로는 useMemo에서는 의존성 배열을 인자로 받아, 의존성 배열에 있는 값 중 하나라도 이전 렌더링과 비교했을 때 변경되었다면,

메모된 값을 활용하는 것이 아니라 새로운 값을 다시 계산한다는 것이다.

 

 

useCallback

useMemo를 조금 더 편리하게 사용할 수 있도록 만든 버전이다.

함수를 memoization 할 수 있도록 해주는 함수이며, 의존성 배열의 동작은 동일하다.

 

 

언제 memoization을 해야 할까?

저장해두고 필요할 때 꺼내서 쓴다 ⇒ 효율적일 것 같다고 생각되지만, ⚠️ 무조건 쓰는게 좋은 것은 아니다.

 

새로운 값을 만드는 것과 어딘가에 이전의 값을 저장해두고 메모이제이션 함수를 호출하고, 의존성을 비교해서 가져올지 말지 여부를 판단하는 것 중 어떤 것이 비용이 더 적게 들까?

💡 새로운 값을 만드는 과정이 복잡하지 않다면, 메모이제이션을 사용하는 것은 오히려 비용이 더 많이 들 수도 있다.

 

어떤 상황에서 사용할까?

  1. 새로운 값을 만드는 연산이 복잡하다.
  2. 함수 컴포넌트의 이전 호출과, 다음 호출 간 사용하는 값의 동일성을 보장하고 싶다.

2의 경우는 함수 컴포넌트의 호출 간 값들의 동일성을 보장하기 위해서인데,

동일성을 보장해야 하는 이유는 React.memo 와 연동해서 사용하기 위해서이다.

useNavigate, useState등도 내부적으로 메모이제이션 되어있다.

 

 

 

최적화를 언제 해야할까?

 

대전제는 최적화는 공짜가 아니라는 것 ⚠️

최적화를 위한 코드가 추가되면 → 프로젝트 복잡도가 증가하고 → 개발자의 시간과 노력 투입되어야 한다.

  • 현업 개발자는 가치창출이 필요
  • 가치 창출하지 못하면 좋은 평가를 받지 못함

 

따라서 최적화가 명확한 가치창출을 할 것이 기대되는 상황에서 수행하는 것이 좋다.(성능 이슈, 버그픽스 등)

최적화에 대한 이유 분석과 동료의 공감대를 형성 후에 진행해야 한다.

무작정 개선해보겠다고 최적화에 뛰어드는 것은 위험성이 있다.

 

 

 

 

3. useEffect & Context API

useEffect의 의존성 배열

 

의존성 배열이란?

  • useEffect에 두번째 인자로 넘기는 배열
  • 두번째 인자를 넘기지 않으면 Effect는 매번 실행되고, 빈 배열을 넘긴다면 컴포넌트의 첫번째 렌더링 이후에만 실행
useEffect(effect, 의존성)

 

 

여기에서 그치면 안 되고, 그 외에도 의존성 배열에 대해 꼭 알아야 할 것이 있다.

useEffect의 의존성 배열은 effect 함수가 의존하고 있는 요소들의 모음이다.

useEffect는 리렌더링이 된 후 의존성 배열을 검사해서 의존성 배열에 있는 값이 변경되었을 경우에 다시 새로운 의존성을 가지고 effect를 실행시켜 준다.

 

 

useEffect 의존성 배열을 잘 설정하는 법

useEffect에서 버그가 발생하지 않게 의존성 배열을 잘 설정하는 방법

❗️모든 의존성을 빼먹지 말고 의존성 배열에 명시해라

가능하다면 의존성을 적게 만들어라

 

 

무한루프 해결 방법

  1. 의존성을 제거하기 ⇒ 함수를 effect 안에 선언하기
  2. 함수를 컴포넌트 바깥으로 이동시키기
  3. 메모이제이션

 

 

 

Context API

 

Context API는 React에서 제공하는 내장 API이다.

컴포넌트들에게 동일한 Context(맥락)을 전달하는데 사용한다.

 

⚠️ 프롭스 드릴링을 피하기 위해 사용하는 것이지, 전역 상태관리가 아니다!

여러 컴포넌트에 동일한 값을 접근할 수 있도록 만들어주는 api (통로라는 개념)

자주 변경되는 데이터를 관리하려면 용도에 맞게 구분해서 사용하면 된다.

 

리액트에서 데이터를 전달하는 기본 원칙은 단방향성이다.

부모 컴포넌트에서 자식 컴포넌트 방향으로만 데이터를 전달할 수 있다. (위에서 아래로)

단방향성은 애플리케이션의 안전성을 높이고 흐름을 단순화하는데 유용하지만 단점도 존재한다.

 

단방향성의 단점, Prop Drilling

  • 너무 많은 단계를 거쳐서 자식 컴포넌트에 데이터를 전달해야 한다는 문제
  • 중간 컴포넌트는 해당 데이터를 사용하지 않을지라도 props를 계속해서 넘겨줘야하는 문제
  • 형제 관계나 특정 범위 안에 있는 컴포넌트들에게 데이터를 넘기기 위해서는 더 복잡한 상황 발생

 

이를 해결하기 위해 Context API를 사용하면 좋다.

 

 


 

 

리액트를 사용하면서 별 생각 없이 '원래 그런가보다' 하고 지나칠 수 있는 내용을 구체적으로 다루어 유익한 시간이었다.

특히 의존성 배열은 처음 접했을 때 어려움을 느꼈던 부분이었다.

Context API는 사용해본 적은 거의 없지만 mobx를 쓸 때와 구조가 비슷해서 이해가 됐다.