[React] React19의 새로운 기능들
React19가 등장하면서 useTransition
, useActionState
, useOptimistic
와 같은 다양한 Hook들이 개선되고 추가되었다. 또한 title, meta 태그를 컴포넌트에서도 작성할 수 있게 되었다.
이번 게시글에선 hook들을 중심적으로 알아보자.
사실 useState
, useEffect
말곤 크게 사용한 적 없었기에, 이를 학습하면서 어떤식으로 코드를 작성하면 더 깔끔하게 작성할 수 있을까 고민해보자.
상황 가정
한 웹서비스에서 랜덤으로 닉네임을 생성해주는 기능이 있다 가정해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export async function randomNickname() {
const firstWord = ['김', '이', '최', '박', '한'];
const secondWord = ['이', '지', '고', '효', '승'];
const thirdWord = ['웅', '은', '린', '이게설마걸리겠어', '연'];
function pickWords() {
const pickFirst = firstWord[Math.floor(Math.random() * firstWord.length)];
const pickSecond = secondWord[Math.floor(Math.random() * secondWord.length)];
const pickThird = thirdWord[Math.floor(Math.random() * thirdWord.length)];
return pickFirst + pickSecond + pickThird;
}
// 2초 지연 가정
await new Promise(resolve => setTimeout(resolve, 2000));
// 20% 확률로 서버오류 가정
if (Math.random() < 0.2) {
throw new Error('랜덤 닉네임 생성 중 오류가 발생했어요.');
}
return pickWords();
}
뭐 진짜 억지 예시이긴하다. 그래도 한번 알아보도록 하자.
기존의 hook 사용
간단하다. useState
로 error, nickname 그리고 받아오고 있는지를 isPending으로 설정하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function App() {
const [nickName, setNickName] = useState<string | null>(null);
const [isPending, setIsPending] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
async function handleGenerateNickname() {
try {
setIsPending(true);
setError(null);
const newNickname = await randomNickname();
setNickName(newNickname);
} catch (error : any) {
setError(error.message);
setNickName("")
} finally {
setIsPending(false);
}
}
return (
<>
<div>
<h2>기존의 Hook 사용 방식</h2>
<button
onClick={handleGenerateNickname}
>
{isPending ? "닉네임을 가져오는 중입니다." : "랜덤 닉네임 생성"}
</button>
{nickName && !error && (
<p>생성된 닉네임: {nickName}</p>
)}
{error && (
<p style={{ color: 'red' }}>{error}</p>
)}
</div>
</>
)
}
이게걸리네
useTransition
을 활용한 isPending 설정
useTransition
를 활용하면 작업이 진행되는 동안 isPending값을 true로 전환되어 기존의 방식과 동일하게 구현 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function App() {
const [nickName, setNickName] = useState<string | null>(null);
// useTransition을 사용!
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
// startTransition 함수 내부에 비동기 함수를 넣어 실행 => 작업을 처리하는 동안 isPending의 값이 true로 바뀜.
function handleGenerateNickname() {
startTransition(async function() {
try {
setError(null);
const newNickname = await randomNickname();
setNickName(newNickname);
} catch (error : any) {
setError(error.message);
setNickName("")
}
}
)
}
return (
<>
<div>
<h2>useTransition활용</h2>
<button
onClick={handleGenerateNickname}
>
{isPending ? "닉네임을 가져오는 중입니다." : "랜덤 닉네임 생성"}
</button>
{nickName && !error && (
<p>생성된 닉네임: {nickName}</p>
)}
{error && (
<p style={{ color: 'red' }}>{error}</p>
)}
</div>
</>
)
}
export default App
동작도 똑같이 잘 한다. 코드도 뭔가 더 간결해진 느낌이다.
useActionState
를 사용한 요청처리
React19에 새로 추가된 hook. form태그의 상태를 추적하여 form이 제출될 때 발생하는 비동기 작업에 따른 상태를 자동으로 업데이트한다.
즉 쉽게말하면, form Tag를 사용해서 감싸준다면..? 이를 활용할 수 있다는 의미이다. 예시를 통해 알아보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function App() {
const [nickName, submitAction, isPending] = useActionState<any>(
async function() {
try {
const newNickname = await randomNickname();
return newNickname;
} catch (error) {
return error;
}
}, ""
)
return (
<>
<div>
<h2>useActionState활용</h2>
{/*form 태그로 감싼다.*/}
<form action={submitAction}>
<button
type="submit"
disabled={isPending}
>
{isPending ? "닉네임을 가져오는 중입니다." : "랜덤 닉네임 생성"}
</button>
{!(nickName instanceof Error) && !isPending && <p>{nickName}</p>}
{nickName instanceof Error && <p>Error: {nickName.message}</p>}
</form>
</div>
</>
)
}
export default App
위 코드를 보면 함수의 실행 결과가 세가지를 요소로하는 배열에 담겨 반환되는 것을 볼 수 있다.
세가지 요소가 의미하는 것
첫번째는 useActionState
에 들어갈 비동기 함수의 return값이 반환된다. 두번째 요소는 form이 submit 버튼을 통해 제출될 때 실행되는 함수이다. 세번째는 작업을 처리하고 있는지 여부이다.
useActionState
에 넣어야 하는 값
비동기함수와, 해당 state의 초기값을 넣는다.
useOptimistic
작업이 성공할 것이라 낙관적으로 가정하고, 이를 화면에 반영하여 사용자 경험을 개선하기 위한 훅이다. 작업을 진행하는 동안, 로딩창을 띄우거나 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function App() {
const [nickname, setNickname] = useState("");
const [optName, setOptName] = useOptimistic(nickname);
async function onSubmitAction() {
setOptName("⏳⌛️")
console.log(optName)
const newName = await randomNickname()
setNickname(newName)
}
return (
<>
<div>
<h2>useOptimistic 활용</h2>
<form action={onSubmitAction}>
<button type="submit">
랜덤 닉네임 생성
</button>
<p>{optName}</p>
</form>
</div>
</>
)
}
export default App
그래서 이렇게하면 뭐가 좋냐? 좀더 빠른 반응성을 가진 UI를 개발할 수 있다.
여기서 주의해야할 점은 그냥 onClick 이벤트로 설정하거나 그러면 안된단거다.
여기서 좀 막혔었는데
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.
와 같은 오류를 확인하고 form으로 변경한 후 해결했다.
즉, form의 action을 사용해야 정상 적용됨을 확인했다.
결론
위와 같은 훅들, 익숙하지 않은 것은 사실이다.
하지만 확실히 코드가 간결해지는 효과,, 무시 못할 것 같다. 또 공식 hook이기에 custom hook보단 기능적으로 우수하다. 개인적으로 이런게 좀 더 깔끔해보여서 좋아하기도해서 추후 이와 같은 기능을 구현할 때 한번 사용해야겠다.