들어가며
React 19에서 새롭게 소개된 useActionState
는 서버 액션의 상태를 우아하게 관리할 수 있게 해주는 강력한 훅입니다.
React Query(TanStack Query)에 익숙하신 분들이라면 더욱 반가우실 것 같네요! 😊
useActionState란? 🤔
useActionState
는 서버 액션의 전체 생명주기를 관리하는 훅으로, 다음과 같은 상태를 제공합니다:
pending
: 액션 실행 중 여부error
: 발생한 에러 정보data
: 성공 시 반환된 데이터
기본 사용법 📝
import { useActionState } from 'react';
// 서버 액션 정의
async function submitComment(formData: FormData) {
'use server';
const comment = await db.comments.create({
content: formData.get('content'),
});
return comment;
}
function CommentForm() {
const [state, action] = useActionState(submitComment);
return (
<form action={action}>
<textarea name="content" />
<button disabled={state.pending}>
{state.pending ? '댓글 작성 중...' : '댓글 작성'}
</button>
{state.error && (
<div className="error">
오류 발생: {state.error.message}
</div>
)}
{state.data && (
<div className="success">
댓글이 작성되었습니다!
</div>
)}
</form>
);
}
React Query와의 비교 🔄
React Query 방식
import { useMutation } from '@tanstack/react-query';
function CommentForm() {
const mutation = useMutation({
mutationFn: (content: string) =>
axios.post('/api/comments', { content }),
onSuccess: () => {
queryClient.invalidateQueries(['comments']);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate(content);
};
return (
<form onSubmit={handleSubmit}>
{/* 폼 내용 */}
</form>
);
}
useActionState 방식
function CommentForm() {
const [state, action] = useActionState(submitComment);
// 자동으로 폼 데이터 처리
return (
<form action={action}>
{/* 폼 내용 */}
</form>
);
}
주요 차이점
- 통합성: useActionState는 React의 Form Actions와 완벽하게 통합
- 간결성: 별도의 상태 관리나 이벤트 핸들러가 불필요
- 타입 안전성: 서버 컴포넌트와 함께 사용 시 end-to-end 타입 안전성 제공
실전 활용 예시 💡
1. 낙관적 업데이트와 함께 사용
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [state, action] = useActionState(async (formData: FormData) => {
'use server';
const newTodo = await createTodo(formData);
return newTodo;
});
// 낙관적 업데이트 처리
useEffect(() => {
if (state.data) {
setTodos(prev => [...prev, state.data]);
}
}, [state.data]);
return (
<div>
<form action={action}>
<input name="title" />
<button type="submit">
{state.pending ? '추가 중...' : '할 일 추가'}
</button>
</form>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
isPending={state.pending}
/>
))}
</ul>
</div>
);
}
2. 복잡한 폼 처리
function RegistrationForm() {
const [state, action] = useActionState(async (formData: FormData) => {
'use server';
// 서버 사이드 유효성 검사
const email = formData.get('email');
if (!isValidEmail(email)) {
throw new Error('유효하지 않은 이메일입니다.');
}
// 사용자 등록 처리
return await registerUser(formData);
});
return (
<form action={action} className="space-y-4">
<fieldset disabled={state.pending}>
<input name="email" type="email" />
<input name="password" type="password" />
<input name="confirmPassword" type="password" />
<button type="submit">
{state.pending ? '가입 중...' : '회원가입'}
</button>
</fieldset>
{/* 상태별 피드백 UI */}
<StatusFeedback state={state} />
</form>
);
}
3. 여러 액션 조합하기
function UserProfile() {
const [profileState, profileAction] = useActionState(updateProfile);
const [avatarState, avatarAction] = useActionState(updateAvatar);
return (
<div className="profile-container">
{/* 프로필 정보 폼 */}
<form action={profileAction}>
<input name="name" />
<input name="bio" />
<button disabled={profileState.pending || avatarState.pending}>
프로필 업데이트
</button>
</form>
{/* 아바타 업로드 폼 */}
<form action={avatarAction}>
<input type="file" name="avatar" />
<button disabled={profileState.pending || avatarState.pending}>
아바타 업데이트
</button>
</form>
{/* 통합 상태 표시 */}
<StatusIndicator
states={[profileState, avatarState]}
/>
</div>
);
}
고급 패턴 🔥
1. 커스텀 훅으로 재사용
function useFormAction<T>(
action: (formData: FormData) => Promise<T>,
options?: {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
) {
const [state, baseAction] = useActionState(action);
useEffect(() => {
if (state.data && options?.onSuccess) {
options.onSuccess(state.data);
}
if (state.error && options?.onError) {
options.onError(state.error);
}
}, [state, options]);
return [state, baseAction] as const;
}
// 사용 예시
function CommentForm() {
const [state, action] = useFormAction(submitComment, {
onSuccess: () => {
toast.success('댓글이 작성되었습니다!');
},
onError: (error) => {
toast.error(error.message);
},
});
return (/* JSX */);
}
2. 상태 지속성 관리
function PersistentForm() {
const [state, action] = useActionState(submitForm);
const [draft, setDraft] = useState('');
// 드래프트 저장
useEffect(() => {
if (!state.pending) {
localStorage.setItem('form-draft', draft);
}
}, [draft, state.pending]);
// 성공 시 드래프트 클리어
useEffect(() => {
if (state.data) {
localStorage.removeItem('form-draft');
setDraft('');
}
}, [state.data]);
return (/* JSX */);
}
성능 최적화 팁 ⚡️
- 불필요한 리렌더링 방지
function ActionButton({ state }) { // 상태에 따른 버튼 렌더링만 담당 return ( <button disabled={state.pending}> {state.pending ? '처리 중...' : '제출'} </button> ); }
function Form() {
const [state, action] = useActionState(submitForm);
return (
);
}
2. **메모이제이션 활용**
```tsx
const MemoizedStatusIndicator = memo(StatusIndicator);
주의사항 ⚠️
- 서버 컴포넌트 제약
- 서버 액션은 'use server' 지시문이 필요
- 클라이언트 컴포넌트에서만 useActionState 사용 가능
- 에러 처리
- 서버 에러와 클라이언트 에러 구분 처리
- 적절한 에러 바운더리 설정
- 상태 초기화
function Form() { const [state, action] = useActionState(submitForm); // 성공 후 폼 초기화 useEffect(() => { if (state.data) { formRef.current?.reset(); } }, [state.data]); return (/* JSX */); }
마치며
useActionState
는 React Query와 같은 외부 라이브러리의 장점을 React 코어에 내장한 듯한 느낌을 주는 강력한 훅입니다.
서버 컴포넌트와 함께 사용하면 더욱 강력한 기능을 발휘하니, 꼭 활용해보세요!
다음에는 또 다른 React 19의 새로운 기능들을 살펴보도록 하겠습니다.
즐거운 코딩 되세요! 🚀
'React.js' 카테고리의 다른 글
[React 19] useFormStatus로 더 똑똑한 폼 UI 만들기 (0) | 2025.03.17 |
---|---|
[React 19] useOptimistic으로 낙관적 UI 구현하기 - 좋아요 버튼 UX 개선하기 (0) | 2025.03.17 |
[React 19] useTransition으로 더 부드러운 UI 구현하기 (0) | 2025.03.17 |
[React] 렌더링 최적화하기 - React.memo 현명하게 사용하기 (0) | 2025.03.17 |
[React] 웹 페이지 복귀, 이탈 탐지 (가시성) (0) | 2023.06.28 |