⚙️ Promise 일급 지원 : use
function use<T>(promise: Promise<T>) : T
promise를 파라미터로 받아 resolve된 값을 return하는 동기 함수
마치 await의 함수형처럼 기능을 함
// async function
const data = await promise;
// React Component / Hooks
const data = use(promise);
use 훅이 suspense를 발동시키는 트리거의 역할을 수행
<Suspense fallback={<div>loading...</div>}>
<MyApp /> // use(promise)
</Suspense>
왜 이름이 use일까?
use 훅이 가지고 있는 한 가지 특별함이 존재함
: 기존에 있던 훅들과는 달리 조건부로 호출이 가능함
→ 훅의 사용으로 인한 여러 가지 문제점을 해결할 수 있을 것이라는 기대를 가짐
🖥️ Case Study
사례 - 유저 인벤토리 조회 : useInventory

react로 만든 admin tool에서 유저의 인벤토리 조회 가능
type UseInventoryArgs = {
userId: string; // 유저의 아이디
search?: string; // 검색 키워드
};
function useInventory(args: UseInventoryArgs)
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory
.filter ((item) => {
if (!search) return true;
return item.name.includes (search);
})
}
useUserInfo 라는 커스텀 훅을 통해 유저의 전체정보를 받아옴
inventory를 filter를 통해 검색한 키워드에 해당하는 아이템 서치
problem 1 : item 안에 name 필드가 존재하지 않음

return inventory
.filter ((item) => {
if (!search) return true;
if (Normal Item이면) **normalItems** 에서 이름 체크;
if (Event Item이면) **eventItems** 에서 이름 체크;
return false;
})
게임은 초기에 모든 리소스를 다운로드하는 방식으로 진행하지만,
이를 브라우저에게 적용시키기는 힘듦
- 수십 MB의 거대한 사이즈
→ 접속마다 다운로드하기 부담 - 개발 환경에서 매 시간 업데이터
→ 캐싱 효율성 X - 꾸준히 증가하는 데이터 총량
→ 확장성
⇒ 각 resource를 따로따로 fetch하고,
이 데이터를 컴포넌트에서 사용하기 위해 hook을 추가로 정의
// fetch
const fetchNormalItems = () => {
return fetch('/res/normal-items');
}
const fetchEventItems = /* 생략 */
// hooks
const useNormalItems = () => {
return useQuery (fetchNormalItems);
}
const useEventItems = /* 생략 */
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory
.filter ((item) => {
if (!search) return true;
return item.name.includes (search);
})
}
Hook의 제약으로 인한 문제점
- 불필요한 Blocking → TTI 증가 → UX ↓
TTI ? Time-to-Interactive, 사용자와 인터렉션이 가능할 때까지의 시간
<sol 1 : search keyword 사용 X, userId만을 가지고 조회하는 경우>
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
search 키워드를 넘기지 않음 → 커스텀 훅이 로딩되었으나 사용 X
- 코드 응집도 저하 → DX ↓
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
- 로딩을 하는 코드와 사용을 하는 코드의 위치가 꽤 멂
- 하지만 이는 어쩔 수 없음 why ? hook은 항상 코드 최상단에서 호출해야 한다는 제약 조건 존재
use로 Hook의 제약 벗어나기
Hook을 쓰지 못하는 곳
🔴 조건문, 반복문
🔴 return 문 다음
🔴 이벤트 핸들러
🔴 클래스 컴포넌트
🔴 useMemo, useReducer, useEffect에 전달한 클로저
------------------------------------------------------------
use는 쓸 수 있는 곳
🟢 조건문, 반복문
🟢 return 문 다음
🔴 이벤트 핸들러
🔴 클래스 컴포넌트
🔴 useMemo, useReducer, useEffect에 전달한 클로저
Before
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
After
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) use(fetchNormalItems()) 에서 이름 체크;
if (Event Item?) use(fetchEventItems()) 에서 이름 체크;
return false;
})
}
// 실제로 리소스를 사용하는 곳에서 데이터 fetching을 하도록 변경
결과
- Top Level Hook 제거 → DX ↑
- 필요한 순간 리소스 로딩 → UX ↑
🙋 그러면 이제 use만 쓰면 되나요?
❌ use는 low-level API 이며, Data Fetching 라이브러리는 더 많은 기능을 제공함
😉 따로 해결했던 문제
중복 fetch → cache ✅
function useInventory(...) {
...
use(fetchNormalItems());
...
}
normal item을 fetching하는 promise가 resolve됨
→ 리렌더링 발생 → 다시 fetching → ♾️
solution. Cache
const fetchNormalItems = **cache**() => {
return fetch('/res/normal-items');
}
일종의 메모이제이션 방식
- 동일한 promise 인스턴스를 리턴함
- cache API는 현재 experimental, lodash.memoize로도 같은 기능 사용 O
Request waterfall → prefetch ✅
// function useInventory
const { inventory } = useKingdom(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal?) use(fetchNormalItems()) 에서 이름 체크;
if (Event?) use(fetchEventItems()) 에서 이름 체크;
return false;
});

