Post

[React] React19의 새로운 기능들

[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>
    </>
  )
}

randomNickname_기존

이게걸리네

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보단 기능적으로 우수하다. 개인적으로 이런게 좀 더 깔끔해보여서 좋아하기도해서 추후 이와 같은 기능을 구현할 때 한번 사용해야겠다.

This post is licensed under CC BY 4.0 by the author.