useState 훅 이해하기
[ 1 ] 불변 상태를 캐시하는 useState 훅
앞에 알아본 useMemo 훅은 불변 상태를 캐시하지만, useState 훅은 가변 상태를 캐시합니다.
useState 훅 임포트
import { useState } from 'react'
useState 훅 사용법은 다음과 같습니다. 리액트 프레임워크는 세터 함수가 호출되면 컴포넌트의 상태에 변화가 있는 것으로 판단하고 즉시 해당 컴포넌트를 다시 렌더링합니다.
useState 훅 사용법
const [값, 값을_변경하는_세터_함수] = useState(초깃값)
그런데 상태는 타입이 있습니다. 즉 상태는 number, boolean, string같은 원시 타입 일 수도 있고, 객체나 배열, 튜플 타입일 수도 있습니다. useState 훅의 선언문을 보면 상태의 타입이 S일 때 초깃값을 매개변수로 받는 제네릭 함수임을 알 수 있습니다.
useState 훅 선언문
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
여기서 Dispatch와 SetStateAction 타입은 다음과 같습니다. 이 두타입이 결합된 Dispatch<SetStateAction<S> setter(newValue) 또는 setter(previousValue => newValue) 둘 중 하나의 함수 타입을 의미합니다.
Dispatch와 SetStateAction 타입
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState:S) => S);
[ 2 ] number 타입일 때 useState훅 사용하기
먼저 상태가 number 타입일 때 useState 훅을 어떻게 사용하는지 알아보겠습니다. 다음 코드는 초깃값을 0으로 설정하므로 이 코드가 실행된 시점에 count는 0으로 설정됩니다.
number 타입일 때 useState 훅 사용법
const [count, setCount) = useState<number>(0)
만일 count값을 1만큼 증가시키고 싶다면 다음처럼 구현할 수 있습니다.
const increment = () => setCount(count => count + 1)
<button onClick={increment}>+</button>
이코드는 현재 count값에 1만큼 더한 새로운 count값을 반환합니다.
count값 1만큼 증가하는 또 다른 방법
const increment = () => setCount(count => count + 1)
그런데 increment 콜백 함수를 다음 코드처럼 useCallback 훅으로 캐시하면 count에 의존성 문제가 발생합니다. count가 초깃값 0일 때 increment가 호출되면 count는 1이 되지만, useCallback 함수 내부에서 count는 여전히 0입니다. 즉 count를 의존성 목록에 등록하지 않으면 리액트는 useCallback 훅의 콜백 함수 내부에서 count값을 갱신하지 않으므로 항상 0이 됩니다. 하지만 count를 의존성 목록에 넣으면 리액트는 변경된 count값 1이 반영된 새로운 콜백 함수를 캐시합니다.
count에 의존성 문제 발생
count increment = useCallback(() => {
setCount(count + 1) // 의존성 목록에 count 넣지 않으면 count는 항상 0이므로
}, [count]) // 의존성 목록에 count를 넣어야함
그런데 흥미로운 것은 increment를 다음처럼 구현하면 count에 의존성 문제가 발생하지 않는다는 점입니다. 코드에서 setCount의 입력 변수 'count => count + 1'은 함수입니다. 리액트는 세터 함수의 입력 변수가 함수일 때는 현재 유지되고 있는 값을 매개변수로 하여 세터 함수를 호출합니다. 그리고 세터 함수가 반환한 값을 새로운 count값으로 설정하므로 count에 의존성 문제가 발생하지 않습니다.
count에 의존성 문제 해결
const increment = useCallback(() => {
setCount(count => count + 1) // 함수를 입력 변수로 세터 호출
}, [ ]) // 의존성 목록에 count를 넣지 않아도 됨