↑ use 훅을 사용함으로써, fetchNormalItems이 호출될 때, blocking 발생
그 후에 fetchEventItems이 호출될 때, blocking 발생
그러나, 굳이 불필요하게 순차적으로 로딩될 필요 X ← request waterfall 현상
request waterfall 해결 방법 : Prefetch, Parallel Query (여기서는 prefetch 사용)
solution. Prefetch
// function useInventory
const { inventory } = useKingdom(userId);
fetchNormalItems(); fetchEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal?) use(fetchNormalItems()) 에서 이름 체크;
if (Event?) use(fetchEventItems()) 에서 이름 체크;
return false;
});


말 그대로 사전에 fetching을 해줌
여기서 use를 사용하지 않았기 때문에 blocking 발생 X
⇒ request waterfall 해결…했지만
new problem : 다시 저하된 응집성
위 코드에서 다시 로딩을 하는 코드와 사용하는 코드의 위치가 멀어짐
→ 응집성 ↓
solution. Dynamic Prefetch
: prefetch 대상을 런타임에 결정
const fetchNormalItems = () => {
현재 페이지에서 normalItem 사용했다고 localStorage에 기록
return fetch('/res/normal-items');
}
// 페이지 접속 시 발생하는 이벤트 핸들러에서 prefetch를 발생시키는 코드 작성
document.onready = () => {
const names = 1ocalstorage에서 사용 기록 읽기
names.forEach((XXX) => fetchXXX());
}
어떤 리소스를 fetching 할 지는 런타임 때 정해짐
→ 각 fetching 코드는 단 한 번만 작성해도 충분해짐
Before - promise 간접 사용

항상 hooks을 통해 간접적으로 사용해야 했음
After - 직접 사용

hook 정의 필요 없어짐
⇒ Promise 일급 지원
🥺 use의 제약
미래 : React component, hook 안에서만 use를 사용할 수 있음
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) use(fetchNormalItems()) 에서 이름 체크;
if (Event Item?) use(fetchEventItems()) 에서 이름 체크;
return false;
})
}
use를 호출하고 있는 함수 : filter에 전달하는 Closure 함수임 ( ≠ component, hook)
제약을 지키지 않는다면? “컴파일러” 에러를 발생시킬 수 있다.
컴파일러 : 성능 최적화를 자동으로 해주는 React Forget Compiler
- async / await는 문법 요소 → 컴파일러 기반 최적화 가능
- async Server Component 최적화 개발 중
반면, use는 함수지만, React 내에서는 문법 요소처럼 역할 하도록 강제
⇒ use의 문법적 제약 = 컴파일러 최적화를 위한 대비
BUT, 현재 react는 DX / UX를 모두 향상할 수 있는 방향 추구 중
→ use가 제약될 날은 아직 멀었다!
🖋️ summary
- 클라이언트 컴포넌트 Promise 일급 지원
- 활용 사례 : DX(응집도) / UX(blocking) 개선
🔗 참고
'Conference' 카테고리의 다른 글
[Conference] 우아콘2023 - 프론트엔드 상태관리 실전 편 with React Query & Zustand (2) | 2024.09.24 |
---|
⚙️ Promise 일급 지원 : use
function use<T>(promise: Promise<T>) : T
promise를 파라미터로 받아 resolve된 값을 return하는 동기 함수
마치 await의 함수형처럼 기능을 함
// async function
const data = await promise;
// React Component / Hooks
const data = use(promise);
use 훅이 suspense를 발동시키는 트리거의 역할을 수행
<Suspense fallback={<div>loading...</div>}>
<MyApp /> // use(promise)
</Suspense>
왜 이름이 use일까?
use 훅이 가지고 있는 한 가지 특별함이 존재함
: 기존에 있던 훅들과는 달리 조건부로 호출이 가능함
→ 훅의 사용으로 인한 여러 가지 문제점을 해결할 수 있을 것이라는 기대를 가짐
🖥️ Case Study
사례 - 유저 인벤토리 조회 : useInventory

