가변 상태를 캐시하는 useState 훅
useMemo 훅은 불변 상태를 캐시하지만, useState 훅은 가변 상태를 캐시한다. 리액트 프레임워크는 세터 함수가 호출되면 컴포넌트의 상태 변화가 있는 것으로 판단하고 즉시 해당 컴포넌트를 다시 렌더링한다.
다만 상태에는 타입이 존재한다. number, boolean, string 같은 원시 타입(primitive), 객체, 배열, 튜플 타입일 수도 있다.
useState의 선언문을 보면 '<S>'는 상태의 타입을 나타낸다. S = string, S = number, S= { key : value} 등으로 타입이 결정된다. 이 제네릭 타입 덕분에 useState는 다양한 타입의 상태 관리를 할 수 있다.
매개변수의 initialState는 상태의 초기값이다. 초기값의 타입은 S 또는 () => S이다.
'S'는 값 자체를 타나낸다. 예를들어 useState(0)에서 0을 넣으면 초기값이 0으로 설정된다.
() => S는 초기값을 반환하는 함수이다.
반환값 [S, Dispatch<SetStateAction<S>>] 은 두요소로 이루어진 배열 형태이다. 'S'는 현재 상태 값을 의미한다.
여기서 Dispatch<SetStateActipon<S>>는 상태를 업데이트 하는 함수이다. 예를들어 setCount(5); 상태를 5로 변경도 가능하고 setCount(prev => prev+1) 이전 상태를 기반으로 계산도 가능하다.
// useState 훅 선언문
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
// useState 훅 사용법
const [값, 값을_변경하는_세터_함수] = useState(초깃값)
// Dispatch와 SetStateAction 타입
// setter(newValue) 또는 setter(previousValue => newValue) 둘중 하나의 함수 타입을 의미
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
Number 타입일 때 useState 훅 사용하기
number타입으로 훅을 사용하기 위해 훅의 초기값 count를 0으로 설정한 후 count 값을 1만큼씩 증가시켜 보자. 아래와 같이 값을 설정하는 방법은 SetStateAction<S> = S, (preState:S) => S 두가지 방식을 지원하다.
// number 타입의 useState 훅 사용
const [count, setCount] = useState<number>(0)
//
const increment = () => setCount(count + 1) // count를 1씩 증가시키기
count increment = () => setCount(count => count + 1) // count를 1씩 증가시키기
<button onClick={increment}>+</button>
위에서 본 increment 콜백 함수를 useCallback 훅으로 캐시하고 setCount(count+1)을 사용 시 useState의 count를 의존성 목록에 추가해야 한다. 하지만 세터 함수의 입력 변수가 함수일 경우 현재 유지되고 있는 값을 매개변수로 해 세터 함수를 호출한다. 그리고 세터함수가 반환한 값을 새로운 count값으로 설정하므로 count의 의존성 문제가 발생하지 않는다.
// count에 의존성 문제 발생
cont increment = useCallback(() => {
setCount(count + 1) // 의존성 목록에 count를 넣지 않으면 count는 항상 0이다.
}, [count]) // 의존성 목록에 count 추가
// count에 의존성 문제 해결
count increment = useCallback(() => {
setCount(count => count + 1) // 함수를 입력 변수로 세터를 호출한다.
}, []) // 세터가 호출되므로 의존성 목록에 count를 추가하지 않아도 된다.
number 타입의 useState훅을 사용하기 위해 코드를 추가한다.
import {useState, useCallback} from 'react'
import {Title} from '../components'
import {Icon} from '../theme/daisyui'
export default function NumberState() {
const [count, setCount] = useState<number>(0)
const increment = useCallback(() => {
setCount(count => count + 1)
}, [])
const decrement = useCallback(() => {
setCount(count => count - 1)
}, [])
return (
<section className="mt-4 mb-8">
<Title>NumberState</Title>
<div className="flex justify-center">
<div className="flex items-center justify-between w-1/4 mt-4">
<Icon name="add" className="btn-primary btn-lg" onClick={increment} />
<p className="text-3xl text-bold text-primary">{count}</p>
<Icon name="remove" className="btn-accent btn-lg" onClick={decrement} />
</div>
</div>
</section>
)
}
리액트 <input> 컴포넌트에 훅 사용하기
리액트의 <input> 요소와 HTML의 <input> 요소는 사용법에 차이가 있다. 또한 리액트는 기본값을 설정할 때 defaultValue와 defaultChecked속성을 사용해야 한다.
// HTML 방식 기본 값 설정 방법
<input type="text" value="default value" />
<input type="checkbox" checked="checked" />
// 리액트 <input>의 checked 속성 사용 예
// 리액트 <input>요소의 checked 속성의 타입이 boolean이기 때문에 ture, false를 전달해야 한다.
<input type="text" defaultValue="default Value" />
<input type="checkbox" defaultChecked={true} />
이제 <input> 태그를 통해 onChange 이벤트 설정을 하기 위해 코드를 추가한다.
import {useState, useCallback, ChangeEvent} from 'react'
import {Title} from '../components'
import {Input} from '../theme/daisyui'
export default function InputTest() {
const [value, setValue] = useState<string>('')
const [checked, setChecked] = useState<boolean>(false)
const onChangeValue = useCallback((e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
setValue(notUsed => e.target.value)
}, [])
const onChangeChecked = useCallback((e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.checked)
setChecked(notUsed => e.target.checked)
}, [])
return (
<section className="mt-4">
<Title>InputTest</Title>
<div className="mt-4"></div>
<div className="flex item-center justify-center p-4 mt-4">
<Input
type="text"
className="input-primary input-sm"
value={value}
onChange={onChangeValue}
/>
<Input
type="checkbox"
className="ml-4 checkbox checkbox-primary input-sm"
checked={checked}
onChange={onChangeChecked}
/>
</div>
</section>
)
}
useToggle 커스텀 훅 만들기
checked와 관련된 부분의 패턴으로 코드를 작성하는건 번거로운 일이다. 해당 번거로움을 줄이기 위해 useToggle 커스텀 훅을 구현한다. 여기서 새로운 부분은 boolean값은 언제나 ture, false이므로 ChangeEvent의 target.checked 속성을 사용하지 않고, ! 연산자로 value값을 역으로 바꾸는 방식을 사용한다.
// checked 속성값 사용 패턴
const [checked, setChecked] = useState<boolean>(false)
const onChangeChecked = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setChecked(notUsed => e.target.checked)
}, [])
// useToggle 커스텀 훅 구현
import {useState, useCallback} from 'react'
export const useToggle = (initialChecked: boolean = false): [boolean, () => void] => {
const [checked, setChecked] = useState<boolean>(initialChecked)
const toggleChecked = useCallback(() => setChecked(checked => !checked), [])
return [checked, toggleChecked]
}
useToggle 훅을 daisyui의 Modal 컴포넌트에 적용하기 위해 코드를 추가한다.
import type {FC} from 'react'
import type {ReactDivProps} from '../../components'
import {Div} from '../../components'
import {Icon} from './Icon'
export type ModalProps = ReactDivProps & {
open?: Boolean
}
export const Modal: FC<ModalProps> = ({open, className: _className, ...props}) => {
const className = ['modal', open ? 'modal-open' : '', _className].join(' ')
return <div {...props} className={className} />
}
export type ModalcontentProps = ReactDivProps & {
onCloseIconClicked?: () => void
closeIconClassName?: string
}
export const ModalContent: FC<ModalcontentProps> = ({
onCloseIconClicked,
closeIconClassName: _closeIconClassName,
className: _className,
children,
...props
}) => {
const showCloseIcon = onCloseIconClicked ? true : false
const className = ['modal-box', showCloseIcon && 'relative', _className].join(' ')
if (!showCloseIcon) {
return <div {...props} className={className} children={children} />
}
const closeIconClassName = _closeIconClassName ?? 'btn-primary btn-outline btn-sm'
return (
<div {...props} className={className}>
<Div className="absolute" right="0.5rem" top="0.5rem">
<Icon name="close" className={closeIconClassName} onClick={onCloseIconClicked} />
</Div>
{children}
</div>
)
}
export type ModalActionProps = ReactDivProps & {}
export const ModalAction: FC<ModalActionProps> = ({className: _className, ...props}) => {
const className = ['modal_action', _className].join(' ')
return <div {...props} className={className} />
}
라디오 버튼 구현 방법
daisyui의 라디오 버튼 CSS 컴포넌트를 구현하기 위해 코드를 추가한다.
import {useMemo} from 'react'
import {Title, Subtitle} from '../components'
import * as D from '../data'
export default function RadioInputTest() {
const jobTitles = useMemo(() => D.makeArray(4).map(D.randomJobTitle), [])
const radioInputs = useMemo(
() =>
jobTitles.map((value, index) => (
<label key={index} className="flex justify-start cursor-pointer label">
<input
type="radio"
name="jobs"
className="mr-4 radio radio-primary"
defaultValue={value}
/>
<span>{value}</span>
</label>
)),
[jobTitles]
)
return (
<section className="mt-4">
<Title>RadioInputTest</Title>
<div className="flex flex-col jstify-center mt-4">
<Subtitle>What is your job?</Subtitle>
<Subtitle className="mt-4">Selected Job : </Subtitle>
<div className="flex justify-center p-4 mt-4">
<div className="flex flex-col mt-4">{radioInputs}</div>
</div>
</div>
</section>
)
}
value 속성으로 라디오 버튼 선택 로직 구현하기
사용자가 선택된 라디오 버튼을 코드에서 알기 위해선 checked 속성값에 '값 === selectedValue'형태의 코드가 필요하다.
고차함수를 사용하지 않고 radio를 구현하기 위해서는 defaultValue속성을 사용해서 설정하면 된다.
import {useMemo, ChangeEvent, useCallback, useState} from 'react'
import {Title, Subtitle} from '../components'
import * as D from '../data'
export default function RadioInputTest() {
const jobTitles = useMemo(() => D.makeArray(4).map(D.randomJobTitle), [])
const [selectedJobTitle, setSelectedJobTitle] = useState<string>(jobTitles[0])
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSelectedJobTitle(notUsed => e.target.value)
}, [])
const radioInputs = useMemo(
() =>
jobTitles.map((value, index) => (
<label key={index} className="flex justify-start cursor-pointer label">
<input
type="radio"
name="jobs"
className="mr-4 radio radio-primary"
checked={value === selectedJobTitle}
defaultValue={value}
onChange={onChange}
/>
<span>{value}</span>
</label>
)),
[jobTitles, selectedJobTitle, onChange]
)
return (
<section className="mt-4">
<Title>RadioInputTest</Title>
<div className="flex flex-col jstify-center mt-4">
<Subtitle>What is your job?</Subtitle>
<Subtitle className="mt-4">Selected Job : {selectedJobTitle}</Subtitle>
<div className="flex justify-center p-4 mt-4">
<div className="flex flex-col mt-4">{radioInputs}</div>
</div>
</div>
</section>
)
}
고차 함수로 라디오 버튼 선택 로직 구현하기
고차함수를 사용할 때는 다음처럼 라디오 버튼의 인덱스로 어떤 버튼을 선택했는지 알 수 있다. 그리고 onchange 이벤트 처리 콜백 함수를 고차함수 형태로 구현하면 된다. 마지막으로 <input>의 onChange 이벤트 속성에 onChange의 index부분만 해소한 부분함수를 설정한다.
import {useMemo, ChangeEvent, useCallback, useState} from 'react'
import {Title, Subtitle} from '../components'
import * as D from '../data'
export default function HigherOrderRadioInputTest() {
const jobTitles = useMemo(() => D.makeArray(4).map(D.randomJobTitle), [])
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const onChange = useCallback(
(index: number) => () => setSelectedIndex(notUsed => index),
[]
)
const radioInputs = useMemo(
() =>
jobTitles.map((value, index) => (
<label key={index} className="flex justify-start cursor-pointer label">
<input
type="radio"
name="higher jobs"
className="mr-4 radio radio-primary"
checked={index === selectedIndex}
defaultValue={value}
onChange={onChange(index)}
/>
<span>{value}</span>
</label>
)),
[jobTitles, selectedIndex, onChange]
)
return (
<section className="mt-4">
<Title>HigherOrderRadioInputTest</Title>
<div className="flex flex-col jstify-center mt-4">
<Subtitle>What is your job?</Subtitle>
<Subtitle className="mt-4">Selected Job : {jobTitles[selectedIndex]}</Subtitle>
<div className="flex justify-center p-4 mt-4">
<div className="flex flex-col mt-4">{radioInputs}</div>
</div>
</div>
</section>
)
}
HTML <form> 요소
HTML의 form요소는 method속성에 데이터를 전송할 GET/POST과 같은 HTTP 메서드를 설정하고, action 속성에 폼 데이터를 전송한 뒤 전환할 화면의 URL을 설정하는 방식으로 사용한다.
method가 POST이면 폼 데이터를 암호화(encryption)하는 encType 속성에 설정한다. 속성값은 application/x-www-form-urlencoded(기본값), multipart/form-data, text/plain이 있다.
하지만 리액트는 SPA 방식 프레임워크라 백엔드 웹 서버가 API방식으로 동작하므로 <from> 요소와 action, method, encType 등의 속성을 설정할 필요가 없다.
다만 관습적으로 사용자 입력을 받는 부분을 <form>요소로 구현한다.
<form>
<input type="submit" value="버튼_텍스트" />
</form>
사용자가 <input type="submit"> 버튼을 누르는 이벤트 처리는 <form> 요소의 onSubmit 이벤트 속성을 다음과 같이 사용한다. 참고로 FormEvent 타입 대신 ChangeEvent나 SyntacticEvent 타입을 사용해도 된다. 주의할 점은 onSubmit은 웹 브라우저가 자체적으로 지니고 있는 이벤트가 있기 때문에 해당 이벤트를 중지시킬 e.preventDefault()를 호출해야 한다.
import type {Formevent} from 'react'
... (생략) ...
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 웹 브라우저가 지니고있는 이벤트를 발생하지 않게 정지시키는 메서드
}
<form onSubmit={onSubmit}>
<input type="submit" value="버튼_텍스트" />
</form>
FormData 클래스
FormData는 자바스크립트 엔진이 기본으로 제공하는 클래스로, 사용자가 입력한 데이터를 웹 서버에 전송할 목적으료 사용한다. Formdata 클래스는 여러가지 메서드를 제공하지만 보통 append() 메서드가 자주 사용된다.
formData를 통해 append() 메서드를 호출해 (키,값) 형태의 데이터를 추가할 수 있다. 또한 FormData의 내용을 JSON 포맷으로 바꾸는 것도 자바스크립트 엔진이 기본으로 제공하는 Object.fromEntries() 함수를 호출하면 된다.
const formData = new FormData()
formData.append('name', 'Jack')
formData.append('email', 'jack@email.com')
// formData를 JSON포맷으로 변환처리
const json = Object.fromEntries(formData)
위 내용을 토대로 daisyui의 폼 관련 CSS 컴포넌트들을 적용할 코드를 추가하자.
import type {FormEvent, ChangeEvent} from 'react'
import {useState, useCallback} from 'react'
import {Title} from '../components'
export default function BasicForm() {
const [name, setName] = useState<string>('')
const [email, setEmail] = useState<string>('')
const onSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData()
formData.append('name', name)
formData.append('email', email)
alert(JSON.stringify(Object.fromEntries(formData), null, 2))
},
[name, email]
)
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setName(notUsed => e.target.value)
}, [])
const onChangeEmail = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setEmail(notUsed => e.target.value)
}, [])
return (
<section className="mt-4">
<Title>BasicForm</Title>
<div className="flex justify-center mt-4">
<form onSubmit={onSubmit}>
<label className="label" htmlFor="name">
<span className="label-text">Username</span>
</label>
<input
value={name}
onChange={onChangeName}
id="name"
type="text"
placeholder="enter your name"
className="input input-primary"
/>
<div className="form-control">
<label className="label" htmlFor="email">
<span className="label-text">Email</span>
</label>
<input
value={email}
onChange={onChangeEmail}
id="email"
type="text"
placeholder="enter your email"
className="input input-primary"
/>
</div>
<div className="flex justify-center mt-4">
<input
type="submit"
value="Submit"
className="w-1/2 btn btn-sm btn-primary"
/>
<input defaultValue="Cancel" className="w-1/2 ml-4 btn btn-sm" />
</div>
</form>
</div>
</section>
)
}
객체 타입 값일 때 useState 훅 사용하기
useState는 객체의 속성 형태(type formType = { name:string, email:string})로 구현할 수 있다. 그리고 FormType객체를 상태로 만들 수 있다. 이럴 경우 onChangeName, onChangeEmail과 같은 콜백 함수를 구현해야 한다.
그러기 위해서는 깊은 복사, 얕은 복사, 타입스크립트의 전개 연산자 구문을 알아야 한다.
// 객체를 상태로 만들기
const [form, setForm] = useState<FormType>({name:'', email:''})
깊은 복사와 얕은 복사, 그리고 의존성 목록
프로그래밍 언어에서는 변수에 담긴 값을 다른 변수에 복사할 때 깊은 복사(deep copy)와 얕은 복사(shallow)라는 2가지 방식을 지원한다.
number, boolean 등 갑싱 메모리 크기를 컴파일 타임 때 알 수 있는 타입은 항상 깊은 복사가 일어난다. 반면 객체, 배열 등 값의 메모리 크기를 런타임 때 알 수 있는 타입(주소값을 갖고 있는 타입)은 얕은 복사가 일어난다.
예외적으로 string타입 문자열은 타입스크립트에서 문자열은 항상 읽기 전용이므로 메모리 크기를 컴파일 때 알 수 있다.
따라서 문자열은 깊은 복사가 일어난다.
아래와 같이 얕은 복사를 하게 되면 form === newForm 은 true이기 때문에 리액트 프레임워크는 컴포넌트를 다시 랜더링하지 않는다. Object.assign({}, form)인 깊은 복사를 해야 form === newForm 결과가 false가 되기 때문에 컴포넌트가 다시 렌더링 되는 결과를 확인 할 수 있다.
const onChangeName = usecallback((e: ChangeEvent<HTMLInputelement>) => {
const newForm = form // 얕은 복사
// const newForm = Object.assign({}, from) // 깊은복사
newForm.name = e.target.value
setForm(newForm)
}, [form])
객체에 적용하는 타입스크립트 전개 연산자 구문
방금 전 깊은 복사를 좀더 단순하게 코딩하기 위해 전개 연산자 구문을 사용할 수 있다. 다음 코드를 보면 두 객체 앞에 점3개(...)를 붙였고 이 연산자를 사용하는 코드의 위치에 따라 잔여 연산자(rest operator) 또는 전개 연산자(spread operator)라고 한다.
// 전개 연산자
let coord = {...{x: 0}, ...{y: 0}};
console.log(coord); // {x: 0, y: 0}
// 전개 연산자를 통해 newForm === form의 결과가 false이므로 컴포넌트는 다시 렌더링된다.
const onChangeANme = useCallback((e. ChangeEvent<HTMLInputelement>) => {
const newForm = {...form} // 전개 연산자를 통한 깊은 복사
newForm.name = e.target.value
// 위의 두행을 하나의 행으로 합쳐준 전개연산자 (깊은복사 + 일부값변경)
// const newForm = {...form, e.target.value}
setFrom(newForm)
},[form])
타입스크립트 객체 반환 구문
setForm함수를 아래와 같이 콜백 함수로 구현해 의존성 목록에 추가하지 않아도 되기 때문에 useCallback 호출 시 선호하는 방식이다. 다만 타입스크립트는 { } 중괄호를 복합 실행문으로 인식을 해 오류가 발생한다.
타입스크립트에서 객체를 반환하는 구문은 객체를 의미하는 { }중괄호를 다시 소괄호로 감싼 ({}) 형태로 사용해야 한다.
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setForm(form => ({...form, name: e.target.value}))
}, [])
ObjectState.tsx 파일 구현하기
객체타입을 통해 파라미터를 관리하는 부분을 코드로 추가한다.
import type {FormEvent, ChangeEvent} from 'react'
import {useState, useCallback} from 'react'
import {Title} from '../components'
type FormType = {
name: string
email: string
}
export default function ObjectState() {
const [form, setForm] = useState<FormType>({name: '', email: ''})
const onSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
alert(JSON.stringify(form, null, 2))
},
[form]
)
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setForm(form => ({...form, name: e.target.value}))
}, [])
const onChangeEmail = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setForm(form => ({...form, email: e.target.value}))
}, [])
return (
<section className="mt-4">
<Title>ObjectState</Title>
<div className="flex justify-center mt-4">
<form onSubmit={onSubmit}>
<label className="label" htmlFor="name">
<span className="label-text">Username</span>
</label>
<input
value={form.name}
onChange={onChangeName}
id="name"
type="text"
placeholder="enter your name"
className="input input-primary"
/>
<div className="form-control">
<label className="label" htmlFor="email">
<span className="label-text">Email</span>
</label>
<input
value={form.email}
onChange={onChangeEmail}
id="email"
type="text"
placeholder="enter your email"
className="input input-primary"
/>
</div>
<div className="flex justify-center mt-4">
<input
type="submit"
value="Submit"
className="w-1/2 btn btn-sm btn-primary"
/>
<input defaultValue="Cancel" className="w-1/2 ml-4 btn btn-sm" />
</div>
</form>
</div>
</section>
)
}
배열 타입 값일 때 useState 훅 사용하기
useSate 훅을 배열타입으로 선언한 경우 콜백함수에서 값의 변경이 발생할 때는 전개연산자를 통한 깊은복사로 의존성목록에 추가하지 않고도 컴포넌트를 다시 렌더링할 수 있다.
배열의 값을 변경하는 방법은 [...number, 4] 이런형태로 전개연산자를 사용하면 된다. useState 배열타입을 통한 코드를 추가하자.
import {useCallback, useState, useMemo} from 'react'
import {Title, Div} from '../components'
import {Icon} from '../theme/daisyui'
import * as D from '../data'
export default function ArrayState() {
const [images, setImages] = useState<string[]>([])
const addImage = useCallback(
// 전개 연산자를 통한 깊은복사 (깊은복사는 다시 랜더링된다)
() => setImages(images => [D.randomImage(200, 100, 50), ...images]),
[]
)
const clearImages = useCallback(() => setImages(notUsed => []), [])
const children = useMemo(
() =>
images.map((image, index) => (
<Div
key={index}
src={image}
className="w-1/5 m-2"
height="5rem"
minHeight="5rem"
/>
)),
[images]
)
return (
<section className="mt-4">
<Title>ArrayState</Title>
<div className="flex justify-center mt-4">
<div data-tip="add image" className="tooltip">
<Icon name="add" onClick={addImage} className="mr-12 btn-primary" />
</div>
<div data-tip="clear all" className="tooltip">
<Icon name="clear_all" onClick={clearImages} />
</div>
</div>
<div className="flex flex-wrap mt-4">{children}</div>
</section>
)
}
'FrontEnd > React' 카테고리의 다른 글
useEffect와 useLayoutEffect훅 이해하기 (0) | 2024.12.01 |
---|---|
[React] useMemo와 useCallback 훅 이해하기 (0) | 2024.11.27 |
[React] 처음 만나는 리액트 훅 (0) | 2024.11.26 |
[React] daisyui CSS 컴포넌트 이해하기 (0) | 2024.11.25 |
[React] 플렉스 레이아웃 이해하기 (0) | 2024.11.24 |