[ 3 ] 리액트 <input> 컴포넌트에 훅 사용하기
리액트이 <input> 컴포넌트는 type속성을 제공하며 이속성에는 다양한(text, checkbox, radio등) 값을 가질 수 있습니다. 이속성은 input이 모양이 화면에 다양하게 보여집니다.
input은 boolean 타입의 checked 속성과 string이나 number 타입의 value 속성 2가지를 제공합니다. 또한 속성값이 바뀌면 onChange 이벤트 속성에 설정한 콜백 함수를 호출합니다.
<input> 컴포넌트 선언문
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
checked?: boolean | undefined;
value?: string | ReadonlyArray<string> | number | undefined';
onChange?: ChangeEventHandler<T> | undefined
}
그런데 리액트의 <input> 요소와 HTML의 <input> 요소는 사용법에 차이가 있습니다.
HTML 방식 기본값 설정 방법
<input type="text" value="default value" />
<input type="checkbox" checked="checked" />
하지만 리액트의<input> 요소의 checked 속성은 타입이 boolean이므로 위 코드의 두 번째 줄은 타입 오류가 발생합니다.
이오류를 해결하려면 아래코드처럼 작성해야합니다.
리액트 <input>의 checked 속성 사용 예
<input type="checkbox" checked={trun} />
그런데 위 처럼 코드를 작성하면 웹브라우저의 콘솔창에 다음과 같은 경고가 뜹니다.

에러 해결 14 - You provided a `value` prop to a form field without an `onChange` handler.
요 녀석은 에러는 아니고 경고임. 다만 리액트 기본 규칙에 어긋나서 발생한 경고이니 만큼 에러 해결 시리즈에 포함함.제품 등록 기능을 구현하면서 내가 입력한 정보를 동일하게 화면에 출력
velog.io
위에같은 경고를 없앨려면 아래코드처럼 수정해야합니다.
리액트 방식 기본값 설정 방법
<input type="text" defaultValue="default value" />
<input type="checkbox" defaultChecked={true} />
이제 onChange이벤트를 어떻게 구현하는지 알아보겠습니다.

이제 텍스트 박스, 체크박스를 onChange이벤트 처리 콜백 함수를 설정하겠습니다. text는 e.target.value ㅡ checkbox는 e.target.checked 속성을 사용해야 합니다.
텍스트 타입일 때 이벤트 설정 방법
import type { ChangeEvent } from 'react'
export default function OnChange() {
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
// console.log('onChange', e.target.value)
}
return <input type="text" onChange={onChange} />
}
이제 위에코드와 합쳐보겠습니다.


[ 4 ] useToggle 커스텀 훅 만들기
checked의관련된 부분입니다.
checked 속성값 사용 코드 패턴
const [checked, setChecked] = useState<boolean>(false)
const onChangeChecked = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setChecked(notUsed => e.target.checked)
}, [] )
위에 번거로움을 줄이기 위해 다음처럼 useToggle 커스텀 훅을 구현하겠습니다.




라디오 버튼 구현 방법
여러 값 중에서 하나만 고를 수 있게 할 때 라디오 버튼을 사용합니다. 그런데 라디오 버튼은 구현이 조금 까다롭습니다.
먼저 라디오 버튼을 여러 개 사용하려면 다음처럼 name속성에 모두 같은 이름을 부여해야 합니다.
라디오 버튼 구현 예
<input type="radio" name="same name" />
<input type="radio" name="same name" />
<input type="radio" name="same name" />
그런데 라디오 버튼이나 체크 박스는 value(혹은 defaultValue) 속성값을 화면에 보이게 하지 않습니다. 이 때문에 라디오 버튼 옆에 텍스트를 함께 보이려면 다음처럼 <label>등을 추가해야합니다.
<label>에 라디오 버튼 구현 예
<label>
<input type="radio" />
<span>라벨 텍스트</span>
</label>
라디오 버튼 구현


