[React 19] 공식문서 톺아보기 - React DOM 컴포넌트 (2)

Study
form, input, link 와 같은 브라우저 컴포넌트가 리액트에서 어떻게 활용될 수 있는지에 대해 공부하였습니다.

form

내장 브라우저 <form> 컴포넌트는 정보 제출을 위한 대화형 컨트롤을 만들 수 있습니다.

<form action={search}>
  <input name='query' />
  <button type='submit'>Search</button>
</form>
<form action={search}>
  <input name='query' />
  <button type='submit'>Search</button>
</form>
  • action: URL 혹은 함수. URL을 action을 통해 전달하면, 폼은 HTML 폼 컴포넌트처럼 동작합니다.
    • 함수를 action이나 formAction에 전달하면, HTTP 메서드는 method 프로퍼티의 값과 관계없이 POST로 처리합니다.
    • url이 아니라 함수를 전달하게 될 경우 formData가 함수에 인수로 전달되어, 폼에서 전달된 데이터에 접근할 수 있습니다.

점진적 향상 지원하는 방법

자바스크립트가 활성화 되기전에 폼 제출을 하게 해준다. 이는 네트워크 속도가 느린 환경에서 좋은 선택지가 될 수 있다.

  • 일반함수

    • onSubmit에 일반함수, action에 url
    // 서버 API 엔드포인트 (예: /api/submit)
    async function submitToServer(formData) {
      const res = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      return res.json();
    }
    
    // 클라이언트
    function MyForm() {
      const handleSubmit = async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        await submitToServer(formData); // JS 활성화 시 비동기 처리
      };
    
      return (
        <form
          action='/api/submit' // JS 미로드 시 폴백 URL
          method='POST'
          onSubmit={handleSubmit} // JS 로드 후 가로채기
        >
          <input name='email' />
          <button>Submit</button>
        </form>
      );
    }
    // 서버 API 엔드포인트 (예: /api/submit)
    async function submitToServer(formData) {
      const res = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      return res.json();
    }
    
    // 클라이언트
    function MyForm() {
      const handleSubmit = async (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        await submitToServer(formData); // JS 활성화 시 비동기 처리
      };
    
      return (
        <form
          action='/api/submit' // JS 미로드 시 폴백 URL
          method='POST'
          onSubmit={handleSubmit} // JS 로드 후 가로채기
        >
          <input name='email' />
          <button>Submit</button>
        </form>
      );
    }
  • useActionState

    import { useActionState } from 'react';
    
    async function submitForm(prevState, formData) {
      // 서버 로직
    }
    
    function MyForm() {
      const [state, action, isPending] = useActionState(submitForm, null);
    
      return (
        <form action={action}>
          <input name='email' />
          <button disabled={isPending}>Submit</button>
        </form>
      );
    }
    import { useActionState } from 'react';
    
    async function submitForm(prevState, formData) {
      // 서버 로직
    }
    
    function MyForm() {
      const [state, action, isPending] = useActionState(submitForm, null);
    
      return (
        <form action={action}>
          <input name='email' />
          <button disabled={isPending}>Submit</button>
        </form>
      );
    }
  • use server

    // 서버 액션 (Next.js)
    async function submitForm(formData) {
      'use server';
      // 데이터 처리
    }
    
    // 클라이언트
    <form action={submitForm}>
      <input name='email' />
      <button>Submit</button>
    </form>;
    // 서버 액션 (Next.js)
    async function submitForm(formData) {
      'use server';
      // 데이터 처리
    }
    
    // 클라이언트
    <form action={submitForm}>
      <input name='email' />
      <button>Submit</button>
    </form>;
    • 서버함수에 추가 인자 전달하는 방법 1 : input태그로 전달하고 formData로 접근
    <form action={submitForm}>
      <input type='hidden' name='secret' value='1234' />
      <button type='submit'>전송</button>
    </form>
    <form action={submitForm}>
      <input type='hidden' name='secret' value='1234' />
      <button type='submit'>전송</button>
    </form>
    • 서버함수에 추가 인자 전달하는 방법2 : bind
      async function addToCart(productId, formData) {
        "use server";
        await updateCart(productId); // productId 사용
      }
    
      // addToCart에 productId를 바인딩한 새 함수 생성
      const addProductToCart = addToCart.bind(null, productId);
    
      return (
        <form action={addProductToCart}>
      async function addToCart(productId, formData) {
        "use server";
        await updateCart(productId); // productId 사용
      }
    
      // addToCart에 productId를 바인딩한 새 함수 생성
      const addProductToCart = addToCart.bind(null, productId);
    
      return (
        <form action={addProductToCart}>
  • 서버함수가 점진적 향상을 지원하기 위한 조건

    • 함수내에 “use server” 선언해야한다.
    • action 속성에 함수를 전달해야한다.
    • formData를 인자로 받는 함수로 정의해야한다.

점진적 향상이 무조건 좋은 기능일까?

  • 순수 정적환경에서는 사용할 수 없다. 서버환경이 필요하다 (Nextjs), 클라이언트 상호작용이 제한적이다. (서버함수 실행 후 페이지 전체 리로드가 기본동작이다.)
  • 실시간 유효성 검사가 어렵다. js 로드 후에는 가능하다.

html 유효성 검사를 이용

<input
  name='email'
  pattern='[^@]+@[^@]+\.[^@]+'
  onChange={(e) => {
    if (!e.target.validity.valid) {
      setError('유효한 이메일을 입력하세요');
    }
  }}
/>
<input
  name='email'
  pattern='[^@]+@[^@]+\.[^@]+'
  onChange={(e) => {
    if (!e.target.validity.valid) {
      setError('유효한 이메일을 입력하세요');
    }
  }}
/>

리액트 훅과의 연동

  • useFormStatus 로 대기상태 표시
    function Submit() {
      const { pending } = useFormStatus(); // form 태그 내부에서 쓰여야 동작한다.
      return (
        <button type='submit' disabled={pending}>
          {pending ? 'Submitting...' : 'Submit'}
        </button>
      );
    }
    function Submit() {
      const { pending } = useFormStatus(); // form 태그 내부에서 쓰여야 동작한다.
      return (
        <button type='submit' disabled={pending}>
          {pending ? 'Submitting...' : 'Submit'}
        </button>
      );
    }
  • useOptimistic 로 낙관적 업데이트
    const [optimisticMessages, addOptimisticMessage] = useOptimistic(
      messages,
      (state, newMessage) => [
        ...state,
        {
          text: newMessage,
          sending: true,
        },
      ],
    );
    const [optimisticMessages, addOptimisticMessage] = useOptimistic(
      messages,
      (state, newMessage) => [
        ...state,
        {
          text: newMessage,
          sending: true,
        },
      ],
    );
  • ErrorBoundary 로 에러처리하기
    <ErrorBoundary fallback={<p>폼 제출 중에 오류가 발생했습니다.</p>}>
      {/* action 함수에 throw new Error()가 필요하다 */}
      <form action={search}>
        <input name='query' />
        <button type='submit'>Search</button>
      </form>
    </ErrorBoundary>
    <ErrorBoundary fallback={<p>폼 제출 중에 오류가 발생했습니다.</p>}>
      {/* action 함수에 throw new Error()가 필요하다 */}
      <form action={search}>
        <input name='query' />
        <button type='submit'>Search</button>
      </form>
    </ErrorBoundary>
  • useActionState 자바스크립트 없이 에러처리하기
    1. <form>이 서버 컴포넌트에서 렌더링. (use server)
    2. <form>의 action 프로퍼티로 전달된 함수가 서버 함수.
    3. useActionState Hook이 오류 메시지를 보여주기 위해 사용.
      async function signup(prevState, formData) {
        "use server";
        const email = formData.get("email");
        try {
          await signUpNewUser(email);
          alert(`Added "${email}"`);
        } catch (err) {
          return err.toString();
        }
      }
      // message에 에러 메세지가 표시
      const [message, signupAction] = useActionState(signup, null);
      return (
          <form action={signupAction} id="signup-form">
      async function signup(prevState, formData) {
        "use server";
        const email = formData.get("email");
        try {
          await signUpNewUser(email);
          alert(`Added "${email}"`);
        } catch (err) {
          return err.toString();
        }
      }
      // message에 에러 메세지가 표시
      const [message, signupAction] = useActionState(signup, null);
      return (
          <form action={signupAction} id="signup-form">

formData

  • 폼 안의 모든 입력값(<input><select> 등)이 자동으로 수집된 객체

  • action 속성에 함수를 전달하게 될 경우 formData가 인수로 전달된다. 그리고 이 데이터에 접근이 가능한다.

  • <form> 태그 내부에 name 속성이 있는 입력 필드가 존재해야 합니다.

  • 폼이 제출되거나 명시적으로 new FormData(formElement)를 호출할 때만 생성됩니다.

    • 접근 예시
    // 1. action 함수에서 name 속성으로 접근
    async function handleSubmit(formData) {
      // formData에서 필드 값 추출
      const username = formData.get("username"); // name 속성으로 접근
    
    // <form action={handleSubmit}>
    
    
    // 2. FormData 생성자로 접근
    function handleSubmit(e) {
      const formData = new FormData(e.target); // 폼 요소로 FormData 생성
      const email = formData.get("email"); // name 속성으로 접근
    
    // <form onSubmit={handleSubmit}>
    
    // 3. useRef로 접근
      const formRef = useRef();
    
      const handleClick = () => {
        const formData = new FormData(formRef.current);
        console.log(formData.get("username"));
      };
    
    // <form ref={formRef}>
    // 1. action 함수에서 name 속성으로 접근
    async function handleSubmit(formData) {
      // formData에서 필드 값 추출
      const username = formData.get("username"); // name 속성으로 접근
    
    // <form action={handleSubmit}>
    
    
    // 2. FormData 생성자로 접근
    function handleSubmit(e) {
      const formData = new FormData(e.target); // 폼 요소로 FormData 생성
      const email = formData.get("email"); // name 속성으로 접근
    
    // <form onSubmit={handleSubmit}>
    
    // 3. useRef로 접근
      const formRef = useRef();
    
      const handleClick = () => {
        const formData = new FormData(formRef.current);
        console.log(formData.get("username"));
      };
    
    // <form ref={formRef}>

다양한 제출 타입 처리하기

  • 사용자가 누른 버튼에 따라 여러 제출 작업을 처리하도록 폼을 설계할 수 있습니다. 폼 내부의 각 버튼은 formAction 프로퍼티를 설정하여 고유한 동작 또는 동작과 연결할 수 있습니다.
  • 사용자가 특정 버튼을 클릭하면 폼이 제출되고 해당 버튼의 속성 및 동작으로 정의된 해당 동작이 실행됩니다. 예를 들어, 폼은 기본적으로 검토를 위해 문서를 제출하지만 formAction이 설정된 별도의 버튼이 있어 문서를 초안으로 저장할 수 있습니다.
  • action의 프로퍼티는 formAction의 속성인 <button>, <input type="submit">, 혹은 <input type="image">로 재정의될 수 있습니다.
export default function Search() {
  function publish(formData) {
    const content = formData.get('content');
    const button = formData.get('button');
    alert(`'${content}' was published with the '${button}' button`);
  }

  function save(formData) {
    const content = formData.get('content');
    alert(`Your draft of '${content}' has been saved!`);
  }

  return (
    <form action={publish}>
      <textarea name='content' rows={4} cols={40} />
      <br />
      <button type='submit' name='button' value='submit'>
        Publish
      </button>
      <button formAction={save}>Save draft</button>
    </form>
  );
}
export default function Search() {
  function publish(formData) {
    const content = formData.get('content');
    const button = formData.get('button');
    alert(`'${content}' was published with the '${button}' button`);
  }

  function save(formData) {
    const content = formData.get('content');
    alert(`Your draft of '${content}' has been saved!`);
  }

  return (
    <form action={publish}>
      <textarea name='content' rows={4} cols={40} />
      <br />
      <button type='submit' name='button' value='submit'>
        Publish
      </button>
      <button formAction={save}>Save draft</button>
    </form>
  );
}
<form action='/default-action'>
  <input type='text' name='name' />
  <button type='submit'>기본 제출</button>
  <button type='submit' formAction='/custom-action'>
    다른 액션으로 제출
  </button>
</form>
<form action='/default-action'>
  <input type='text' name='name' />
  <button type='submit'>기본 제출</button>
  <button type='submit' formAction='/custom-action'>
    다른 액션으로 제출
  </button>
</form>

input

내장 브라우저 <input> 컴포넌트를 사용하면 여러 종류의 폼 입력Input을 렌더링할 수 있습니다.

  • formAction을 사용할 경우 부모컴포넌트 form의 action을 재정의한다.

유효성 검사를 위한 속성들

  • max : 숫자 타입. 숫자 와 날짜 입력들의 최댓값을 지정합니다.
  • maxLength: 숫자 타입. 텍스트와 다른 입력들의 최대 길이를 지정합니다.
  • min : 숫자 타입. 숫자 와 날짜 입력들의 최솟값을 지정합니다.
  • minLength : 숫자 타입. 텍스트와 다른 입력들의 최소 길이를 지정합니다.
  • onInvalid이벤트 핸들러 함수. 폼 제출 시 input이 유효하지 않을 경우 실행되며 invalid 내장 이벤트와 달리 React onInvalid 이벤트는 버블링됩니다.
  • pattern: 문자열 타입. value가 일치해야 하는 패턴을 지정합니다.

입력에 레이블 제공하기

  • label 태그안에 input 태그를 포함시킨다.
    <label>
      Your first name:
      <input name='firstName' />
    </label>
    <label>
      Your first name:
      <input name='firstName' />
    </label>
  • label의 htmlFor와 input의 id같은 값으로 지정한다.
          <label htmlFor={ageInputId}>Your age:</label>
          <input id={ageInputId} name="age" type="number" />
          <label htmlFor={ageInputId}>Your age:</label>
          <input id={ageInputId} name="age" type="number" />

폼 제출시 입력값 읽기

  • state로 관리하지 않고 form 태그에서 제출한 데이터에 접근할 수 있다. 이때 반드시 input태그안에 name 속성을 지정해야한다.

    function handleSubmit(e) {
      // 브라우저가 페이지를 다시 로드하지 못하도록 방지합니다.
      e.preventDefault();
    
      // 폼 데이터를 읽습니다.
      const form = e.target;
      const formData = new FormData(form);
    
      // formData를 직접 fetch body로 전달할 수 있습니다.
      fetch('/some-api', { method: form.method, body: formData });
    }
    
    // <form method="post" onSubmit={handleSubmit}>
    //   <input name="myInput" defaultValue="Some initial value" />
    function handleSubmit(e) {
      // 브라우저가 페이지를 다시 로드하지 못하도록 방지합니다.
      e.preventDefault();
    
      // 폼 데이터를 읽습니다.
      const form = e.target;
      const formData = new FormData(form);
    
      // formData를 직접 fetch body로 전달할 수 있습니다.
      fetch('/some-api', { method: form.method, body: formData });
    }
    
    // <form method="post" onSubmit={handleSubmit}>
    //   <input name="myInput" defaultValue="Some initial value" />

폼 태그 내부의 버튼은 기본적으로 폼을 제출한다.

  • 폼 제출이 아니라 버튼으로 쓰고 싶다면 <button type="button"> 타입을 통해 버튼임을 명시한다.

내장 브라우저 <link> 컴포넌트는 스타일시트와 같은 외부 리소스를 사용하거나 링크 메타데이터로 문서를 주석 처리할 수 있게 해줍니다.

  • 어떤 컴포넌트에서든 <link>를 렌더링할 수 있으며, React는 대부분의 경우 해당 DOM 요소를 <head>에 배치합니다.
  • rel="stylesheet"인 경우
    • precedence: 문자열 타입. <link> DOM 노드를 문서의 <head> 내 다른 요소와 비교하여 순위를 지정하여 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지 결정한다. 동일 우선순위는 preload나 preinit 함수로 로드되었는지에 관계없이 함께 적용된다.
  • rel="preload"rel="modulepreload"인 경우
    • as: 문자열 타입. 리소스의 유형을 지정합니다. 가능한 값은 audiodocumentembedfetchfontimageobjectscriptstyletrackvideoworker

특별한 렌더링 동작

  • 특별한 렌더링 동작
    • React는 <link> 컴포넌트에 해당하는 DOM 요소를 React 트리의 어디에 렌더링하든 상관없이 항상 문서의 <head>에 배치합니다.
  • 특별한 렌더링 동작이 수행되지 않는 경우
    • <link>에 rel="stylesheet"  precedence속성이 없는경우 (스타일시트의 순서보장)
      • 이 특별한 동작을 위해 반드시 precedence 속성이 있어야 합니다. 이는 문서 내 스타일시트의 순서가 중요하기 때문입니다. React는 다른 스타일시트와의 순서를 결정하기 위해 precedence 속성을 사용합니다.
    • <link>에 itemProp 속성이 있는 경우 (메타데이터 분리)
      • 이 속성은 문서 전체가 아니라 페이지의 특정 부분에 대한 메타데이터를 나타냅니다.
    • <link>에 onLoad또는 onError 속성이 있는 경우 (로딩상태 제어)
      • 연결된 리소스의 로딩을 React 컴포넌트 내에서 수동으로 관리하기 때문입니다.

스타일시트에 대한 특별한 동작

  • 스타일시트가 로드되는 동안 <link>를 렌더링하는 컴포넌트는 일시 중단됩니다
    • 스타일이 적용되지 않은 깜빡임 방지.
  • 여러 href 속성이 동일한 스타일시트에 대한 링크를 렌더링하는 경우 중복을 제거한다.
  • 동작하지 않는 경우
    • precedence 속성이 없는 경우
    • onLoad, disabled 등 수동 제어 속성을 사용할 경우
  • 주의사항
    • 런타임 속성 변경이 불가능하다
      <link
        rel='stylesheet'
        href={dark ? 'dark.css' : 'light.css'} // 🔴 런타임 변경 불가
      />
      <link
        rel='stylesheet'
        href={dark ? 'dark.css' : 'light.css'} // 🔴 런타임 변경 불가
      />
    • DOM에 잔류한다.
      function ConditionalLoader({ show }) {
        return show && <link rel='stylesheet' href='conditional.css' />;
      }
      function ConditionalLoader({ show }) {
        return show && <link rel='stylesheet' href='conditional.css' />;
      }
      show의 값이 false가 되더라도 링크는 여전히 남아있는다. 전역으로 적용된 스타일이 바뀌면 레이아웃 재계산 등 성능 저하가 발생하므로 스타일을 바꾸고 싶다면 스타일이 다른 컴포넌트로 스위칭을 하거나 클래스 네임을 동적으로 설정하여 스타일을 바꾸기

meta

내장 브라우저 <meta> 컴포넌트를 사용하면 문서에 메타데이터를 추가할 수 있습니다.

  • 어느 컴포넌트에서나 <meta>를 렌더링할 수 있으며, React는 항상 해당 DOM 요소를 문서의 <head>에 배치합니다. (<meta>itemProp Props가 있는 경우에는 예외)
  • 다음 속성 중 하나만 가져야 합니다. name, httpEquiv, charset, itemProp.
  • itemProp 는 페이지 전체가 아니라 특정 항목에 대한 메타데이터이다.

script

내장 브라우저 <script> 컴포넌트를 사용하면 문서에 스크립트를 추가할 수 있습니다.

  • async: 불리언 값. 브라우저가 문서의 남은 부분을 처리할 때까지 스크립트 실행을 연기할 수 있도록 합니다.
  • fetchPriority: 문자열. 여러 스크립트를 동시에 가져올 때 브라우저가 스크립트를 우선순위로 순위 지정할 수 있도록 합니다. "high""low", 또는 "auto" (기본값)일 수 있습니다.
  • defer: 문자열. 문서가 로딩될 때까지 브라우저가 스크립트를 실행하지 못하도록 합니다.

특별한 렌더링 동작

  • React는 <script> 컴포넌트를 문서의 <head>로 이동시키고, 중복된 동일 스크립트를 제거합니다.
  • 조건
    • srcasync={true} 속성을 제공해야한다.
  • 동작하지 않는 경우
    • onError / onLoad 속성을 포함한 경우

style

내장 브라우저 <style> 컴포넌트를 사용하면 문서에 인라인 CSS 스타일시트를 추가할 수 있습니다.

  • precedence: 문자열 타입. 문서의 <head> 내 다른 요소들에 비해 <style> DOM 노드의 순위를 지정하여, 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지를 결정합니다. React는 먼저 발견한 우선순위를 “낮게”, 나중에 발견한 우선순위를 “높게” 추론합니다. 많은 스타일 시스템은 스타일 규칙이 원자적이기 때문에 단일 우선순위 값을 사용해도 잘 작동할 수 있습니다. 동일한 우선순위를 가지는 스타일시트는 <link> 태그인지 인라인 <style> 태그인지 preinit 함수로 로드된 것인지와 무관하게 함께 적용됩니다.

특별한 렌더링 동작

  • React는 <style> 컴포넌트를 문서의 <head>로 이동시키고, 동일한 스타일시트의 중복을 제거하며, 스타일시트가 로딩되는 동안 서스펜스할 수 있습니다.
  • 조건
    • hrefprecedence 속성을 제공

title

내장 브라우저 <title> 컴포넌트를 사용하면 문서의 제목을 지정할 수 있습니다.

특별한 렌더링 동작

  • React는 <title> 컴포넌트에 해당하는 DOM 요소를 React 트리 내 어디에서 렌더링하든 항상 문서의 <head> 내에 배치합니다.
  • 하나의 <title> 만 사용할것. 여러개를 렌더링하면 모든 제목을 head안에 배치하고 검색엔진의 동작이 제대로 동작하지 않을 수 있다.
  • 예외
    • <title>이 <svg> 컴포넌트 내에 있는 경우, 특별한 동작이 없습니다. 이 맥락에서는 문서의 제목을 나타내는 것이 아니라 해당 SVG 그래픽에 대한 접근성 주석이기 때문입니다.
    • <title>에 itemProp 속성이 있는 경우, 특별한 동작이 없습니다. 이 경우 문서의 제목이 아니라 페이지의 특정 부분에 대한 메타데이터를 나타내기 때문입니다.
읽어주셔서 감사합니다