Next.js 마이그레이션할 필요 없다 글이랑 뭔가 비슷한 느낌일 지는 모르겠지만 완전히 다른 문제다.
왜그런지 공식 블로그 글을 기반으로 간단하게 알려주도록 하겠다.
한국 시각으로 2024년 12월 6일, React 19가 정식 출시되었다.
내 글에서 연말에 나올 거라 예측했는데, 이 예측이 맞았다.
물론 그다음 예측에 Vercel의 기여 폭주로 빨리 나올까 예상은 했지만, Suspense 문제 덕분에 틀렸다.
아무렴 어떤가, 그럼 왜 React 19 업그레이드 필요가 없다는 건가?
먼저 대상을 얘기하겠다.
바로 SPA 위주로 개발하고 있는 개발자들 대상이다.
Next.js로 개발한다면, 업글하는 순간 SSR과 <form>
, 서버 기능과 커스텀 요소 지원까지 혜택 개쩐다.
하지만 SPA의 향상점은 그리 많지 않다.
차근차근 공식 블로그 글을 기반으로 설명 시작하겠다.
혹시 startTransition
함수를 안다면, 여기서 하나의 유틸리티 훅 함수가 19버전부터 생긴다.
useTransition
훅 함수는 비동기 업무와 진행 상태를 한 번에 해결해준다.
이 훅 덕분에 서버에서 데이터 변경 시 좀 더 쉽고 간결하게 비동기 업무를 수행할 수 있게 되었다.
참고로, startTransition
의 비동기 지원은 19부터 시작되었다. 따라서 18 이하에 startTransition
함수 내 비동기 쓸 생각은 하지 마라!
있기 전
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};
return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
프론트엔드를 깊게 파본 개발자라면 알겠지만, 브라우저 이벤트 콜백은 비동기를 지원하지 않는다.
그래서 함수 리턴값이 Promise
기반이면 브라우저 오류 메시지는 덤.
따라서 event.preventDefault()
함수는 비동기 상에서 실행하면 씹히면서 콘솔에 오류 메시지까지 뿜뿜해버린다.
나는 그런 특성을 알기 때문에 then
문법을 쓰지만... 달라질 게 있나...
생긴 후
function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
})
};
return (
<div>
<input value={name} onChange={(event) => setName(event.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}
useTransition
훅은 진행 중 상태와 업무처리를 한 번에 해결해주는 훅 함수다. 그래서 이렇게 간단하게 해결 가능하다.
물론 startTransition
함수 단독으로 써도 상관은 없다. 대신 옛날처럼 진행 중 상태가 필요할 뿐이지.
이 기능은 그나마 SPA에서도 혜택을 볼 수 있는 신규 훅 함수다.
React 19의 업그레이드 주요 요인 중 하나가 바로 <form>
기능 향상이다.
이 훅은 폼 양식과 오류 상태까지 한번에 해결해주는 함수가 되겠다.
위 예시를 이 훅 함수로 교체하면 아래와 같이 된다.
function ChangeName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get("name"));
if (error) {
return error;
}
redirect("/path");
return null;
},
null,
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
여기서 키포인트를 알려주자면,
error
즉, 업무 결과값 중 오류 메시지 담당즉, 상태값과 비동기 업무 함수, 그리고 진행 상황을 한 번에 관리하는 훅이 되겠다.
React 19 부터는 action에 비동기 함수 추가가 가능하다. 위 예시를 기반으로 이렇게 함수 지정이 가능하다는 뜻이다.
function ChangeName({ name, setName }) {
const [error, setError] = useState(null);
const submitAction = async (formData) => {
const error = await updateName(formData.get("name"));
if (error) {
setError(error);
}
redirect("/path");
};
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}
물론 함수 메모이제이션을 원한다면 useCallback
을 감싸도 된다.
이 기능은 Next.js App Router 환경에서 react-hook-form
쓸 게 아니면 질리도록 쓸 패턴이 될 테니 암기하도록.
폼 양식 안에서 부모 폼의 상태값을 가져오는 훅이다. 사용법이야 별 거 없다.
import {useFormStatus} from 'react-dom';
function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
단, 주의 사항으로 이 컴포넌트는 <form>
태그 또는 기반 컴포넌트의 자식으로 넣어야 하며, 안그러면 아무것도 없다.
여기서 눈치챈 사람이 있겠지만, 이제 <form>
태그는 React 자체적으로 Context
를 가지게 된다는 얘기도 된다.
물론 로우레벨로 Context 가져오는 방법은... 모르겠다. 혹시 생기면 공유하도록 하겠다.
단어 뜻을 알면 뭐 워낙 유명한 '낙관적' 상태를 관리하는 훅이다. 리액트 차기 버전에서 한국인들이 가장 관심을 가진 훅 함수가 바로 이거였다.
예전에 velog 내 어떤 글이었는지 잊었지만, 별점을 주거나 좋아요를 누를 때, '변경 후 적용' 이 아닌, '일단 적용한 뒤 변경'에 대한 실속있는 예시 글이 있었다. 누가 썼는지 까먹었지만 발견하면 예시 공유하도록 하겠다.
사용법은 아래와 같다.
function ChangeName({currentName, onUpdateName}) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName); // 바꿨다고 치자.
const updatedName = await updateName(newName); // 실제로 바꾸는 업무
onUpdateName(updatedName); // 진짜 바꿨다고 부모 컴포넌트에 알려주자
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}
여기서 좀 불편한 점이 있다면, 아무래도 낙관적 상태관리는 제어가 가능한 패턴을 적용해야 한다는 점이다.
이 불편한 점은 어자피 큰 부분은 아니기 때문에, 특히 실용적인 예시로 좋아요나 별점 줄때 말이지.
대부분 단적인 값을 적용하는 사례이므로 이런 불편함은 감수할 가치가 있다.
사실상 React 19의 주인공이다. 비동기 상태제어를 제공하는 유일한 훅이다.
하지만 안타깝게도... 컴포넌트 본문 내의 비동기는 지원하지 않는다.
이유는 간단하다. 상태 바뀌면 함수 갈아엎어지는데 본문의 Promise가 갈아지면 어떻게 되겠어?
만약 그런 시나리오를 원한 경우, Tanstack Query가 있으므로 이놈을 쓰도록 하자.
근데 이걸로 끝나면 심심하지. 이 훅의 특징은 아래와 같다.
Promise
및 Context
를 받는다.if
문 등의 제어문 내에서도 사용 가능하다. for
같은 반복문도!하지만 가장 특징적인 단점이라면, 일회성 훅이라는 것이다.
그래서 비동기를 메모이제이션 하면 해결될 것 같아 useMemo
로 해결하면 어떨까 생각은 이미 리액트 공식에서 정해놓은 한계 상 일치감치 접어두게 만드는 한계다.
즉, useId
처럼 한번 뱉으면 역할은 끝난다. 재활용? 없다.
그리고 <Suspense>
컴포넌트가 강제된다. React.lazy
와 달리 외부 리소스 관리의 효율성도 없다.
하지만 공식에서는 위와 같은 문제를 차근차근 개선한다고 하니, 미래가 창창한 훅이 되겠다.
가장 중요한 점은, 리액트 19부터 공식적인 방법으로 비동기 지원의 신호탄이라는 것이다.
(2024-12-19) 혼란 줘서 미안. 리액트 공식에서는 19 정식 버전부터 '서버 함수'라 지칭하였다. 서버 액션은 정식 차용 전 용어이므로 참고만 하도록 하자.
React 19의 주요 컨텐츠가 되겠다.
서버 컴포넌트는 상태관리가 없다.
서버 함수는 서버 자원을 빌릴 수 있는 함수 매커니즘을 제공한다.
지금 당장에는 Next.js 를 위한 기능이긴 하다.
조만간 Remix 등의 다른 프레임워크나 환경에서 차용되길 기대하도록 하자.
참고로 Remix 에서는 저거 도입 안한다고 한다. 왜냐고? React Router에 도입할 거니까!
그렇다. ref
에 특수한 개체를 제공하지 않는 이상, forwardRef
같이 ref
컴포넌트 래퍼로 감쌀 이유가 없어졌다는 것이다.
그리고, ref
에 함수를 주입할 경우, useEffect
리턴 함수처럼 정리할 업무까지 담을 수 있게 되었다.
드디어 리액트도 타 라이브러리 친화력이 생긴 것이다. 이건 스벨트에게 위협적인 기능이 될 수 있다.
Next.js 에서는 이 오류 메시지 때문에 개고생을 한 이력이 있다고 한다.
그래서 Vercel이 폭주하지 않았나 싶기는 한데, 어쨌든, 알고보니 React 문제였고 그 둘의 협력으로 좀 더 친절하게 해결되었다.
이제 뭔소린지 알 수 없는 하이드레이션 오류 때문에 문서 뒤지고 자시고 이런 일이 줄었다는 건 천만다행이다.
그럼 Consumer는 어디에?
useContext
훅use
훅떡하니 있다. 더 말할 거 없지?
아, Class Component는 어떡하냐고? 솔직히 관심은 없는데 한번 알아봤더니, 클래스 컴포넌트는 Context 사용 방식이 2가지 방법이 있었는데,
static contextType
<Context.Consumer>
여기서 2번째 방법이 증발해버렸다는 거다. 물론 deprecated
단계라 당장 없어지지는 않았지만...
조만간 클래스 컴포넌트가 사라질 날도 멀지 않았다는 반증이기도 하지.
한때 SPA 개발자들 빡치게 했던 Suspense 컴포넌트 동작 매커니즘 문제를 해결했는데,
일단 내가 요약해 줄테니, 만약 자세한 내막을 원하면 딴사람이 조만간 쓸 테니 그 글 보면 되겠다.
이전에는 Suspense 내 fallback 속성이 렌더링 된 상태에서 실제 내용이 렌더링된 다음, 전체 컴포넌트를 렌더링했다. 이렇게 해버리니, SPA 상의 렌더링 매커니즘 차이를 알지 못한 채 Vercel의 렌더링 순서 변경 커밋 이후 SSR에서는 멀쩡했지만 SPA 상에서 폭포수처럼 순차적으로 Suspense가 렌더링되고 나서야 전체 페이지가 렌더링되는 거지같은 문제가 발생했던 것이다.
즉, 기본 매커니즘의 문제가 의도치 않게 Vercel에 의해 발견되어 우린 그것도 모르고 Vercel을 욕했지만, 알고보니 같은 피해자였던 것이었다. (근데 SSR 위주로 테스트하고 커밋했으니 미안하다고 말하기 어렵다 흥!)
이제 Suspense 컴포넌트를 fallback 속성만 렌더링하고, 나머지 컴포넌트를 렌더링한 다음, 그다음 본문이 끝나면 렌더링되는 식으로 개선했다. 대체 여태까지 왜 이렇게 했을까? 이걸 고치는 사람 입장에서도 의문을 가졌을 법 하다.
사실상 SPA에게도 혜택이 주어진 셈이 하나 더 생겼다. 꽤 좋은 혜택이다...
꽤... 좋지... 왜냐면 확실히 이런 패널티가 Vercel의 막무가내식 참여가 아니었으면 아무도 발견 못했으니까.
또하나의 의문은 SSR에는 별 영향이 없다는 것이다.
서버 렌더링은 특성 상 후자로밖에 뿌릴 수밖에 없다. 서버 상에서 <Suspense>
를 처리하면 서버상에서 본문 컴포넌트를 기다리는 바보같은 짓을 하게 된다. 그렇게 되면 우리가 fallback
속성의 내용을 볼 수가 없게 되는 건 덤.
이런 식이면 느려터진 페이지 렌더링 문제가 자연스레 당연히 따라오게 되니, 진짜 그랬으면 아무도 SSR 안 썼지. 졸라게 비효율적으로 느려 터진 SSR을 누가 쓰니?
서버 컴포넌트가 SSR을 살린 셈이 됐다. 이게 의도한 것인지 아닌 것인지는 까봐야 알겠지만.
아래는 이번 본문의 목적에서 중요한 부분이 아니니 넘어가겠다.
<link>
태그 및 <meta>
태그 지원<script async>
지원엄밀히 말하면, 당장 업글할 필요가 없는 이유를 말한 거다.
지금까지 나열한 것만 봐도 SPA 개발자들에게는 당장 업글할 만한 명분을 찾을 수 없다는 것을 알텐데 말이지.
useTransition
: 그냥 상태관리 하나 내장된 유틸리성 훅useActionState
: 이것도 그냥 상태관리 하나 더 내장된 유틸리성 훅useActionState
, <form action={Promise}>
:react-hook-form
걷어낼 것인가 말 것인가?useOptimistic
: 너희들 이건 솔직히 땡기는거 알아. 근데 이거 하나 때문에?use
: 계륵 혹은 @tanstack/query
ref
: 좋아지긴 했어. 하지만 업글하기에 이녀석의 마이그레이션 비용이 비싸다면?Context
: 클래스 컴포넌트 개발자에게 큰 장벽이 생겼다.개발자가 죽으면 먼저 가있던 기술 스택이 마중나온다는 얘기가 있다.
나는 이 이야기를 무척 좋아한다.
개뜬금없다고? 어 맞다. 내가 노린 거야.
근데 그 어느 기술 스택이라도, 업그레이드는 상당한 비용을 요구한다. 비록 JS라도 말이지.
Vue 2에서 3으로 마이그할 때도, 물론 @vue/compat
패키지 등으로 빠르게 마이그할 수 있는 패키지를 공식 제공하고는 있지만,
스케일이 클 수록 업그레이드 비용은 비례해서 증가하는 건 당연한 일이지.
안정적으로 준비한 개발 템플릿이 과연 신 버전에 업글한다 한들, 신 버전에 준비되어 있는지...
상당한 고민과 비용이 수반된다는 것이다.
이를 SI에서는 해결하지 않는다. 그런 인프라가 없거나, 그럴 능력이 없기 때문에.
그래서 자바를 업글한들, 스프링을 업글한들, 결국에 코딩하면 자바 1.6 시절 20년전 패턴에 맴돌고 있는 것이다.
프론트엔드도 jQuery 아니면 시체고 말이지.
리액트도 이런 고민에 빠지는데, 신규라도 마찬가지로, 준비가 됐느냐 이거다.
그리고, 대체적으로 초기 버전은 어떤 버그나 이슈가 발생할지도 모르는 상황에 써야 할 가치가 충분한가도 고려해야 한다.
그래서, SPA만 바라볼 때, 업그레이드 이점은 현저히 혜택이 줄어든다. 대부분 서버와 관련된 기능들이기 때문에.
하지만 서버 중점적인 기능으로 바라봤을 때, 비약적인 발전이라고 볼 수 있다.
그래서 이렇게 결론내렸다.
(2024-12-19) SSG는 어디에 해당하냐고? SSG는 빌드 타임때 서버 잠깐 건드릴 뿐이므로 서버 컴포넌트는 쓸 수 있지만 SPA가 주력이기 때문에 하등 차이가 없다. 따라서 SPA 따라가면 된다. Astro 또한 해당되는 얘기니 새겨듣도록.
끗.
리액트는 훅이 줄어드는거 같으면서도 오히려 생각해야할 훅이 더 늘어나는 것 같네요.
정적인 페이지 만드는 입장에서는 ui 컴퍼넌트 말고는 도입하기가 두려워요 어떻게 또 바뀌려나..
ssr, meta framework 가 정말 좋은거 같긴하지만 저에겐 svelte, astro, lit 만한게 없네요