dohun.log

[React 코드 읽기] 1. formAction 본문

Study/React

[React 코드 읽기] 1. formAction

dohun31 2024. 12. 21. 00:42

최근에 React 19가 정식 출시됐다.

몇 달 전 FETR(사내 FE 기술 리뷰 시간)에서 몰리가 소개한 익숙한 스펙들이 있었는데, 내부 코드가 궁금해져서 하나씩 살펴보기 시작했다.

대형 오픈소스답게 릴리스 문서에 각 API가 개발된 PR이 친절하게 링크되어 있어서 하나씩 읽어보기로 했고, 그 첫 번째로 formAction과 useFormStatus를 선택했다.

 

첫 번째로 이것을 고른 이유는 FETR에서 관련 훅 설명을 들으면서 생긴 궁금증 때문이다. form 태그를 사용했을 때 내부의 button이 어떻게 status를 알 수 있는 걸까? 어떻게 기본 HTML 태그가 Context처럼 동작하는 걸까? 이런 의문이 들었고, 그래서 첫 번째로 선정했다.

formAction

formAction은 이번 React 19에 추가된 신규 API로 form 태그에 action props를 넘길 수 있고, input이나 button에 formAction이라는 props를 넘길 수 있다.

form에 있는 action attr은 기존에 알고있던 스펙이었는데 input이나 button에 전달하는 formAction은 React에서 만든 스펙인줄 알았더니 원래 있던 스펙이었다. (공부좀 해..!)

 

그래서 MDN에서 찾아봤더니 이렇게 설명하고 있다. (button, input)

  • 제출한 정보를 처리할 URL
  • form의 action attributes보다 우선순위가 높음

 

아무튼 PR을 하나씩 읽어보자.


 

Allow empty string to be passed to formAction

 

PR 이름에서 알 수 있듯이 form의 formAction props에 빈 문자열을 넣을 수 있게 처리했다.

 

 

Add (Client) Functions as Form Actions

 

