요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

*FEConf2023에서 발표한 <use 훅이 바꿀 리액트 비동기 처리의 미래 맛보기>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 use훅의 등장과 특징에 관해 살펴보고 실제 실무에서 사용한 코드를 바탕으로 기존 훅의 제약을 살펴봅니다. 2회에서는 기존 훅의 제약을 use를 통해 어떻게 해결했는지를 알아보고, use훅에도 존재하는 제약을 살펴보며 앞으로의 리액트를 생각해봅니다. 본문에 삽입된 이미지의 출처는 모두 동명의 발표자료로, 따로 출처를 표기하지 않았습니다. 발표자료는 FEConf2023 홈페이지에서 다운받을 수 있습니다. 

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

use 훅이 바꿀 리액트 비동기 처리의 미래 맛보기(1)

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

*FEConf2023에서 발표한 <use 훅이 바꿀 리액트 비동기 처리의 미래 맛보기>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 use훅의 등장과 특징에 관해 살펴보고 실제 실무에서 사용한 코드를 바탕으로 기존 훅의 제약을 살펴봅니다. 2회에서는 기존 훅의 제약을 use를 통해 어떻게 해결했는지를 알아보고, use훅에도 존재하는 제약을 살펴보며 앞으로의 리액트를 생각해봅니다. 본문에 삽입된 이미지의 출처는 모두 동명의 발표자료로, 따로 출처를 표기하지 않았습니다. 발표자료는 FEConf2023 홈페이지에서 다운받을 수 있습니다. 

 

FEConf2023에서 발표된 ‘use훅이 바꿀 리액트 비동기 처리의 미래 맛보기’/문태근 데브시스터즈 프론트엔드 엔지니어

 

use 훅이 바꿀 리액트 비동기 처리의 미래에 대해 소개하게 된 문태근입니다. 저는 현재 데브시스터즈라는 모바일 게임회사에서 프론트엔드 엔지니어로 근무하고 있습니다. 주로 게임 운영과 게임 개발에 관련된 여러 가지 툴을 개발하고 있습니다.

 

이번 글에서는 새롭게 출시된 use라는 훅에 대해 알아보고, 실무에서 작성했던 코드를 기반으로 use를 활용하여 기존 훅의 문제를 같이 해결해 보겠습니다. 또, use의 제약 조건에서 엿볼 수 있는 리액트의 미래에 대해 소개합니다. 리액트 18.2 버전을 기준으로 하며, 최근에 서버 컴포넌트라는 새로운 개념이 추가되었지만, 별도의 언급이 없는 한 이번 글에서는 클라이언트 컴포넌트를 기준으로 설명하겠습니다.

 

 

리액트의 데이터 fetch 변천사

여러분이 처음 리액트를 배웠을 때, 비동기 데이터 처리를 위한 방법이 무엇이라고 배웠나요? 처음 작성했던 마이앱이라는 컴포넌트에서 비동기로 데이터를 불러오는 fetch 함수를 작성할 때 코드를 어떻게 작성하셨나요?

 

아마도 아래와 같이 useState와 useEffect를 통해서 데이터를 불러와서 데이터 state에 저장했을 것 같습니다. 그리고 로딩 중인 상태와 에러 상태를 대응하기 위해서 로딩 state와 에러 state를 추가했을 것 같습니다. 이러한 세 가지 필수 상태(데이터, 로딩, 에러)를 커스텀 훅으로 정의하여 사용하는 것이 일반적인 패턴입니다.

 

리액트 데이터 fetch의 일반적인 형태

 

리액트 쿼리와 같은 전문적인 데이터 fetch 라이브러리는 위와 같은 패턴을 전문적으로 사용할 수 있게 해줍니다. 데이터, 로딩, 에러 세 가지 상태의 전환은 라이브러리에 맡기고 결괏값만 사용하면 됩니다.

 

데이터 fetch 라이브러리의 형태 (리액트 쿼리)

 

또 최근에 서스펜스라는 새로운 개념이 리액트에 도입되었습니다. 서스펜스 개념을 통해 우리는 ‘데이터가 로딩이 완료된 상태’만 고려하면 됩니다. 아래와 같이 <MyApp />은 데이터가 로딩된 상태, <Suspense />는 로딩 중인 상태, <ErrorBoundary />는 에러 상태를 처리하는 구조입니다.

 

서스펜스와 에러 바운더리

 