value 속성으로 라디오 버튼 선택 로직 구현하기
사용자가 선택한 라디오 버튼을 코드에서 알려면 checked 속성값에 다음과 같은 형태의 코드를 작성해야 합니다. 그런데 '값 === selectedValue'에서 값 부분을 구체적으로 어떻게 작성해야 할지 고민입니다.
사용자 선택 가져오기
const [selectedValue, setSelectedValue] = useState<String>('초기 선택 값')
<input type="radio" name="same name" checked={값 === selectedValue} />
<input type="radio" name="same name" checked={값 === selectedValue} />
<input type="radio" name="same name" checked={값 === selectedValue} />
만약 고차함수를 사용하고 싶지 않다면 값 부분은 다음처럼 defaultValue 속성을 사용해서 설정해야 합니다.
defaultValue 속성 사용
const [selectedValue, setSelectedValue] = useState<String>('초기 선택 값')
<input type="radio" name="same name" checked={값1 === selectedValue} defaultValue='값1' />
<input type="radio" name="same name" checked={값2 === selectedValue} defaultValue='값2' />
<input type="radio" name="same name" checked={값3 === selectedValue} defaultValue='값3' />
onChange 이벤트 콜백
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setSelectedValue(notUsed => e.target.value)
}


고차 함수로 라디오 버튼 선택 로직 구현하기
앞에 작성한 RadioInputTest 컴포넌트는 value속성을 사용해 선택 로직을 구현했지만, 고차함수를 사용할 때 는 다음처럼 라디오 버튼의 인택스로 어떤 버튼을 선택했는지 알 수 있습니다.
인덱스로 선택 값 가져오기
const [selectedIndex, setSelectedIndex] = useState<number>(0)
고차 함수로 onChange 콜백 구현
const onChange = useCallback((index: number) => () => setSelectedIndex(notUsed => index), [])
부분 함수 설정
<input onChange={onChange(index)} />


HTML <form> 요소
서버에서 HTML을 생성해 웹 브라우저 쪽에 전송하는 전통 방식의 웹 개발에서는 사용자에게 데이터를 입력받을 때 <form> 요소를 사용합니다. 이때 <form> 요소는 method속성에 데이터를 전송할 GET과 같은 HTTP 메소드를 설정하고 action 속성에는 폼 데이터를 전송한뒤 전환할 화면의 URL을 설정하는 방식으로 사용합니다. 만일 method 설정값이 POST이면 폼 데이터를 암호화하는 다음 3가지 방식 중 하나를 encType속성에 설정합니다.
1. application/x-www-form-urlencoded (기본값)
2. multipart/form-data
3. text/plain
하지만 리액트와 같은 SPA 방식 프런트엔드 프레임워크를 사용할 때는 백엔드 웹 서버가 API 방식으로 동작하므로 굳이 <form> 요소와 action, method, encType등의 속성을 설정할 필요가 없습니다. 다만 관습적으로 사용자 입력을 받는 부분을<form>요소로 구현합니다. 다음은 리액트로 <form>을 구현할 때 코드 패턴입니다.
폼 구현 패턴
<form>
<input type="submit" value="버튼_텍스트" />
</from>
그리고 사용자가 <input type="submit"> 버튼을 눌렀을 때 이벤트 처리는 <form> 요소의 onSubmit 이벤트 속성을 다음처럼 사용합니다. 참고로 FormEvent 타입 대신 ChangeEvent나 SyntacticEvent 타입을 사용해도 됩니다.
import type {FormEvent} from 'react'
...(생략)...
const onSubmit = (e: FormEvent<HTMLFormElement>) => {}
<form onSubmit={onSubmit}>
<input type="submit" value="버튼_텍스트" />
</form>
그런데 onSubmit 이벤트가 발생하면 <form>이 있는 웹 페이지를 다시 렌더링한다는 것입니다. 이 때문에 onSubmit을 구현할 때는 반드시 e.preventDefault()를 호출해 웹 페이지가 다시 렌더링되지 않도록 해야 합니다.
렌더링 막기
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() // 중요!
}
FormData 클래스
FormData는 자바스크립트 엔진이 기본으로 제공하는 클래스로서 사용자가 입력한 데이터들을 웹 서버에 전송할 목적으로 사용합니다. FormData클래스는 다음과 같은 메서드들을 제공하는데 보통은 append() 메서드만으로 충분합니다.

