리액트에서 함수현 컴포넌트를 사용하면 리액트의 편리한 hook
을 사용할 수 있다.
hook의 종류는 다양한데 그중 가장 기본적이고 제일 중요한 useState
를 다시 공부해 보자. 더 딥하게!
컴포넌트의 상태를 간편하게 생성하고 업데이트 해주는 도구를 제공해준다.
컴포넌트의 상태를 말한다.
const [state, setState] = useState(초기값);
컴포넌트의 현재 상태 값은 state 라는 변수에 들어있고 state를 변경하고 싶으면 setState 함수를 이용해서 변경할 수 있다. 여기서 state와 setState의 이름은 마음대로 지정할 수 있다.
setState를 이용해서 state를 변경하면 해당 컴포넌트는 화면에 다시 렌더링이 된다.
import { useState } from "react";
export default function useStatePage() {
const [time, setTime] = useState(1);
return (
<div>
<span> 현재 시간: {time}시 </span>
<button>Upadate</button>
</div>
);
}
숫자 1은 time 이라는 state 안에 들어있는 초기값 1을 나타낸다.
이제 버튼을 누를 때마다 시간이 업데이트 되도록 함수를 만들어주자.
handleClick 함수 안에 state의 값을 바꿔주는 도구인 setTime 함수의 인자로는 새 state로 반영될 값을 넣어준다. time + 1
import { useState } from "react";
export default function useStatePage() {
const [time, setTime] = useState(1);
const handleClick = () => {
setTime(time + 1);
};
return (
<div>
<span> 현재 시간: {time}시 </span>
<button onClick={handleClick}>Upadate</button>
</div>
);
}
버튼이 클릭이 될 때마다 handleClick 함수가 작동되면서 time state를 setTime 함수를 이용해서 state 가 업데이트가 된다.
여기서 state 가 변경될 때마다 브라우저가 다시 그려지는데 이는 불필요한 렌더링을 일어나게 한다. 이것은 뒤에서 다루겠다.
import { useState } from "react";
export default function useStatePage() {
const [time, setTime] = useState(1);
const handleClick = () => {
// newTime 변수생성
let newTime;
// 시간이 12시를 넘어간다면
if (time >= 12) {
// 다시 newTime을 1로 설정을 해준다.
newTime = 1;
} else {
// 그렇지 않으면 newTime에는 그냥 현재시간에 + 1 씩 증가
newTime = time + 1;
}
// setTime에 만든 변수 newTime 넣어주기
setTime(newTime);
};
console.log("업데이트!!!");
return (
<div>
<span>현재 시각 : {time}시 </span>
<button onClick={handleClick}>update</button>
</div>
);
}
출력을 해보면 12에서 넘어갈 때 다시 1로 돌아오는 것을 확인할 수 있다.
여기까지가 useState에 대한 전반적인 기본기이고 좀 더 깊에 알아보자.
우선 버튼을 누르기 전에 기본적으로 가지고 있어야 할 state를 만들어보자.
useState 안에는 배열로 이름 두 개를 넣어줄 것이다.
export default function useStatePage() {
const [names, setNames] = useState(["둘리", "짱구"]);
return (
<div>
<input type="text" />
<button>upload</button>
// names 이름을 돌면서 item 하나하나마다 p태그를 만들어준다.
// key 값으로는 배열의 idx 값을 주자.
{names.map((item, idx) => {
return <p key={idx}>{item}</p>;
})}
</div>
);
}
이제 input에 새 이름을 입력하고 업로드 버튼을 누를 때마다 setNames 함수를 이용해서 names stae를 업데이트해주자.
그전에 현제 input 안에 무슨 값을 가지고 있는지 알고 있어야 한다. 그럼 그 값이 무슨 값을 가지고 있는지 알려주는 state를 만들고, input 태그에 value 속성을 state인 input으로 지정을 해주자.
input state 가 사용자가 입력을 할 때마다 핸들링을 해줄 함수 onchange 도 만들자.
handleInputChange 함수는 인자를 event로 받고, setInput을 통해서 input state를 업데이트해줄 거다.
export default function useStatePage() {
const [names, setNames] = useState(["둘리", "짱구"]);
const [input, setInput] = useState("")
// 인자로는 event 받는다.
const handleInputChange = (event) => {
// event 안에 있는 target 안에 있는 value를 새로운 Input 으로 지정
setInput(event.target.value)
}
console.log(input)
return (
<div>
<input type="text" value={input}/>
<button onChange={handleInputChange}>upload</button>
// names 이름을 돌면서 item 하나하나마다 p태그를 만들어준다.
// key 값으로는 배열의 idx 값을 주자.
{names.map((item, idx) => {
return <p key={idx}>{item}</p>;
})}
</div>
);
}
자, 이제 Input state가 사용자의 입력을 받을 때마다 어떻게 변하는지 console.log로 확인을 해보자.
input을 입력할 때마다 state가 업데이트되는 것을 볼 수 있다.
그럼 이제 upload를 처리해 줄 함수를 만들어보자.
export default function useStatePage() {
const [names, setNames] = useState(["둘리", "짱구"]);
const [input, setInput] = useState("")
const handleInputChange = (event) => {
setInput(event.target.value)
}
const handleUpload = () =>{
// 인자에는 3가지 이름이 들어가야한다.
setNames(["둘리","짱구","소현"])
}
return (
<div>
<input type="text" value={input}/>
<button onClick={handleUpload}>upload</button>
// names 이름을 돌면서 item 하나하나마다 p태그를 만들어준다.
// key 값으로는 배열의 idx 값을 주자.
{names.map((item, idx) => {
return <p key={idx}>{item}</p>;
})}
</div>
);
}
setNames 함수를 불러서 state를 업데이트해주자. 인자에는 3가지 이름이 들어간다. 하지만 조금 이상하다.
const handleUpload = () =>{
// 인자에는 3가지 이름이 들어가야한다.
setNames(["둘리","짱구","소현"])
}
새로 업데이트해줄 state는 이전에 이미 존재하던 state와 밀접하게 연관이 되어있다. 이런 경우 setNames 인자 안에 바로 값을 주는 것이 아니라 콜백 함수를 전달해 준다. 그리고 return 값이 바로 새롭게 업데이트될 state 값이 되는 것이다. 콜백의 인자로는 업데이트 전 이전 상태의 state를 가지고 있게 된다. 이름은 prevState 다.
리턴해줄 값은 새로운 state인 어떤 배열이 될 것이고, 첫 번째 값은 input 이 될 것이고 뒤에 올 값은 prev 안에 들어있는 이전 state가 될 것이다. 지금의 경우는 ["둘리", "짱구"]를 담고 있는 배열일 것이다.
const handleUpload = () =>{
setNames((prevState) => {
console.log("이전 state", prevState) // ["둘리", "짱구"]
return([input, ...prevState])
})
}
그런데 state들이 업데이트될 때마다 컴포넌트는 계속해서 렌더링이 된다. names state에 초기값을 가져올 때 무거운 작업을 한다면 컴포넌트가 계속해서 렌더링이 되기 때문에 성능에 좋지 않다.
이 경우 맨 처음에 렌더링이 될 때만 불리게 해주는데 useState에 콜백으로 넣어준다. return 값으로는 해당 무거운 작업을 하는 함수를 넣어주면 된다.
import { useState } from "react";
// 📌 무거운 함수
const heaywork = () => {
console.log("무거운작업");
return ["둘리", "짱구"];
};
export default function useStatePage() {
const [names, setNames] = useState(() => {
return heaywork();
});
const [input, setInput] = useState("");
const handleInputChange = (e) => {
setInput(e.target.value);
};
// console.log(input);
const handleUpload = () => {
setNames((prev) => {
console.log("이전스테이트:", prev);
return [input, ...prev];
});
};
return (
<div>
<input type="text" value={input} onChange={handleInputChange} />
<button onClick={handleUpload}>upload</button>
{names.map((name, idx) => {
return <p key={idx}>{name}</p>;
})}
</div>
);
}
콘솔에 찍어보면 무거운 함수는 처음에만 렌더가 되는 것을 확인할 수 있다.
정리하자면 초기값을 가져올 때 무거운 작업을 해야 한다면 바로 안에 값을 넣어주는 것이 아니라 콜백 형태로 우리가 원하는 값을 리턴해주는 콜백을 리턴해주면 맨 처음 렌더링 될 때만 그 함수가 실행되는 것을 확인할 수 있다.
지금까지 나는 상태 값만 바꿔주는 경우로 state를 사용해왔다. 하지만 위의 내용을 통해 useEffect의 역할을 useState 가 초기값을 무거운 타 함수를 콜백으로 넣어 불필요한 렌더링을 잡아 줄 수 있다는 것이 굳이 useEffect를 쓰지 않고 처리할 수 있다는 점에서 너무 효율적이라고 생각한다. 이것은 리팩토링 과정에서 바로 써먹어야겠다.