서스펜스를 사용하는 방법은 아주 간단합니다. 아래와 같이 데이터 fetch 라이브러리들의 서스펜스 옵션만 켜주면 됩니다. 그럼 이전에 봤던 것과 다르게 로딩과 에러에 대한 처리는 할 필요가 없게 됩니다.

 

리액트 쿼리의 서스펜스 옵션

 

그럼 이제 한 가지 드는 의문점이 있습니다. 이 코드에서 핵심은 데이터를 fetch 하는 코드인데, 왜 굳이 여기서 훅을 써야 할까요?

 

fetch 사용을 위한 훅 사용

 

그냥 fetch에 어웨이트를 바로 하면 안 되는 걸까요? 아시다시피 이런 형태는 불가능합니다. 왜냐하면 클라이언트 컴포넌트는 async 함수일 수 없기 때문입니다.

 

async일 수 없는 클라이언트 컴포넌트

 

그렇다면 fetch가 리턴 한 promise가 resolve 되었을 때의 결괏값을 동기적으로 꺼낼 수 있는 마법 같은 함수가 있다면 얼마나 좋을까요?

 

동기적으로 결괏값을 꺼내는 함수

 

 

use: 리액트 팀에서 준비하는 promise 1급 지원

리액트 팀에서 이러한 것을 가능하게 하기 위해 RFC(리액트의 스펙에 대해 논의하는 문서)에서 논의하고 있는 내용이 있습니다. 바로 promise 1급 지원에 대한 내용입니다.

 

RFC의 promise 일급 지원 <출처: React.js RFC>

 

이 RFC에서 새로 도입하고자 논의하고 있는 바로 훅이 바로 use라는 이름의 새로운 훅입니다. 앞서 소개한 것처럼 promise를 파라미터로 받아서  resolve 된 값을 리턴하는 동기 함수의 시그니처를 가지고 있습니다.

 

use 훅

 

만약 자바스크립트의 await가 함수였다면 이런 모습이지 않을까 싶은 형태입니다. 정확히 동일한 기능을 하는 것은 아니지만, 결과만 보면 거의 비슷하게 동작한다고 보면 될 것 같습니다.

 

await와 use

 

그리고 이 use라는 훅이 지금까지 써왔던 서스펜스를 발동시키는 트리거의 역할을 하게 됩니다. <MyApp />을 렌더링 하는 도중에 아직  resolve 되지 않은 promise를 use를 하게 되면 가장 가까운 서스펜스의 폴백이 렌더링 되는 구조입니다.

 

서스펜스의 트리거

 

왜 이름이 use일까?

use라는 훅은 기존의 훅들과 다르게 이름이 특이합니다. 보통 useState나 useEffect와 같이 use 뒤에 무엇인가 붙는 형태의 이름을 가집니다. 그런데 use는 아무것도 붙지 않았죠. 왜 use는 이렇게 특별한 이름을 가지는 걸까요? 그 이유는 기존의 훅들과는 다르게 조건부로 호출될 수 있다는 특별한 점 때문입니다. 앞으로 생겨날 다양한 훅 중에서도 오직 use만 이러한 특별함을 가질 것이기 때문에 특별한 이름을 가지게 되었다고 합니다.

 

use는 어떻게 동작할까?

아직 use의 정확한 구현체는 나오지 않았습니다. 실제로 리액트에서 use를 사용하면 타입 체크는 통과하지만 런타임에서 use가 존재하지 않는다는 에러가 발생합니다. use가 서스펜스를 발동시키는 트리거 역할을 하는데, 그렇다면 조타이(Jotai), 리코일(Recoil), 리액트 쿼리(React Query)와 같은 메이저 라이브러리들은 어떻게 서스펜스를 발생시키고 있을까요? RFC 문서를 찾아보면, 리액트 18.2를 기준으로 아직은 안정화되지 않은 방법으로 이를 지원하고 있습니다. 이 방법은 바로 임의의 함수가 promise를 throw 하는 것입니다. 즉, use를 호출하는 대신에 promise를 직접 throw 하라는 의미입니다.

 

아래는 실제 조타이라는 상태 관리 라이브러리에서 use를 사용하는 코드입니다.

 

조타이의 use 사용

 