다음은 FormData의 append() 메서드를 호출해 (키, 값) 형태의 데이터를 추가하는 코드 예입니다.
데이터 추가
const fomrData = new FormData()
formData.append('name', 'Jack')
formData.append('email', 'jack@email.com')
만일 FormData의 내용을 JSON 포맷으로 바꾸고 싶다면 자바스크립트 엔진이 기본으로 제공하는 Object.fromEntries() 함수를 다음처럼 호출하면 됩니다.
const json = Object.fromEntries(formData)



객체 타입 값일 때 useState 훅 사용하기
앞서 작성한 BasicForm은 name과 email을 상탯값으로 만들었는데 다음처럼 객체의 속성형태로도 구현할 수있습니다.
속성 형태로 구현
type FormType = {
name: string
email: string
}
그리고 다음처럼 FormType 객체를 상태로 만들 수 있습니다. 그런데 이처럼 객체를 상태로 만들면 onChangeName, onChangeEmail과 같은 콜백 함수를 구현해야 합니다. 그러려면 깊은 복사와 얕은 복사, 그리고 타입스크립트의 전개 연산자 구문을 알아야 합니다.
객체를 상태로 만들기
const [form, setForm] = useState<FormType>({name: '', email: ''})
깊은 복사와 얕은 복사, 그리고 의존성 목록
대다수 프로그래밍 언어에서 어떤 변수에 담긴 값을 다른 변수에 복사할 때는 깊은 복사와 얕은 복사 라는 2가지 방식을 지원합니다. 복사 방식은 값의 타입에 따라 각기 다르게 적용됩니다. 만일 number, boolean등 값의 메모리 크기를 컴파일 타임 때 알 수 있는 타입은 항상 깊은 복사가 일어납니다. 반면에 객체, 배열 등 값의 메모리 크기를 런타임 때 알 수 있는 타입은 얕은 복사가 일어납니다. 한 가지 예외 상황은 string 타입 문자열인데, 타입스크립트에서 문자열은 항상 읽기 전용이므로 메모리 크기를 컴파일 타임 때 알 수 있습니다. 따라서 문자열은 깊은 복사가 일어납니다.
예시 )
1. 얕은복사
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original }; // 얕은 복사
shallowCopy.a = 10; // 원본에 영향 없음
shallowCopy.b.c = 20; // 원본에 영향 있음
console.log(original); // { a: 1, b: { c: 20 } }
console.log(shallowCopy); // { a: 10, b: { c: 20 } }
2. 깊은복사
const original = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(original)); // 깊은 복사
deepCopy.a = 10; // 원본에 영향 없음
deepCopy.b.c = 20; // 원본에 영향 없음
console.log(original); // { a: 1, b: { c: 2 } }
console.log(deepCopy); // { a: 10, b: { c: 20 } }
리액트 훅 프로그래밍에서 깊은 복사 여부가 중요한 이유는 대다수 훅 함수에 필요한 의존성 목록 때문입니다. 다음 코드에서 setForm()을 호출할 때 e.target.value로 얻은 name값을 어떻게 사용할지 고민됩니다.
name값을 어떻게 사용할까?
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const name = e.target.value
setForm(/* 구현 필요 */)
}, [form])
예를 들어 setForm()을 다음처럼 구현한다고 가정하겠습니다. 이코드는 객체를 복사하므로 얕은 복사가 적용됩니다.
복사 방식 비교
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const newForm = form // 얕은 복사
// const newForm = Object.assign({}, form) // 깊은 복사
newForm.name = e.target.value
setForm(newForm)
}, [form])
리액트 프레임워크는 내부적으로 form상태에 변화가 생겼는지를 form === newForm 형태로 비교합니다. 그런데 객체 타입의 복사는 항상 얕은 복사이므로 이 비굣값은 항항 true입니다.
따라서 리액트는 form에 아무런 변화가 없다고 간주합니다. 따라서 리액트는 웹 페이지를 다시 렌더링하지 않으므로 <input>에 값을 입력해도 form에 반영되지 않습니다.
이 얕은 복사 문제는 코드에 주석으로 해놓은 Object.assign() 함수를 사용하면 깊은 복사가 일어나 form === newForm이 항상 false가 되어 웹 페이지를 다시 렌더링 합니다. 따라서 웹 페이지는 정상으로 동작합니다 .
그런데 한가지 문제는 코드를 이런형태로 작성하는 것이 조금 번거롭다는 점입니다. 간결하게 코드 구현하는걸 해보겠습니다.
객체에 적용하는 타입스크립트 전개 연산자 구문
다음 코드에서 첫 줄은 두 객체 앞에 점 3개(...)가 붙었는데, 이연산자를 사용하는 코드의 위치에 따라 잔여 연산자 또는 전개 연산자 라고 합니다. 다음 코드는 전개 연산자를 사용해 두객체를 병합하는 예입니다.
전개 연산자
let coord = {...{x: 0}, ...{y: 0}};
console.log(coord); // {x: 0, y: 0}
다음코드는 깊은 복사가 일어나는 코드에서 Object.assign 호출을 전개 연산자로 좀 더 간결하게 구현한 것입니다.
전개 연산자로 간결하게 구현
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const newForm = {...from} // 깊은복사
newForm.name = e.target.value
setForm(newForm)
}, [form])
또한 타입스크립트 전개 연산자는 다음 처럼 객체의 속성값 가운데 가운데 일부를 변경할 수도 있습니다.
전개 연산자로 속성값 변경
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const newForm = {...form, name: e.target.value}
setForm(newForm)
}, [form])
타입스크립트 객체 반환 구문
앞 코드에서 setForm() 호출은 다음처럼 콜백 함수로 구현할 수 있습니다. 이렇게 하면 form을 useCallback의 의존성 목록에 추가하지 않아도 되므로 useCallback을 호출 할 때 선호하는 방식입니다. 그런데 이코드에서 return 키워드는 필요가 없습니다.
setForm()을 콜백으로 구현
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setForm(form => { return {...form, name: e.target.value})
}, [])
다음코드는 불필요한 return 키워드를 생략하고 새로운 form 객체를 반환하도록 구현한 예 입니다. 그런데 한 가지 문제는 타입스크립트 컴파일러가 {...form, name: e.target.value} 코드를 객체 구문이 아니라 복합 실행문으로 인식한다는 것입니다.
복합 실행문으로 인식
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setForm(form => {...form, name: e.target.value})
}, [])
타입스크립트에서 객체를 반환하는 구문은 객체를 의미하는 중괄호 {}를 다시 소괄호로 감싼 ({}) 형태로 사용해야 합니다 이는 복합 실행문은 소괄호로 감쌀 수 없다는 특성을 활용한 것입니다.
객체 반환 구문
const onChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setForm(form => ({...form, name: e.target.value}) )
}, [])
ObjectState.tsx 파일 구현하기



