무엇
<textarea>는 여러 줄을 입력하는 텍스트 필드다. 브라우저 기본 요소를 그대로 렌더한다.
HTML과 달리 React에서는 내용을 자식으로 넣지 않는다. <textarea>내용</textarea>이 아니라 defaultValue(또는 value)로 준다.
<textarea defaultValue="초기 내용" rows={4} cols={40} />
무슨 일이 일어나나
<select>, <input>과 같은 제어/비제어 규칙을 따른다. defaultValue만 주면 비제어로, 초기값만 정하고 이후 입력은 브라우저가 관리한다. value를 주면 제어가 되어 React가 값을 강제한다. 이때 onChange로 상태를 갱신하지 않으면 입력이 화면에 반영되지 않는다.
제어로 쓸 때 value는 컴포넌트가 사는 동안 늘 문자열이어야 한다. null이나 undefined가 섞이면 제어에서 비제어로 넘어가 버려 경고가 뜬다. 초기값이 없을 수 있으면 value={text ?? ''}처럼 빈 문자열로 맞춘다.
HTML의 <textarea>본문</textarea> 형태를 지원하지 않는 이유도 이 모델 때문이다. 초기 내용은 자식이 아니라 값 prop으로 다뤄야 제어/비제어 판단이 일관된다.
사용법
비제어는 defaultValue로 초기 내용만 채운다. rows, cols로 크기를 정한다.
export function PostEditor() {
return (
<textarea
name="postContent"
defaultValue="어제 자전거를 탔다."
rows={4}
cols={40}
/>
)
}
제어는 value와 onChange를 짝짓는다. 입력할 때마다 다른 UI를 다시 그려야 할 때(예: 미리보기) 쓴다.
import { useState } from 'react'
export function MarkdownEditor() {
const [content, setContent] = useState('_안녕_ **마크다운**!')
return (
<>
<textarea value={content} onChange={(e) => setContent(e.target.value)} />
<MarkdownPreview markdown={content} />
</>
)
}
실무 예시
라벨과 묶어 폼에 넣고, 제출 때 FormData로 값을 읽는 흔한 형태다. name이 폼 데이터의 키가 된다.
export function EditPost() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
console.log(Object.fromEntries(formData)) // { postContent: "..." }
}
return (
<form onSubmit={handleSubmit}>
<label>
내용
<textarea name="postContent" defaultValue="" rows={4} cols={40} />
</label>
<button type="submit">저장</button>
</form>
)
}
왜 중요한가
댓글, 글 본문, 메모처럼 긴 텍스트를 받는 곳마다 쓴다. 규칙 자체는 <input>, <select>와 같아서 따로 외울 게 많지 않다. 다만 "초기 내용을 children이 아니라 값 prop으로 준다"는 한 가지가 HTML 습관과 어긋나 처음에 자주 걸린다. 그 지점만 넘기면 나머지는 폼 요소 공통 규칙 그대로다.
Reference