이런 식으로 리액트가 익스포트 하고 있는 use라는 함수가 있으면 해당 함수를 사용하고, 그렇지 않으면 직접 정의를 해서 쓰고 있습니다. 코드를 자세히 보면 promise가 펜딩된 상태이면 promise를 throw 하고,  resolve 된 상태이면  resolve 된 결괏값을 바로 리턴합니다. 에러가 발생한다면 기존처럼 에러를 throw 합니다. 이와 같이 use를 직접 정의하여 사용한 방법을 이 글에서는 안정화되지 않은 use라고 칭하겠습니다.

 

중간 정리

1. use라는 새로운 훅 출시 예정

2. use는 서스펜스의 트리거

3. use는 await와 비슷한 역할

4. use는 조건부 호출 가능

 

이 중 4번의 조건부 호출이 가능하다는 특별함은 아주 매력적인 기능이라고 생각합니다. 이 기능을 통해 기존에 우리가 훅을 사용하며 느꼈던 여러 가지 문제점들을 해결할 수 있을 것이라고 기대되기 때문입니다.

 

그럼 이제 제가 실제 데브시스터즈 실무에서 작성했던 코드를 예시로 기존 훅에서 느낀 문제점을 살펴보고, use 도입을 통해 이런 문제점들을 어떻게 극복했는지 살펴보겠습니다.

 

 

use: 케이스 스터디

아이템 인벤토리 훅

아래 그림은 모바일 게임 쿠키런 킹덤의 아이템 인벤토리 화면입니다. 그리고 아이템 인벤토리를 확인할 수 있는 어드민 툴이 있습니다. 이 어드민 툴은 리액트로 개발되어 있습니다.

 

쿠키런 킹덤의 인벤토리 화면

 

이번에는 아이템 인벤토리를 조회하기 위한 useInventory라는 훅을 같이 만들어보겠습니다. useInventory 훅은 어떤 유저를 조회할 것인지를 의미하는 유저 아이디 파라미터를 필수적으로 받고, 검색을 위한 검색 키워드 파라미터를 조건적으로 받습니다. 대략적인 코드는 아래와 같습니다.

 

useInventory 훅

 

함수 내부의 첫 번째 줄에서 useUserInfo라는 커스텀 훅에서 게임 서버로부터 유저의 전체 정보를 받아옵니다. 그다음 인벤토리에 해당하는 정보만 구조 분해 할당을 통해 가져옵니다. 다음으로 인벤토리의 모든 아이템을 필터 함수를 통해 순회합니다. 전달받은 검색 키워드가 존재하지 않는다면 모든 아이템을 리턴하고, 검색 키워드가 존재하면 아이템의 이름이 해당 검색 키워드를 포함하고 있는지 체크합니다.

 

지금까지의 코드를 보면 큰 문제점이 없을 것 같지만, 한 가지 문제점이 있습니다. 아이템이라는 데이터에서 네임이라는 필드가 존재하지 않는다는 문제입니다. 게임 서버에서 클라이언트에 보내주는 데이터에는 아래 그림에서처럼 네임 필드가 아닌 리소스 아이디 필드(resId)와 아이템 개수 필드(count)가 들어있습니다.

 

name이 아닌 resId를 속성으로 가진 실제 데이터

 

실제 게임 클라이언트는 자신이 가지고 있는 리소스 데이터 안에서 이 리소스 아이디에 해당하는 아이템 데이터를 찾고 이를 합쳐 화면에 표시하는 구조입니다. 또, 각 아이템마다 리소스 데이터의 종류가 다를 수 있습니다. 이 글에서는 편의상 노멀 아이템과 이벤트 아이템이라는 두 가지 종류가 있다고 가정하겠습니다. 이제 기존 코드의 네임 필드를 찾던 코드를 아래와 같이 리소스 데이터에서 일치하는 데이터를 찾는 코드로 바꿀 수 있습니다.

 

리소스 데이터 조회 코드로 변경

 

리소스 데이터 로딩 방법

게임을 플레이해 보신 분들이라면 아래와 같이 게임 클라이언트가 필요한 리소스 데이터를 사전에 다운로드하는 경험을 해보셨을 것입니다. 일반적으로 게임 클라이언트는 사전에 리소스 데이터를 모두 다운로드하는 방식을 사용합니다.

 

 

그러나 브라우저 환경에서 사용하기에는 아래와 같이 여러 가지 어려운 문제점들이 있습니다.

 

브라우저 환경에서 리소스를 한번에 로드시 생기는 문제

 