이제 formAction의 내부 구현이 추가되었다.

} else if (tag === 'input' || tag === 'button') {
if (key === 'action') {
  console.error(
    'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.',
  );
} else if (
  tag === 'input' &&
  props.type !== 'submit' &&
  props.type !== 'image' &&
  !didWarnFormActionType
) {
  didWarnFormActionType = true;
  console.error(
    'An input can only specify a formAction along with type="submit" or type="image".',
  );
} else if (
  tag === 'button' &&
  props.type != null &&
  props.type !== 'submit' &&
  !didWarnFormActionType
) {
  didWarnFormActionType = true;
  console.error(
    'A button can only specify a formAction along with type="submit" or no type.',
  );

위에 첨부해둔 MDN 문서에서 확인한것 처럼 button, input에 formAction을 전달해서 사용하려면 button의 경우 type이 submit이어야 하고 input인 경우에는 type이 submit이거나 image(input의 경우)이어야한다.

 

이런 조건을 위 코드에서 하나씩 확인한다.

 

그리고 코드를 읽다보면 sumitter라는게 계속 등장하는데 처음엔 React에서 만들어낸 스펙인줄 알았다. 그런데 코드를 보면 navtiveEvent에서 꺼내 사용하고 있길래 찾아봤더니 SubmitEvent에 있는 속성이었다..! (공부 좀 해 22..)

아까 위에서 button과 input의 formAction attr 우선순위는 form태그의 action attr보다 높다고 확인했었는데, 내부 구현에서도 스펙대로 구현해둔것을 볼 수 있다.

const form: HTMLFormElement = (nativeEventTarget: any);
let action = (getFiberCurrentPropsFromNode(form): any).action;
const submitter: null | HTMLInputElement | HTMLButtonElement =
  (nativeEvent: any).submitter;
let submitterAction;
if (submitter) {
  const submitterProps = getFiberCurrentPropsFromNode(submitter);
  submitterAction = submitterProps
    ? (submitterProps: any).formAction
    : submitter.getAttribute('formAction');
  if (submitterAction != null) {
    // The submitter overrides the form action.
    **action = submitterAction;**
  }
}

action은 먼저 form태그의 Fiber노드에서 action props로 초기화되고, nativeEvent.submmiter의 Fiber노드에 formAction props가 존재한다면 action에 대입해준다.

 

그리고 submitForm함수를 만들어 queue에 push해준다.

function submitForm() {
  if (nativeEvent.defaultPrevented) {
    // We let earlier events to prevent the action from submitting.
    return;
  }
  // Prevent native navigation.
  event.preventDefault();
  let formData;
  if (submitter) {
    // The submitter's value should be included in the FormData.
    // It should be in the document order in the form.
    // Since the FormData constructor invokes the formdata event it also
    // needs to be available before that happens so after construction it's too
    // late. The easiest way to do this is to switch the form field to hidden,
    // which is always included, and then back again. This does means that this
    // is observable from the formdata event though.
    // TODO: This tricky doesn't work on button elements. Consider inserting
    // a fake node instead for that case.
    // TODO: FormData takes a second argument that it's the submitter but this
    // is fairly new so not all browsers support it yet. Switch to that technique
    // when available.
    const type = submitter.type;
    submitter.type = 'hidden';
    formData = new FormData(form);
    submitter.type = type;
  } else {
    formData = new FormData(form);
  }
  // TODO: Deal with errors and pending state.
  action(formData);
}

 

그리고 테스트 코드를 확인해보면 더 쉽게 이해할 수 있는데, rootActionCalled가 false이고 실제로 submit했을때 form 태그의 action은 호출되지 않음을 확인할 수 있다.

it('should allow passing a function to an input/button formAction', async () => {
  const inputRef = React.createRef();
  const buttonRef = React.createRef();
  let rootActionCalled = false;
  let savedTitle = null;
  let deletedTitle = null;

  function action(formData) {
    rootActionCalled = true;
  }

  function saveItem(formData) {
    savedTitle = formData.get('title');
  }

  function deleteItem(formData) {
    deletedTitle = formData.get('title');
  }

  function App() {
    return (
      <form action={action}>
        <input type="text" name="title" defaultValue="Hello" />
        <input
          type="submit"
          formAction={saveItem}
          value="Save"
          ref={inputRef}
        />
        <button formAction={deleteItem} ref={buttonRef}>
          Delete
        </button>
      </form>
    );
  }

  const stream = await ReactDOMServer.renderToReadableStream(<App />);
  await readIntoContainer(stream);
  await act(async () => {
    ReactDOMClient.hydrateRoot(container, <App />);
  });

  expect(savedTitle).toBe(null);
  expect(deletedTitle).toBe(null);

  submit(inputRef.current);
  expect(savedTitle).toBe('Hello');
  expect(deletedTitle).toBe(null);
  savedTitle = null;

  submit(buttonRef.current);
  expect(savedTitle).toBe(null);
  expect(deletedTitle).toBe('Hello');
  deletedTitle = null;

  expect(rootActionCalled).toBe(false);
});

 

Rethrow errors from form actions

 

비동기 작업을 완변하게 지원하기 위해서 transition 적용했다.

#submitForm 핸들러에서 이전 PR에서 만들었던 action을 그대로 호출했었는데 startHostTransition를 호출하고, 인자로 action 함수를 넘긴다.

startHostTransition 함수 내부에선 startTransition을 호출하고 있고
주석을 읽어보면 해당 host component (form)는 stateful하게 업그레이드 되었다고 한다!

 

HostComponet란? (ChatGPT한테 물어봄)

  • React가 렌더링할 수 있는 기본 HTML 요소 또는 네이티브 컴포넌트
  • 브라우저의 DOM API와 직접적으로 상호작용하게 되는 요소
  • ex) form, input, div 등등

 

Restore server controlled form fields to whatever they should be

 

Insert temporary input node to polyfill submitter argument in FormData

 

submitter가 없는 버전을 위한 Polyfill 추가

 

 

 


정리

React 코드라고 하면 너무 거대해서 읽기 어렵고 막막하기만 했는데 생각보다 CHANGELOG에 친절하게 API별로 작업한 PR을 남겨줘서 보기 편했던것 같다. 그리고 이번에 테스트 코드 덕분에 어떤 작업이 추가됐는지, 어떤 부분을 중점적으로 보면되는지 쉽게 파악할 수 있었다. 회사에서도 한번 이렇게 테스트 코드를 도입해봐도 좋을것 같은 느낌이다. 내년에 Action Item으로 추가해야겠다.

 

다음엔 useFormStatus 코드를 읽고 정리해보려고 한다. 사실 React 19 문서를 봤을때 제일 궁금했던 API가 useFormStatus여서 제일 기대된다. Context가 아닌 form태그가 어떻게 Context 처럼 동작하는건지 너무 궁금했는데 formAction을 읽으면 대충 힌트를 찾은것 같다.

'Study > React' 카테고리의 다른 글

react-konva로 알아보는 Declarative Canvas  (6) 2024.01.07
Comments