react로 만든 admin tool에서 유저의 인벤토리 조회 가능
type UseInventoryArgs = {
userId: string; // 유저의 아이디
search?: string; // 검색 키워드
};
function useInventory(args: UseInventoryArgs)
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory
.filter ((item) => {
if (!search) return true;
return item.name.includes (search);
})
}
useUserInfo 라는 커스텀 훅을 통해 유저의 전체정보를 받아옴
inventory를 filter를 통해 검색한 키워드에 해당하는 아이템 서치
problem 1 : item 안에 name 필드가 존재하지 않음

return inventory
.filter ((item) => {
if (!search) return true;
if (Normal Item이면) **normalItems** 에서 이름 체크;
if (Event Item이면) **eventItems** 에서 이름 체크;
return false;
})
게임은 초기에 모든 리소스를 다운로드하는 방식으로 진행하지만,
이를 브라우저에게 적용시키기는 힘듦
- 수십 MB의 거대한 사이즈
→ 접속마다 다운로드하기 부담 - 개발 환경에서 매 시간 업데이터
→ 캐싱 효율성 X - 꾸준히 증가하는 데이터 총량
→ 확장성
⇒ 각 resource를 따로따로 fetch하고,
이 데이터를 컴포넌트에서 사용하기 위해 hook을 추가로 정의
// fetch
const fetchNormalItems = () => {
return fetch('/res/normal-items');
}
const fetchEventItems = /* 생략 */
// hooks
const useNormalItems = () => {
return useQuery (fetchNormalItems);
}
const useEventItems = /* 생략 */
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory
.filter ((item) => {
if (!search) return true;
return item.name.includes (search);
})
}
Hook의 제약으로 인한 문제점
- 불필요한 Blocking → TTI 증가 → UX ↓
TTI ? Time-to-Interactive, 사용자와 인터렉션이 가능할 때까지의 시간
<sol 1 : search keyword 사용 X, userId만을 가지고 조회하는 경우>
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
search 키워드를 넘기지 않음 → 커스텀 훅이 로딩되었으나 사용 X
- 코드 응집도 저하 → DX ↓
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
- 로딩을 하는 코드와 사용을 하는 코드의 위치가 꽤 멂
- 하지만 이는 어쩔 수 없음 why ? hook은 항상 코드 최상단에서 호출해야 한다는 제약 조건 존재
use로 Hook의 제약 벗어나기
Hook을 쓰지 못하는 곳
🔴 조건문, 반복문
🔴 return 문 다음
🔴 이벤트 핸들러
🔴 클래스 컴포넌트
🔴 useMemo, useReducer, useEffect에 전달한 클로저
------------------------------------------------------------
use는 쓸 수 있는 곳
🟢 조건문, 반복문
🟢 return 문 다음
🔴 이벤트 핸들러
🔴 클래스 컴포넌트
🔴 useMemo, useReducer, useEffect에 전달한 클로저
Before
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
const normalItems = useNormalItems();
const eventItems = useEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) normalItems 에서 이름 체크;
if (Event Item?) eventItems 에서 이름 체크;
return false;
})
}
After
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) use(fetchNormalItems()) 에서 이름 체크;
if (Event Item?) use(fetchEventItems()) 에서 이름 체크;
return false;
})
}
// 실제로 리소스를 사용하는 곳에서 데이터 fetching을 하도록 변경
결과
- Top Level Hook 제거 → DX ↑
- 필요한 순간 리소스 로딩 → UX ↑
🙋 그러면 이제 use만 쓰면 되나요?
❌ use는 low-level API 이며, Data Fetching 라이브러리는 더 많은 기능을 제공함
😉 따로 해결했던 문제
중복 fetch → cache ✅
function useInventory(...) {
...
use(fetchNormalItems());
...
}
normal item을 fetching하는 promise가 resolve됨
→ 리렌더링 발생 → 다시 fetching → ♾️
solution. Cache
const fetchNormalItems = **cache**() => {
return fetch('/res/normal-items');
}
일종의 메모이제이션 방식
- 동일한 promise 인스턴스를 리턴함
- cache API는 현재 experimental, lodash.memoize로도 같은 기능 사용 O
Request waterfall → prefetch ✅
// function useInventory
const { inventory } = useKingdom(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal?) use(fetchNormalItems()) 에서 이름 체크;
if (Event?) use(fetchEventItems()) 에서 이름 체크;
return false;
});