이 문제점들을 극복하기 위해 저희 팀에서는 각 리소스 데이터를 조회하는 fetch 함수를 따로 정의하고, 이 데이터를 다시 컴포넌트 안에서 사용하기 위해 useQuery 훅을 이용해서 useNormalItems 훅과 useEventItems 훅을 추가로 정의하였습니다. 최종적으로 리소스 데이터를 불러오는 두 가지 훅을 useInventory 상단에 선언하는 구조를 만들었습니다.

 

리소스 데이터를 불러오는 훅

 

리소스 데이터를 불러오는 훅을 사용하는 방법

 

훅의 제약으로 인한 문제점

위에서 작성한 코드는 훅의 제약으로 인한 몇 가지 문제점이 있었습니다.

 

1. 불필요한 블로킹으로 인한 TTI (Time to interactive, 유저가 인터랙션 가능할 때까지의 시간) 증가

2. TTI 증가로 인한 UX (User experience) 성능 저하

3. 코드 응집도 저하로 인한 DX (Developer experience) 저하

 

앞선 예제를 통해 위 문제점들을 같이 해결해 보겠습니다.

 

불필요한 블로킹 문제

먼저 불필요한 블로킹 문제를 보겠습니다. 이번에는 검색 키워드를 전달하지 않고, 유저 아이디로만 조회한다고 가정하겠습니다. 훅이 호출되면 코드 상단의 리소스 데이터를 불러오는 훅이 동작하고, 이때 블로킹이 발생합니다. 그리고 모든 블로킹이 끝난 다음에 인벤토리 각 아이템을 filter 함수를 통해 순회합니다. 검색 키워드가 존재하지 않기 때문에 모든 아이템에 대해 트루를 리턴하고 순회가 끝납니다.

 

코드에서 볼 수 있듯이 상단에서 리소스 데이터를 다 로딩했지만 정작 사용은 하지 않고 훅이 종료됩니다. 페이지에 처음 접속했을 때는 검색 키워드가 없기 때문에 모든 아이템들을 반환하면 되는 상황임에도 불구하고, 사용하지 않는 불필요한 리소스 데이터를 모두 불러온 것이 문제였습니다.

 

로드했지만 사용되지 않은 리소스 데이터

 

코드 응집도 저하 문제

다음으로 코드 응집도 저하 문제를 살펴보겠습니다. 리소스 데이터를 불러오는 부분은 코드 상단에 위치하고, 불러온 리소스 데이터를 사용하는 부분은 코드의 하단에 위치합니다. 즉, 리소스를 로딩하는 코드와 이를 사용하는 코드의 거리가 멀어집니다.

 

코드 응집도 저하 문제

 

위 예제는 편의상 아이템 종류가 적지만, 실제 코드에서 아이템 종류가 많아진다면 어떻게 될까요? 그리고 리소스 데이터가 수백 종류에 이른다면 어떻게 될까요? 아래의 실제 코드를 보면 데이터를 로딩하는 하는 부분과 사용하는 부분의 거리가 굉장히 먼 것을 알 수 있습니다. 실제로 게임의 특성상 리소스 데이터가 수백 종류에 이르는 방대한 양이었고, 리소스 데이터를 사용하는 페이지도 수십 페이지에 이를 정도로 굉장히 다양했습니다. 그래서 매 페이지에서 다 리소스 데이터에 로딩을 위한 코딩을 대응하는 게 정말 쉽지 않은 경험이었습니다.

 

코드 응집도 저하 사례

 

이와 같은 문제가 발생한 이유는 훅은 최상단에서 호출해야 한다는 제약 조건 때문입니다. 즉, 인벤토리 데이터를 순회하는 필터 함수의 반복문 안에서 리소스 데이터를 조회하는 훅을 호출할 수 없기 때문에 이 훅은 코드 상단에 선언될 수밖에 없었습니다.

 

다음 글에서는 실제로 use를 사용하면서 이런 문제를 어떻게 해결했는지 알아보겠습니다.

 

FEConf

편집 오신엽 객원 에디터

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 1
FEConf
508
명 알림 받는 중

작가 홈

FEConf
508
명 알림 받는 중
프론트엔드 컨퍼런스 FEConf 공식 계정입니다.

좋아요

댓글

스크랩

공유

공유

요즘IT가 PICK한 뉴스레터를 매주 목요일에 만나보세요

요즘IT가 PICK한 뉴스레터를
매주 목요일에 만나보세요

뉴스레터를 구독하려면 동의가 필요합니다.
https://auth.wishket.com/login