[React 19] 공식문서 톺아보기 - React DOM 컴포넌트 (2)
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
자바스크립트 없이 에러처리하기<form>
이 서버 컴포넌트에서 렌더링. (use server)<form>
의action
프로퍼티로 전달된 함수가 서버 함수.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
내장 이벤트와 달리 ReactonInvalid
이벤트는 버블링됩니다.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>
컴포넌트는 스타일시트와 같은 외부 리소스를 사용하거나 링크 메타데이터로 문서를 주석 처리할 수 있게 해줍니다.
- 어떤 컴포넌트에서든
<link>
를 렌더링할 수 있으며, React는 대부분의 경우 해당 DOM 요소를<head>
에 배치합니다. rel="stylesheet"
인 경우precedence
: 문자열 타입.<link>
DOM 노드를 문서의<head>
내 다른 요소와 비교하여 순위를 지정하여 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지 결정한다. 동일 우선순위는preload
나preinit
함수로 로드되었는지에 관계없이 함께 적용된다.
rel="preload"
나rel="modulepreload"
인 경우as
: 문자열 타입. 리소스의 유형을 지정합니다. 가능한 값은audio
,document
,embed
,fetch
,font
,image
,object
,script
,style
,track
,video
,worker
특별한 렌더링 동작
- 특별한 렌더링 동작
- React는
<link>
컴포넌트에 해당하는 DOM 요소를 React 트리의 어디에 렌더링하든 상관없이 항상 문서의<head>
에 배치합니다.
- React는
- 특별한 렌더링 동작이 수행되지 않는 경우
<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에 잔류한다.
show의 값이 false가 되더라도 링크는 여전히 남아있는다. 전역으로 적용된 스타일이 바뀌면 레이아웃 재계산 등 성능 저하가 발생하므로 스타일을 바꾸고 싶다면 스타일이 다른 컴포넌트로 스위칭을 하거나 클래스 네임을 동적으로 설정하여 스타일을 바꾸기
function ConditionalLoader({ show }) { return show && <link rel='stylesheet' href='conditional.css' />; }
function ConditionalLoader({ show }) { return show && <link rel='stylesheet' href='conditional.css' />; }
- 런타임 속성 변경이 불가능하다
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>
로 이동시키고, 중복된 동일 스크립트를 제거합니다. - 조건
src
와async={true}
속성을 제공해야한다.
- 동작하지 않는 경우
- onError / onLoad 속성을 포함한 경우
style
내장 브라우저 <style>
컴포넌트를 사용하면 문서에 인라인 CSS 스타일시트를 추가할 수 있습니다.
precedence
: 문자열 타입. 문서의<head>
내 다른 요소들에 비해<style>
DOM 노드의 순위를 지정하여, 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지를 결정합니다. React는 먼저 발견한 우선순위를 “낮게”, 나중에 발견한 우선순위를 “높게” 추론합니다. 많은 스타일 시스템은 스타일 규칙이 원자적이기 때문에 단일 우선순위 값을 사용해도 잘 작동할 수 있습니다. 동일한 우선순위를 가지는 스타일시트는<link>
태그인지 인라인<style>
태그인지preinit
함수로 로드된 것인지와 무관하게 함께 적용됩니다.
특별한 렌더링 동작
- React는
<style>
컴포넌트를 문서의<head>
로 이동시키고, 동일한 스타일시트의 중복을 제거하며, 스타일시트가 로딩되는 동안 서스펜스할 수 있습니다. - 조건
href
와precedence
속성을 제공
title
내장 브라우저 <title>
컴포넌트를 사용하면 문서의 제목을 지정할 수 있습니다.
특별한 렌더링 동작
- React는
<title>
컴포넌트에 해당하는 DOM 요소를 React 트리 내 어디에서 렌더링하든 항상 문서의<head>
내에 배치합니다. - 하나의
<title>
만 사용할것. 여러개를 렌더링하면 모든 제목을 head안에 배치하고 검색엔진의 동작이 제대로 동작하지 않을 수 있다. - 예외
<title>
이<svg>
컴포넌트 내에 있는 경우, 특별한 동작이 없습니다. 이 맥락에서는 문서의 제목을 나타내는 것이 아니라 해당 SVG 그래픽에 대한 접근성 주석이기 때문입니다.<title>
에itemProp
속성이 있는 경우, 특별한 동작이 없습니다. 이 경우 문서의 제목이 아니라 페이지의 특정 부분에 대한 메타데이터를 나타내기 때문입니다.