리액트에서 함수형 컴포넌트를 사용한다면 훅(Hook)이라는 기능을 필수적으로 사용하게 된다. 지난 포스팅에서도 다루었지만, 예제와 함께 더 자세하게 기본 중의 기본 훅인 useState
를 알아볼 것이다.
자바스크립트 ES6의 Destructuring Assignment와 Spread syntax 문법을 알아야 이해할 수 있다.
Destructuring Assignment(구조 분해 할당)이란, 이름 그대로 객체나 배열 등의 구조를 분해해서 할당해주는 문법이다. 이름만 거창하지 코드를 보면 이해가 쉽다.
const person = {
name : "hyun",
job : "developer"
}
const name = person.name;
const job = person.job;
기존 문법에서 객체의 key,value에 접근하기 위해서는 .
을 통해 코드를 작성했어야 했다. Destructuring Assignment를 이용하게 되면 객체의 key를 {}
안에 정의해주고 객체를 넣어주면, 객체의 key와 value들이 각각 할당된다.
const person = {
name : "hyun",
job : "developer"
}
const {name,job} = person;
객체 안의 key의 이름을 그대로 쓰지 않고 바꿀 수도 있다.
const person = {
name : "hyun",
job : "developer"
}
const {name: personName, job: personJob} = person;
console.log(personName, personJob); // hyun, developer
객체 뿐만 아니라 배열에서도 동일하게 사용이 가능하다.
const people = ["철수", "영희"];
const first = people[0];
const second = people[1];
기존의 배열 원소를 접근하는 방법이다. 이를 Destructuring Assignment를 사용하면 아래처럼 쓸 수 있다.
const people = ["철수", "영희"];
const [first, second] = people;
console.log(first, second); // 철수, 영희
Spread Syntax란 배열이나 객체 앞에 ...
을 붙여서 펼쳐주는 문법이다. 이 문법 역시 코드를 보면 이해가 쉽다.
const obj1 = { key: 'key1'};
const obj2 = { key: 'key2'};
const array = [obj1, obj2];
객체들을 담고 있는 배열이 있다. 이 배열을 복사하기 위해서는 어떻게 해야 할까?
const copy = [...array];
[]
로 새로운 배열을 만들고, array
안에 들어있던 녀석들을 그 안에 ...
를 통해 기존의 배열을 벗겨낸 후 알맹이만 가져와 넣는다.
배열을 펼쳐서 새로운 원소를 추가하는 것도 간단하다.
const copy2 = [...array, { key: 'key3' }];
중요한 것은 spread 연산자는 객체 안에 들어있는 item들을 하나하나씩 복사해 오는 것이 아니라, 객체가 가리키고 있는 주소의 참조값만 복사해 온다는 것이다. spread 연산자를 통해 복사해 온다고 해도, 복사 원본을 변경하게 되면 복사한 녀석도 같이 변경이 되기 때문에 주의해야 한다. 배열 뿐만 아니라 객체에도 사용이 가능하다.
리액트에서 state
란 말 그대로 상태, 컴포넌트가 가지는 상태를 말한다. 예를 들어 사람이라는 컴포넌트가 있다면 이름, 나이, 성별 등의 상태를 가질 수 있다. useState
는 컴포넌트의 상태를 생성&업데이트 할 수 있는 도구(훅)이다.
useState
를 사용하기 위해서는 반드시 import를 해야 한다.
import { useState } from 'react';
useState
는 자바스크립트 ES6의 Destructuring Assignment 문법과 함께 자주 사용한다.
const [state, setState] = useState(초기값);
먼저, state
의 생성과 동시에 초기값을 useState
함수에 인자로 넣어주면, state
와 setState
를 배열 형태로 리턴해준다. 현재 상태값은 state
에 저장되고, 상태를 변경하고 싶을 땐 setState
함수를 이용한다.
const [name, setName] = useState("hyun");
물론 state
와 setState
의 이름은 우리가 원하는 대로 정할 수 있다. 사람이라는 컴포넌트 안에 name
이 있다면, 이를 변경하고 싶을 때는setState("hyun123")
과 같이 인자로 업데이트할 값을 넘겨주면 된다.
setState
를 이용해 state
를 변경하면, 해당 컴포넌트는 화면에 다시 업데이트(렌더링)이 된다.
import React, { useState } from 'react';
function App() {
return (
<>
<span> 숫자 : 1 </span>
<button> 업데이트 </button>
</>
)
}
export default App;
간단한 페이지를 구성해보았다. 지금은 업데이트 버튼을 눌러도 아무런 반응이 없는 상태이다. 여기서 state
를 하나 추가해보자.
import React, { useState } from 'react';
function App() {
console.log("렌더링🖌");
const [num, setNum] = useState(1);
const handleClick = () => {
setNum(num+1);
}
return (
<>
<span> 숫자 : { num } </span>
<button onClick = { handleClick }> 업데이트 </button>
</>
)
}
export default App;
num, setNum
을 useState
로 추가하고, 버튼을 클릭할 때 num
을 1씩 증가해 주는 handleClick
을 정의했다.
이 업데이트 버튼을 누를 때마다 state
가 변경되므로 해당 컴포넌트가 브라우저 상에 다시 그려지게(렌더링) 된다. 렌더링이 될 때마다num
에는 1이 증가된 값이 들어있게 된다.
이번에는 배열 값을 가지는 state
를 사용하고, 업데이트 하는 예제이다.
import { useState } from 'react';
function App() {
const [people, setPeople] = useState(["철수","영희"]);
return (
<>
<input type="text" />
<button> 업데이트 </button>
{ people.map((person, idx)=>{
return <p> {person} </p>
})}
</>
)
}
export default App;
people
이라는 state안에 철수,영희의 배열이 들어 있고, map 반복문으로 people
배열에 속해 있는 사람들을 화면에 출력하고 있다. 이제 input에 이름을 입력하고 업데이트 버튼을 누르면 people
안에 새로운 사람을 추가하는 코드를 작성해 볼 것이다.
import { useState } from 'react';
function App() {
const [people, setPeople] = useState(["철수","영희"]);
const [input, setInput] = useState("");
const handleInput = (e) => {
setInput(e.target.value);
}
console.log(input)
return (
<>
<input type="text" value={input} onChange={ handleInput } />
<button> 업데이트 </button>
{ people.map((person, idx)=>{
return <p> {person} </p>
})}
</>
)
}
export default App;
input
이라는 state
를 추가하여 input에 들어오는 값을 저장하게 했다. input 창에 값이 변화할 때마다 handleInput
이 호출되어 input
에 업데이트 된다. 콘솔창으로 확인해 보면 아래와 같이 될 것이다.
그 다음으로는 버튼을 눌렀을 때 input
에 들어있는 값을 people
에 추가해주면 된다.
import { useState } from 'react';
function App() {
const [people, setPeople] = useState(["철수","영희"]);
const [input, setInput] = useState("");
const handleInput = (e) => {
setInput(e.target.value);
}
const handleUpdate = (e) => {
setPeople((prev) => {
return ([input, ...prev]);
})
}
return (
<>
<input type="text" value={input} onChange={ handleInput } />
<button onClick={ handleUpdate }> 업데이트 </button>
{ people.map((person, idx)=>{
return <p> {person} </p>
})}
</>
)
}
export default App;
handleUpdate
라는 함수를 만들고, 버튼이 클릭될 때 호출되도록 onClick
에 걸어두었다. 예를 들어 민수를 추가한다고 할 때, setPeople(["철수","영희","민수"])
이렇게 들어가야 한다. 즉 setPeople([기존 state, 추가할 state])
가 되어야 한다.
const handleUpdate = (e) => {
setPeople((prev) => {
return ([input, ...prev]);
})
}
...prev
에는 철수,영희가 있고 input
에는 민수가 들어갈 것이다. 결과를 확인해보면 잘 동작한다.
앞에서 state
가 업데이트 될 때마다 컴포넌트가 다시 렌더링된다고 했다. 만약에 우리가 people
의 초기값을 가져올 때, 외부 API를 이용해서 값을 불러온다거나, 복잡한 가공을 거쳐야 한다거나 해서 무겁고 오래 걸리는 작업을 거친다고 가정해 보자. 그러면 계속해서 렌더링을 해야 하기 때문에 성능이 저하될 것이다. 그 상황을 한번 예제로 만들어보고 해결해 보자.
import { useState } from 'react';
const getPeople = () => {
console.log("People 정보를 가져오는 중 💦"); // 무겁고 오래 걸리는 작업
return ["철수", "영희"];
}
function App() {
const [people, setPeople] = useState(getPeople());
const [input, setInput] = useState("");
const handleInput = (e) => {
setInput(e.target.value);
}
const handleUpdate = (e) => {
setPeople((prev) => {
return ([input, ...prev]);
})
}
return (
<>
<input type="text" value={input} onChange={ handleInput } />
<button onClick={ handleUpdate }> 업데이트 </button>
{ people.map((person, idx)=>{
return <p> {person} </p>
})}
</>
)
}
export default App;
예제 2와 똑같은 코드에, getPeople
이라는 함수를 만들고 people
의 초기값으로 세팅해줬다.
input창에 글자를 쓸 때마다, input
state가 업데이트된다. state
가 업데이트 되면 컴포넌트가 다시 렌더링 되고, 컴포넌트가 렌더링 되면 컴포넌트 내부 값들이 다시 초기화되므로 people
또한 다시 초기화가 된다. 그 과정에서 무겁고 오래걸리는 작업이 계속해서 수행되는 것을 확인할 수 있다.
우리가 원하는 것은 맨 처음에 렌더링이 될 때만 getPeople
이 호출되는 것이다. 그렇게 하려면 setPeople
의 초기값을 넣어주는 인자에 바로 getPeople
을 넣어주는 것이 아니라, 콜백함수를 넣어 주는 것이다.
import { useState } from 'react';
const getPeople = () => {
console.log("People 정보를 가져오는 중 💦");
return ["철수", "영희"];
}
function App() {
// 초기값 인자에 콜백함수 넣어주기
const [people, setPeople] = useState(()=>{
return getPeople();
});
const [input, setInput] = useState("");
const handleInput = (e) => {
setInput(e.target.value);
}
const handleUpdate = (e) => {
setPeople((prev) => {
return ([input, ...prev]);
})
}
return (
<>
<input type="text" value={input} onChange={ handleInput } />
<button onClick={ handleUpdate }> 업데이트 </button>
{ people.map((person, idx)=>{
return <p> {person} </p>
})}
</>
)
}
export default App;
input
이나 people
state가 변경되어도 getPeople
이 호출되지 않는 것을 확인할 수 있다.
초기값을 가져올 때 무거운 작업을 해야 한다면 바로 안에 값을 넣는 것이 아니라, 우리가 원하는 값을 리턴해주는 콜백 함수를 넣어주자. 그러면 맨 처음에 렌더링될 때만 불려지게 된다.
const [state, setState] = useState("초기값");
과 같은 형태로 사용한다.setState()
를 사용해서 state를 변경할 때마다 컴포넌트는 다시 렌더링이 된다. setState((prev) => {
// 추가하거나 삭제하는 작업
return newState;
})
useState()
를 사용해서 초기화를 할 때, 무거운 일을 해야 한다면 인자로 콜백함수를 넣어주면 첫 렌더링 때만 실행되게 할 수 있다.