↑ use 훅을 사용함으로써, fetchNormalItems이 호출될 때, blocking 발생
그 후에 fetchEventItems이 호출될 때, blocking 발생
그러나, 굳이 불필요하게 순차적으로 로딩될 필요 X ← request waterfall 현상
request waterfall 해결 방법 : Prefetch, Parallel Query (여기서는 prefetch 사용)
solution. Prefetch
// function useInventory
const { inventory } = useKingdom(userId);
fetchNormalItems(); fetchEventItems();
return inventory.filter((...) => {
if (!search) return true;
if (Normal?) use(fetchNormalItems()) 에서 이름 체크;
if (Event?) use(fetchEventItems()) 에서 이름 체크;
return false;
});


말 그대로 사전에 fetching을 해줌
여기서 use를 사용하지 않았기 때문에 blocking 발생 X
⇒ request waterfall 해결…했지만
new problem : 다시 저하된 응집성
위 코드에서 다시 로딩을 하는 코드와 사용하는 코드의 위치가 멀어짐
→ 응집성 ↓
solution. Dynamic Prefetch
: prefetch 대상을 런타임에 결정
const fetchNormalItems = () => {
현재 페이지에서 normalItem 사용했다고 localStorage에 기록
return fetch('/res/normal-items');
}
// 페이지 접속 시 발생하는 이벤트 핸들러에서 prefetch를 발생시키는 코드 작성
document.onready = () => {
const names = 1ocalstorage에서 사용 기록 읽기
names.forEach((XXX) => fetchXXX());
}
어떤 리소스를 fetching 할 지는 런타임 때 정해짐
→ 각 fetching 코드는 단 한 번만 작성해도 충분해짐
Before - promise 간접 사용

항상 hooks을 통해 간접적으로 사용해야 했음
After - 직접 사용

hook 정의 필요 없어짐
⇒ Promise 일급 지원
🥺 use의 제약
미래 : React component, hook 안에서만 use를 사용할 수 있음
function useInventory({ userId, search }) {
const { inventory } = useUserInfo(userId);
return inventory.filter((...) => {
if (!search) return true;
if (Normal Item?) use(fetchNormalItems()) 에서 이름 체크;
if (Event Item?) use(fetchEventItems()) 에서 이름 체크;
return false;
})
}
use를 호출하고 있는 함수 : filter에 전달하는 Closure 함수임 ( ≠ component, hook)
제약을 지키지 않는다면? “컴파일러” 에러를 발생시킬 수 있다.
컴파일러 : 성능 최적화를 자동으로 해주는 React Forget Compiler
- async / await는 문법 요소 → 컴파일러 기반 최적화 가능
- async Server Component 최적화 개발 중
반면, use는 함수지만, React 내에서는 문법 요소처럼 역할 하도록 강제
⇒ use의 문법적 제약 = 컴파일러 최적화를 위한 대비
BUT, 현재 react는 DX / UX를 모두 향상할 수 있는 방향 추구 중
→ use가 제약될 날은 아직 멀었다!
🖋️ summary
- 클라이언트 컴포넌트 Promise 일급 지원
- 활용 사례 : DX(응집도) / UX(blocking) 개선
🔗 참고
'Conference' 카테고리의 다른 글
[Conference] 우아콘2023 - 프론트엔드 상태관리 실전 편 with React Query & Zustand (2) | 2024.09.24 |
---|