선언적으로 프로그래밍

김윤진·2022년 10월 6일
1

React

목록 보기
12/13

레벨로그 프로젝트

보이는 화면


도입전

일단 코드를 한번 보자.

const 페이지컴포넌트 = () => {
   const {
    state: {
      상태1,
      상태2,
      상태3,
      상태4,
    },
    ref: { 
      // ref...
    },
    handler: {
      // handler 함수...
    },
  } = 페이지커스텀훅();
  
  if (상태1이 없다면) {
  	return <Loading />   
  }
  
  return(
    // UI 로직...
  )
}

컴포넌트의 구조는 State Waterful방식으로 최상단 페이지 컴포넌트에서 모든 API 요청을 응답받고 그에 대한 상태로 화면을 그려주는 방식이다. 즉, 최상단 컴포넌트인 페이지 컴포넌트에서 모든 상태를 관리하고 하위 컴포넌트로 내려주는 구조이다.
이 구조는 장단점이 존재한다.

  • 장점은 한 페이지에서의 모든 상태를 한 곳에서 관리하기에 상태에 변경이 생길 경우 해당하는 커스텀 훅을 찾아가면 된다. 이로 인해 파일을 찾는데에 들이는 시간을 단축할 수 있다.

또한 단점도 존재한다.

  • 해당하는 페이지에서 보여줘야하는 상태가 늘어난다면 커스텀 훅의 크기가 점차 비대해진다.
  • 최상단에서 모든 상태를 관리하기에 불필요한 props 전달이 필요하게 된다. 최상단에서 필요없는 상태들도 가지고 있기 발생하는 문제이다. 이는 컴포넌트의 제 역할을 하지 못하게 한다. 컴포넌트에 종속된 상태인데 props로 받아서 보여주기 때문이다. 이로 인해 페이지컴포넌트에 결합도가 높은 컴포넌트가 생기며 재사용하기 곤란해진다.
  • 이 상태들이 API 요청을 통해 결정되는 상태라면 또 다른 문제가 발생한다. 바로 React의 비교적 신기능인 Suspense를 활용하지 못한다. Suspense의 children이 promise가 pending 상태면 fallback을 렌더링하고 promise가 fulfilled되면 children을 다시 렌더링한다. 그런데 children에서는 promise를 실행하지 않기에 Suspense가 동작하지 않는다. 그렇게 되면 선언적으로 코드를 작성하기 어렵다. loading 상태와 error 상태는 useState를 통해 정의하고 if문으로 분기처리하여 loading, error 상태에 따라 화면을 보여줘야 하기 때문이다. Suspense의 내부 코드를 살펴보면 if문으로 분기처리를 하면서 상태를 throw한다. 결국 선언적인 코드는 명령적인 코드를 추상화한 것이다. 그래서 Suspense 대신해서 loading, error 처리하는 컴포넌트로 children(화면에 보이는 컴포넌트)를 감싸주어도 Suspense와 비스무리하게 동작할 수 있다.
const 페이지컴포넌트 = () => {
  const {
    state: {
      상태1로딩,
      상태2로딩,
      상태3로딩,
      상태1,
      상태2,
      상태3,
    },
    ref: { 
      // ref...
    },
    handler: {
      // handler 함수...
    },
  } = 페이지커스텀훅();

  return(
    <CustomSuspense 
      상태1로딩={상태1로딩} 
      상태2로딩={상태2로딩}
      상태3로딩={상태3로딩}
    >
	// UI 로직...
    </CustomSuspense>
  )

이처럼 loading과 error를 상태로 만들고 CustomSuspense라는 컴포넌트에 props로 상태를 넣어주면 Suspense와 비스무리하게 동작한다. 그러나 이 또한 세가지 문제가 남아있다.

  • 바로 모든 API 요청이 하나의 Suspense에 종속되기에 모든 API 요청이 끝나야 children을 렌더링하는 문제가 있다. 그렇게 되면 페이지에서 하나의 컨텐츠 API 요청에서 병목이 걸리거나 error가 발생하면 전체 페이지를 볼 수 없다는 것이다.
  • API 요청에 상태가 새로 생길 때마다 loadingerror 상태를 만들어줘야하는 문제가 발생한다. 커스텀 훅에서 CustomSuspense으로 props 또한 넘겨줘야 한다.
  • CustomSuspense에서 loading컴포넌트를 띄워주고 API 요청이 성공해 loading상태가 false가 되었다면 그제서야 children을 그려주는 문제가 있다. 사용자 측면에서 불편을 느낄 수 있다는 것이다.

도입 후

const 페이지컴포넌트 = () => {
  const { 
    상태1, 
    상태1hadnler함수 
  } = 상태1커스텀훅();

  return(
    <>
      // UI 로직 ...
      <Suspense fallback={<Loading />}
        <상태23보여주는컴포넌트 />
      </Suspense>
    </>
  )
  
const 상태2와3보여주는컴포넌트 = () => {
  const { 상태2 } = 상태2커스텀훅();
  const { 상태3 } = 상태3커스텀훅(); 
  
  return (
     // UI 로직...
  )
}

이제 페이지 컴포넌트에서 모든 상태를 지니고 있지 않는다. 컴포넌트에 종속되어야하는 상태는 해당 컴포넌트에 존재한다. API 요청으로 상태를 결정하는 경우에는 Suspense를 동작시킨다.

  • 컴포넌트의 역할이 명확히 분리되고 결합도가 낮아졌다.
  • state waterfall 문제가 발생하지 않는다.
  • loading이 끝나고 컴포넌트를 렌더링하는 것이 아니기 때문에 사용성이 좋아진다.
  • Suspense를 활용해 선언적으로 코드를 작성할 수 있다.

리펙터링을 마치고 나니 코드가 간결하지고 선언적으로 작성됨을 느낄수 있었다. 그렇다고 이전의 구조 틀렸다고는 생각하지 않는다. 왜냐하면 현재 리펙터링한 페이지는 모든 컨텐츠가 로드되야 사용자가 정상적으로 사용할 수 있는 페이지이기 때문이다. 여러 컨텐츠 중에 하나라도 로드되지 않는다면 사용하는 입장에서는 사용 의미가 퇴색되기 때문이다. 컴포넌트 구조, 커스텀 훅 구조에는 정답이 없지만 React를 쓰는 입장으로 React가 추구하려는 방향으로 프로그래밍을 하는 것이 React를 잘 쓴다고 할 수 있지 않을까?

0개의 댓글