배열 타입 값일 때 useState 훅 사용하기
배열 또한 타입이므로 다음처럼 useState훅을 사용해 상태로 만들수 잇습니다.
배열을 상태로 만들기
const [images, setImages] = useState<string[]>([])
더욱더 간결하게 코드만들수가 있습니다.
onClick 이벤트 속성에 콜백 설정
<Icon name="add" onClick={addImage} />
<Icon name="clear_all" onClick={clearImage} />
배열에 적용하는 타입스크립트 전개 연산자 구문
앞서 객체에 적용하는 전개 연산자 구문을 알아보았는데, 전개연산자는 다음처럼 배열에도 적용할 수 잇습니다.
전개 연산자는 깊은 복사를 일으키므로 numbers === nuewNumbers는 항상 false입니다. 즉 number === newNumbers는 리액트의 의존성 목록 아이템으로 사용 할 수 있습니다.
배열에 전개 연산자 사용
const numbers = [1, 2, 3]
const newNumbers = [...numbers, 4]
다음은 전개연산자로 addImage 콜백 함수를 구현한 예 입니다.
전개 연산자 활용 addImage 콜백 구현
const addImage = useCallback(() => setImages(Images => [D.randomImage(), ...images]), [])
전개 연산자를 이용한 clearImages 콜백 구현
const clearImages = useCallback(() => setImages(netUsed => []), [])
ArrayState 컴포넌트 만들기

