📋 개요
- useId
- startTransition, useTransition
- useDefferedValue
- useSyncExternalStore
- useInesrtoinEffect
✅ useId
최상위 수준에서 호출되어 고유 ID를 생성하는 React Hook으로, 접근성 속성에 전달될 수 있다.
- key 는 데이터 식별을 위해 사용되므로 데이터에서 생성되는 것이 더 바람직하며, 너무 많은 호출이 일어나기 때문이다.
📦 사용하는 이유)
- 하드코딩의 횟수를 줄이자
- 컴포넌트를 여러번 사용하더라도 id 속성이 겹치지 않는다.
- hydration mismatch를 해결할 수 있다.
더보기
예시)
사용자의 이름 정보를 받기 위해 input을 컴포넌트로 받아서 id값을 name이라고 하였다. 이때 이 컴포넌트를 반복해서 사용하게 된다면 id가 name인 input이 여러 개 생기게 되는데 이는 좋은 상황이 아니다. 그렇기에 이러한 id 속성의 중복을 막을 수 있다. 또한 id를 설정을 할 때 일일히 하드 코딩으로 구현하는 것은 바람직하지 않다고 react-ko에서 말한다.
또한 무작위로 id값을 생성하는 경우에 발생할 수 있는 hydration mismatch를 해결할 수 있다.
📦 사용 예시)
- input을 반복해서 많이 사용하는 경우에 사용할 수 있으며 회원가입이나 로그인 상황에서 에러문구를 띄울 때 유용하게 사용될 것이다.
더보기
import { useId } from 'react';
function InputField() {
const id = useId();
return (
<>
<label>
ID:
<input
type="text"
aria-describedby={id}
/>
</label>
<p id={id}>
The ID should contain at least 18 characters
</p>
</>
);
}
✅ starTransition, useTransition 🔗
isPending과 startTransition 두개의 항목이 있는 배열을 반환하는 훅으로 UI를 차단하지 않고 상태를 업데이트 할 수 있다.
- startTransition()을 통해 우선순위가 낮은 상태 업데이트들을 transition이라고 표기해 우선순위를 정해준다. (transition이라고 적히면 낮은 우선순위)
- 낮은 우선순위를 가진 상태는 다른 상태 업데이트가 호출되면 중단된다.
📦 사용하는 이유)
- 느린기기에서도 사용자 인터페이스 업데이트를 반응성 있게 유지할 수 있다.
- UI 랜더링 시 우선순위에 따라 업데이트를 할 수 있다.
- Suspense와 연계하여 불필요한 로딩 인디케이터(Spinner와 같은 것)의 노출을 막을 수 있어 UX를 개선시킨다.
더보기
예시)
느린 기기에서 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하게 되었을 때 첫 번째 리렌더링이 완료될 때까지 기다린 이후 다음 클릭할 수 있었을텐데 Hook의 사용을 통해서 기다릴 필요 없이 다른 탭을 클리갛ㄹ 수 있게 되었다.
📦 사용 예시)
- 느린 사용자 기기에서 탭의 전환 시에 사용하면 유용하다.
더보기
const App = () => {
const [tab, setTab] = useState('about');
return (
<>
{/* 탭을 클릭하면 렌더링할 탭 컴포넌트가 설정된다 */}
<TabButton isActive={tab === 'about'} onClick={() => setTab('about')}>
About
</TabButton>
<TabButton isActive={tab === 'posts'} onClick={() => setTab('posts')}>
Posts (slow)
</TabButton>
<TabButton isActive={tab === 'contact'} onClick={() => setTab('contact')}>
Contact
</TabButton>
<hr />
{/* 현재 탭에 따라 탭 컴포넌트가 렌더링 된다 */}
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</>
);
};
const TabButton = ({ children, isActive, onClick }) => {
const [isPending, startTransition] = useTransition();
// 현재 탭이 활성화 되면 isActive 상태가 된다.
if (isActive) {
return <b>{children}</b>;
}
// 대기 중인 transition이 있다면 isPending이 된다.
if (isPending) {
return <b className="pending">{children}</b>;
}
/**
* props로 받은 onClick 함수를 startTransition으로 감싸주기 때문에
* onClick 함수(setTab)은 transition으로 설정되어 렌더링시 우선순위에서 밀리게 된다.
* 그 결과 오랜시간이 걸리는 PostsTab 컴포넌트를 렌더링 하는 도중 다른 탭을 누르게 되면
* PostsTab 컴포넌트의 렌더링을 멈추고 다른 컴포넌트를 렌더링하게 된다.
**/
const handleButtonClick = () => {
startTransition(() => {
onClick();
});
};
return <button onClick={handleButtonClick}>{children}</button>;
};
const AboutTab = () => {
return <p>Welcome to my profile!</p>;
};
const PostsTab = () => {
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// 1 ms 동안 아무것도 하지 않음으로써 매우 느린 코드를 실행한다.
}
return <p>PostsTab</p>;
};
const ContactTab = () => {
return <p>ContactTab</p>;
};
const ContactTab = () => {
return <p>ContactTab</p>;
};
📦 주의 사항)
1️⃣ startTransition
- 동기 함수여야 한다.
- transition으로 표시된 setState는 다른 setState 업데이트 시 중단된다.
- 다른 상태 업데이트가 있을 경우 우선 순위에서 밀린다.
- 텍스트 입력을 제어하는데 사용할 수 없다.
- input에 사용하게 되면 사용자의 입력이 즉각적으로 반영이 되지 않는 경우도 생기기에 좋지 못한 선택이다.
✅ useDefferedValue 🔗
UI 일부의 업데이트를 지연시킬 수 있는 React Hook이다.
- 지연시키려는 값을 매개변수로 받으며, 초기 랜더링 시에는 매개변수로 제공한 값과 동일하며, 업데이트가 발생하면 백그라운드에서 새 값으로 리랜더링을 시도하기 전까지 이전 값을 반환하여 UX를 개선시킨다.
📦 사용하는 이유)
- Debounce와 비슷한 효과를 낼 수 있으며, 그보다 뛰어난 성능을 보인다.
- Suspense와 통합되어 새 값으로 인한 백그라운드 업데이트로 UI가 일시 중단되면 fallback이 표시되지 않고 기존의 값이 유지되어 UX에 좋다.
📦 사용 예시)
- 검색을 통해서 리스트를 가져오는 상황에서 다른 검색동안 이전 값을 유지하는데 유용하게 사용될 수 있다.(Debouncing + a).
더보기
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
📦 주의 사항)
- 전달하는 값은 원시값이거나 컴포넌트의 외부에서 생성된 객체여야 한다.
- 랜더링 중에 생성되는 객체(컴포넌트 안에서 생성되는 객체)를 생성하고 즉시 전달하면 렌더링 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있다.
- 사용하는 자체로 추가 네트워크 요청을 방지하지 않기에 주의해야 한다.
- Effect의 실행은 useDefferedValue로 인해 백그라운드 리랜더링이 완료되고 화면에 커밋되어야 실행된다.
✅ useSyncExternalStore 🔗
외부 스토어를 구독할 수 있는 React Hook 즉, 외부 스토어(external store)와 싱크(sync)를 맞추는 훅(use)이다.
- concurrnet 렌더링이 등장하면서 렌더링이 렌더링을 일시중지할 수 있게되었는데 이 일시 중지가 발생하는 문제로 인해 UI에 동일한 데이터가 다른 값을 표시하는 경우가 생겨났고 이러한 문제를 해결하는 방법이다.
- Redux와 MobX와 같은 외부 상태 저장소와 React와의 동기화를 쉽고 안정적으로 수행할 수 있도록 한다.
- 옵저버 패턴을 이용하며 컴포넌트가 store를 구독하게 하여 store가 바뀔 때마다 컴포넌트를 리렌더링하게 할 수 있다.
📦 사용하는 이유)
- concurrent Mode의 문제점을 지속적인 외부 상태와 React 컴포넌트 사이의 상태 동기화를 통해 해결해 줄 수 있다.
더보기
예시)
리액트 뿐만아니라 다양한 라이브러리 및 프레임워크가 사용된 프로젝트가 있다. 이때 여기서 사용하고 있는 스토어(외부 라이브러리)와 리액트에 상태를 통합해서 사용하기 위해서 사용하며, 주로 기존의 비 React 코드와 통합을 할 때 유용하다.
📦 사용 예시)
- 필수로 2개의 매개변수와 선택적으로 1개의 매개변수가 있다.
- subscribe의 경우 함수 내에서 구독과 구독취소를 모두 선언해주어야 한다.
- getSnapshot는 스토어 데이터의 스냅샷을 반환하는 함수로 저장소가 변경되어 반환값이 달라지면 컴포넌트를 리랜더링 한다.
- getServerSnapshot은 데이터의 초기 스냅샷을 반환하는 함수로 hydrate하는 동안에만 사용된다.
더보기
// App.js
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
export default function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
return (
<>
<button onClick={() => todosStore.addTodo()}>Add todo</button>
<hr />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
// todoStore.js
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];
export const todosStore = {
addTodo() {
todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
emitChange();
},
subscribe(listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot() {
return todos;
}
};
function emitChange() {
for (let listener of listeners) {
listener();
}
}
📦 주의 사항)
- snapshot을 캐시해야한다는 오류가 발생할 수 있다.
- 이는 함수가 호출될 때마다 새 객체를 반환한다는 의미이며, getSnapshot 함수는 반환값이 지난번과 다르면 컴포넌트를 리랜더링하기에 실제 변경 사항이 있을 때에만 다른 객체를 반환해야 한다.
- subscribe 함수는 컴포넌트 안에 만들면 안된다.
- subscribe 함수는 실제 변화가 있을 시에만 새롭게 전달되야한다. 그렇지 않으면 구독과 구독취소를 반복해서 일으켜 무한루프에 빠질 수 있다.
✅ useInsertionEffect
useEffect의 버전 중 하나로 DOM 변이 전에 실행된다.
- Effect 로직이 포함되어 컴포넌트가 DOM에 추가되기 전에 setup 함수를 실시하고 제거되기 전에 cleanup 함수를 실행한다.
- undefined를 반환한다.
- 동적으로 스타일을 삽입하는 경우에 사용된다.
📦 사용하는 이유)
1️⃣ CSS-in-JS 라이브러리에서 동적 스타일 삽입하기
일반적으로 사용되는 접근 방식은 3가지가 있다.
1. 컴파일러 사용해서 CSS 파일로 정적 추출
2. 인라인 스타일로 적용 ex) <div style={{ opacity: 1 }}>
3. 런타임에서 <style> 태그 삽입 (권장되지 않음, 스타일 계산 빈도가 훨씬 증가하며, 잘못된 시점에 발생하면 속도 저하의 원인)
이 중 useInsertionEffect는 런타임에 스타일을 주입해야 하는 경우 2번째 문제를 해결하며 사용할 수 있는 방법이다.
2️⃣ 일반적으로 대부분의 개발자는 필요로하지 않는다.
- CSS-in-JS 라이브러리와 같은 제작자를 위해 설계됨
📦 주의 사항)
- 서버 랜더링 중에 실행되지 않고 클라이언트에서만 실행된다.
- 사용이 매우 한정적이므로 필요한지 여부를 잘 확인하고 사용해야 한다.
'CS > 프레임워크&라이브러리' 카테고리의 다른 글
[React] 18버전의 추가된 새로운 기능 (step- 2) (0) | 2024.02.24 |
---|---|
React v18 - 2 (0) | 2024.02.24 |
React v18 - 1 (0) | 2024.02.17 |
프레임워크와 라이브러리 (2) | 2024.01.26 |
리액트와 jQuery의 차이점 (0) | 2023.11.02 |