지금까지 우리가 만든 컴포넌트는 동적으로 바뀌는 상황이 없었다. 즉, 값이 바뀌는 일이 없었다.
이번에는 컴포넌트에서 보여줘야 하는 내용이 사용자 인터랙션에 따라 바뀌어야 할 때 어떻게 구현할 수 있는지에 대해 알아보자.
react 16.8 이전 버전에서는 함수형 컴포넌트에서는 상태를 관리할 수 없었다. 그런데 react 16.8부터 hooks라는 기능이 도입되면서 함수형 컴포넌트에서도 상태를 관리할 수 있게 되었다.
이번에는 useState 함수를 통해 상태를 관리하는 방법을 알아볼건데, 이 useState가 hooks중 하나이다.
버튼을 누르면 숫자가 바뀌는 counter 예제를 통해 상태를 어떻게 관리하는지 배워보자.
먼저 src 폴더 안에 Counter.js 파일을 생성한다.
import React from 'react';
function Counter() {
return (
<div>
<h1>0</h1>
<button>+1</button>
<button>-1</button>
</div>
);
}
export default Counter;
이제 App 컴포넌트에서 Counter 컴포넌트를 사용해보자.
import React from 'react';
import Counter from './Counter';
function App() {
return (
<Counter />
);
}
export default App;
먼저, Counter 컴포넌트에서 이벤트 설정하는 방법부터 알아보자.
버튼에 클릭 이벤트가 발생했을 때 특정 함수가 호출되도록 설정해보자.
// Counter.js
import React from 'react';
function Counter() {
const onIncrease = () => {
console.log('+1');
};
const onDecrease = () => {
console.log('-1');
};
return (
<div>
<h1>0</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
버튼을 누르면 콘솔에 잘 찍히는지 확인해보자.
여기서 주의할 점은 onClick = {onIncrease()}
처럼 onIncrease 함수를 호출하면 안된다.
호출해서 할당하면 브라우저에 랜더링될 때 이미 onIncrease 함수가 호출되기 때문이다. 버튼을 클릭했을 때만 호출해야 하므로 onIncrease를 호출하지 않고 적어야 한다.
이제 컴포넌트에서 동적인 상태를 끼얹어보자.
// Counter.js
// react에서 useState 함수를 불러온다.
import React, { useState } from 'react';
function Counter() {
// number라는 상태를 만들건데, number 상태의 초기값은 0이라고 하겠다.
// setNumber는 useState(0)에서 초기값 0을 바꿔주는 함수이다.
const [number, setNumber] = useState(0);
const onIncrease = () => {
console.log('+1');
};
const onDecrease = () => {
console.log('-1');
};
return (
<div>
<h1>0</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
원래 const [number, setNumber] = useState(0);
이 코드는 다음과 같다.
const number = numberState(0);
const setNumber = numberState(1);
디스트럭처링 할당을 통해 한줄로 줄여쓸 수 있었다. useState가 호출되면 배열을 반환하게 되는데, 첫 번째 요소를 number, 두 번째 요소를 setNumber라는 이름으로 추출하겠다는 뜻이다.
이제, setNumber가 초기값을 바꿔주는 함수라고 했으니, console.log들을 지우고 코딩해보자.
import React, { useState } from 'react';
function Counter() {
const [number, setNumber] = useState(0);
const onIncrease = () => {
setNumber(number + 1);
};
const onDecrease = () => {
setNumber(number - 1);
};
return (
<div>
{/*바뀌는 상태를 랜더링 해야 하기 때문에 0을 지우고 {number}를 입력*/}
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
Counter를 완성했다.
만약 useState(5)라고 한다면 초기값이 0이 아니라 5부터 시작하게 된다.
추가적으로, useState를 사용할 때 함수형 업데이트를 할 수 있다.
위의 예제에서 setNumber에서 다음 업데이트하고 싶은 값인 number를 넣어주었는데 number 대신 그 다음 값을 어떻게 하겠다는 함수를 넣어줄 수 있다.
const onIncrease = () => {
setNumber(prevNumber => prevNumber + 1);
};
기존에 number + 1이라고 하면, 상태 number를 참조해서 1 더한 값을 넣지만, 함수를 넣어줄 수 있고 이를 업데이트 함수라고 한다.
지금 당장은 왜 사용하는지 이해가 안갈 수 있고 아직은 큰 차이가 없을 수 있다. 그런데, 나중에 리액트 컴포넌트를 최적화할 때 업데이트 함수가 필요하다. 지금은 업데이트 함수가 최적화와 관련이 있다는 것만 알아두고 넘어가자.
리액트에서 input 상태를 어떻게 관리하는지 예시를 통해 살펴보자.
이번 예시는 input에 입력한 값이 출력되고 초기화 버튼을 누르면 input에 입력한 값이 초기화되는 예시이다.
src 폴더 아래에 InputSample.js 파일을 만든다.
// InputSample.js
import React from 'react';
function InputSample() {
return (
<div>
<input />
<button>초기화</button>
<div>
<b>값 : </b>
어쩌고 저쩌고..
</div>
</div>
);
}
export default InputSample;
App.js에서 InputSample을 불러온다. 이번에는 onchange 이벤트를 사용해보자.
// InputSample.js
// useState 함수 불러오기
import React, { useState } from 'react';
const InputSample = () => {
// 상태 설정 + input의 상태를 체크하기 때문에 초기값은 빈 문자열
const [text, setText] = useState('');
const onClick = () => {
setText('');
};
const onChange = e => {
setText(e.target.value);
};
return (
<div>
<input onChange={onChange} value={text}/>
<button onClick={onClick}>초기화</button>
<div>
<b>값 : {text}</b>
</div>
</div>
);
};
input태그에 value props를 text로 준 이유는 이를 빼면 초기화 버튼을 눌렀을 때 입력이 초기화되지 않고 그대로 있다. 버튼을 클릭하면 onClick 함수로 인해 text 상태가 빈 문자열이 바뀌는데 이는 input 값이 알 수 없다. 따라서 value={text}라고 해주어야 한다.
위와 같은 여러 개의 input 상태 관리하는 방법을 알아보자.
InputSample을 수정해보자.
import React, { useState } from 'react';
function InputSample() {
// 초기값은 빈 문자열
const [text, setText] = useState('');
const onReset = () => {
};
const onChange = e => {
};
return (
<div>
<input placeholder="이름" />
<input placeholder="닉네임" />
<button onClick={onReset}>초기화</button>
<div>
<b>값 : </b>
이름 (닉네임)
</div>
</div>
);
};
export default InputSample;
input이 두 개인 경우, useState도 두개, onChange 함수도 두개로 구현할 수 있지만 이는 좋은 방법이 아니다.
더 좋은 방법은 input에 name이라는 값을 설정하고 이벤트가 발생했을 때 이 값을 참조하는 것이고 useState는 문자열을 관리하는 것이 아닌 여러 개의 문자열을 담을 수 있는 객체로 관리하는 것이다.
그러면 useState의 인수로 하나의 값이 아니라 객체가 들어온다.
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: ''.
});
// 디스트럭처링 할당(중복을 피하기 위해)
const [name, nickname] = inputs;
const onChange = e => {
const [name, value] = e.target;
// input이 하나였을 땐 이렇게 했지만 여러 개일땐 객체를 넣어야 한다.
setInputs(value);
};
};
input이 여러개일때 객체를 넣어야 한다. 리액트에서 객체를 업데이트 할때는 방법이 조금 다르다. 객체를 업데이트하려면 기존의 객체를 복사해야 한다.
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: '',
});
const { name, nickname } = inputs;
const onReset = () => {
};
const onChange = e => {
const [name, value] = e.target;
const newInputs = {
// 기존 객체를 복사
...inputs,
// 새로운 value로 덮어씌움
[name]: value,
};
setInputs(nextInputs);
};
return (
<div>
<input name="name" placeholder="이름" onChange={onChange} />
<input name="nickname" placeholder="닉네임" onChange={onChange} />
<button onClick={onReset}>초기화</button>
<div>
<b>값 : </b>
이름 (닉네임)
</div>
</div>
);
};
그런데, 굳이 newInputs라는 변수를 만들어 다시 setInputs에 전달할 필요 없이 setInputs 안에서 처리해줘도 된다.
해당 부분을 변경해보면,
const onChange = e => {
const [name, value] = e.target;
setInputs({
...inputs,
[name]: value,
})
};
초기화 기능도 구현해보자.
const onReset = () => {
setInputs({
name: '',
nickname: '',
});
};
input의 value값도 설정해주자.
import React, { useState } from 'react';
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: '',
});
const { name, nickname } = inputs;
const onReset = () => {
setInputs({
name: '',
nickname: '',
})
};
const onChange = e => {
const [name, value] = e.target;
setInputs({
...inputs,
[name]: value,
});
};
return (
<div>
<input
name="name"
placeholder="이름"
onChange={onChange}
value={name}
/>
<input
name="nickname"
placeholder="닉네임"
onChange={onChange}
value={nickname}
/>
<button onClick={onReset}>초기화</button>
<div>
<b>값 : </b>
{name}({nickname})
</div>
</div>
);
};
export default InputSample;
정리를 해보면, 여러 개의 input의 상태는 객체 형태로 관리를 해야 한다.
useState({객체})
또, input값이 변경되어 객체를 업데이트 해야 한다면 기존의 객체를 복사한 후 덮어씌우는 방식으로 업데이트 해야 한다. 이렇게 하는 것을 불변성을 지킨다 라고 한다.
불변성을 지켜줘야만 리액트 컴포넌트에서 상태가 업데이트 됐음을 감지할 수 있고 이에 따라 필요한 랜더링이 발생하게 된다. 또, 이 불변성을 지켜야만 컴포넌트 최적화도 가능하게 된다.
이렇게 하지 않으면 상태가 변경된 것이 랜더링 되지 않는다.