본문 바로가기

React.js

[React 19] useActionState로 서버 상태 관리하기

들어가며

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>
  );
}

주요 차이점

  1. 통합성: useActionState는 React의 Form Actions와 완벽하게 통합
  2. 간결성: 별도의 상태 관리나 이벤트 핸들러가 불필요
  3. 타입 안전성: 서버 컴포넌트와 함께 사용 시 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 */);
}

성능 최적화 팁 ⚡️

  1. 불필요한 리렌더링 방지
    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);

주의사항 ⚠️

  1. 서버 컴포넌트 제약
    • 서버 액션은 'use server' 지시문이 필요
    • 클라이언트 컴포넌트에서만 useActionState 사용 가능
  2. 에러 처리
    • 서버 에러와 클라이언트 에러 구분 처리
    • 적절한 에러 바운더리 설정
  3. 상태 초기화
  4. function Form() { const [state, action] = useActionState(submitForm); // 성공 후 폼 초기화 useEffect(() => { if (state.data) { formRef.current?.reset(); } }, [state.data]); return (/* JSX */); }

마치며

useActionState는 React Query와 같은 외부 라이브러리의 장점을 React 코어에 내장한 듯한 느낌을 주는 강력한 훅입니다.
서버 컴포넌트와 함께 사용하면 더욱 강력한 기능을 발휘하니, 꼭 활용해보세요!

다음에는 또 다른 React 19의 새로운 기능들을 살펴보도록 하겠습니다.
즐거운 코딩 되세요! 🚀