"use client";
import { useState } from "react";
export default function Home() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div className='m-20'>
<button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded mb-2'>
Click me
</button>
<p>Count is: {count}</p>
</div>
);
}
그런데 버튼을 한 번 클릭 할 때 마다 4씩 한번에 증가시키기 위해 handleClick을 아래와 같이 수정하면 원하는 대로 동작할까? 기대와는 다르게 여전히 1씩 증가할 것이다.
이는 이전 상태가 고려되지 않았기 때문이다. setState는 비동기적 처리 과정이므로 setState의 인자로 state를 사용하면 아직 state가 갱신되기 전의 값이 계속 들어가기 때문에 count에는 계속 0이 들어가게 된다.
const handleClick = () => {
setCount(count + 1); // setCount(0 + 1);
setCount(count + 1); // setCount(0 + 1);
setCount(count + 1); // setCount(0 + 1);
setCount(count + 1); // setCount(0 + 1);
};
여기서 처음 의도한 대로 한번에 4씩 증가시키려면 함수를 인자로 사용하면 된다. 함수를 사용하여 이전 값을 인자로 전달받으면 이제 count 대신 이전 값을 사용할 수 있다.
setState의 인자로 사용된 함수는 이전 state값을 전달받으며 그 값을 이용한 함수들은 큐에 저장되어 순서대로 실행된다. 따라서 큐에서 차례로 prev(이전) 값을 받아 수행할 수 있으니 모든 setState 구문이 동작하는 것이다.
const handleClick = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
export default function ProductCard({ id }) {
if (!id) {
return "No id provided";
}
return (
<section>
{
// Product card...
}
</section>
);
}
React Hook "useState"가 조건부로 호출됩니다. 모든 구성 요소 렌더에서 React Hook을 정확히 같은 순서로 호출해야 합니다. 조기 반환 후에 실수로 React Hook을 호출했습니까?
"use client";
import { useState, useEffect } from "react";
export default function ProductCard({ id }) {
if (!id) {
return "No id provided";
}
const [something, setSomething] = useState("test");
useEffect(() => {}, [something]);
return (
<section>
{
// Product card...
}
</section>
);
}
"use client";
import { useState, useEffect } from "react";
export default function ProductCard({ id }) {
const [something, setSomething] = useState("test");
useEffect(() => {}, [something]);
if (!id) {
return "No id provided";
}
return (
<section>
{
/* Product card... */
}
</section>
);
}
"use client";
import { useState, useEffect } from "react";
export default function ProductCard({ id }) {
const [something, setSomething] = useState("test");
useEffect(() => {}, [something]);
return (
<section>
{!id
? "No id provided"
: {
/* Product card... */
}}
</section>
);
}
"use client";
import { useState } from "react";
export default function User() {
const [user, setUser] = useState({ name: "", city: "", age: 50 });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ name: e.target.value });
};
console.log(user);
return (
<form className='m-20'>
<input
type='text'
className='border-solid border-2 border-sky-500 p-1'
placeholder='Your name'
onChange={handleChange}
/>
</form>
);
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser({
...user,
name: e.target.value,
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser((prev) => {
return {
...prev,
name: e.target.value,
};
});
};
"use client";
import { useState } from "react";
export default function Form() {
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
});
return (
<form className='flex flex-col gap-y-2 m-4'>
<input
type='text'
name='firstName'
placeholder='first name'
className='px-4 py-2 border-solid border-2 border-sky-500 p-1'
/>
<input
type='text'
name='lastName'
placeholder='last name'
className='px-4 py-2 border-solid border-2 border-sky-500 p-1'
/>
<input
type='text'
name='email'
placeholder='email'
className='px-4 py-2 border-solid border-2 border-sky-500 p-1'
/>
<input
type='text'
name='password'
placeholder='password'
className='px-4 py-2 border-solid border-2 border-sky-500 p-1'
/>
</form>
);
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
"use client";
import { useState, useEffect } from "react";
const PRICE_PER_ITEM = 500;
export default function Cart() {
const [quantity, setQuantity] = useState(1);
const [totalPrice, setTotalPrice] = useState(0);
const handleClick = () => {
setQuantity(quantity + 1);
};
useEffect(() => {
setTotalPrice(quantity * PRICE_PER_ITEM);
}, [quantity]);
return (
<div className='m-4'>
<button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded'>
Add {quantity} item
</button>
<p>Total price: {totalPrice}</p>
</div>
);
}
하지만 totalPrice state와 useEffect를 굳이 사용할 필요 없이 동일하게 동작하도록 코드를 간소화 할 수 있다.
처음 컴포넌트가 렌더링 될 때 PRICE_PER_ITEM을 500으로 설정했으므로 totalPrice는 500이 된다. 버튼을 클릭하면 handleClick 함수가 실행되며 quantity를 업데이트하므로 리렌더링 되며 totalPrice line도 다시 실행되며 totalPrice 값이 업데이트 된다.
이렇게 이미 존재하는 state에서 파생하거나 계산할 수 있다면 매번 위와 같이 새로운 state를 생성하고 useEffect를 사용할 필요가 없다.
"use client";
import { useState } from "react";
const PRICE_PER_ITEM = 500;
export default function Cart() {
const [quantity, setQuantity] = useState(1);
const totalPrice = quantity * PRICE_PER_ITEM;
const handleClick = () => {
setQuantity(quantity + 1);
};
return (
<div className='m-4'>
<button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded'>
Add {quantity} item
</button>
<p>Total price: {totalPrice}</p>
</div>
);
}
아래의 코드는 버튼을 클릭하면 price의 값을 0으로 초기화 시켜주는 코드다. 그리고 동시에 컴포넌트가 렌더링 될 때 마다 console을 찍고 있다. 이 상태에서 새로고침하면 컴포넌트가 처음 마운트 될 때 Price 컴포넌트의 모든 명령문들이 실행된다.
그런데 버튼을 몇번을 다시 클릭해봐도 처음 렌더링 된 이후로는 console이 찍히지 않는 것을 확인할 수 있다. 그 이유는 현재 price의 값과 버튼을 클릭해서 변경한 price의 값이 같기 때문에(true) state가 변경되지 않아 리렌더링이 발생하고 있지 않기 때문이다. state를 동일한 문자열로 변경하고 버튼을 클릭해도 리렌더링 되지 않는다.
"use client";
import { useState } from "react";
export default function Price() {
const [price, setPrice] = useState(0);
const handleClick = () => {
setPrice(0);
};
console.log("Component rendering");
return (
<div className='m-10'>
<button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded'>
Click me
</button>
</div>
);
}
그러나 state의 값이 객체라면 어떻게 될까? state 값을 객체로 바꾸고 setState도 똑같은 객체로 변경해서 다시 버튼을 클릭해보면 이전과는 다르게 버튼을 클릭할 때 마다 리렌더링 되는 것을 확인할 수 있다. 버튼을 클릭할 때 마다 똑같은 값으로 변경하는데 왜 리렌더링이 매번 발생할까? 그 이유는 string이나 number는 값에 의한 전달(Pass-by-value), 객체는 참조에 의한 전달(Pass-by-reference)이기 때문이다.
객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달되는데 이것을 참조에 의한 전달이라 한다. 그렇기 때문에 객체는 설령 값이 같다고 하더라도 두 객체의 주소값은 다르기 때문에 javascript는 서로 다른 객체라고 판단하며, 리액트도 두 객체는 서로 다른 값이므로 버튼을 클릭할 때 마다 매번 리렌더링을 시키는 것이다.
"use client";
import { useState } from "react";
export default function Price() {
const [price, setPrice] = useState({
number: 100,
totalPrice: true,
});
const handleClick = () => {
setPrice({
number: 100,
totalPrice: true,
});
};
console.log("Component rendering");
return (
<div className='m-10'>
<button onClick={handleClick} className='bg-blue-500 px-4 py-2 text-white rounded'>
Click me
</button>
</div>
);
}
아래 코드는 서버로 부터 데이터를 불러와 post state를 업데이트하고 있다. 그런데 아래의 코드를 실행해보면 title이라는 프로퍼티를 찾을 수 없다는 에러를 볼 수 있을 것이다.
useEffect는 컴포넌트가 렌더링 된 후에 실행된다. tsx(jsx) 부분은 이미 렌더링이 완료 됐는데 fetch 요청은 아직 완료되지 않았으므로 post 값을 아직 불러오지 못한 상태에서 title과 body에 접근을 시도하려고 하기 때문에 아래와 같은 에러가 발생하는 것이다.
"use client";
import { useState, useEffect } from "react";
export default function BlogPostExample() {
const [post, setPost] = useState(null);
useEffect(() => {
fetch("https://dummyjson.com/posts/1")
.then((res) => res.json())
.then((data) => {
setPost(data);
});
}, []);
return (
<article className='m-10'>
<h1>Title: {post.title}</h1>
<br />
<p>Body: {post.body}</p>
</article>
);
}
?.
, optional chaining)을 사용할 수 있다. 옵셔널 체이닝은 값이 null 또는 undefined인 경우 undefined를 반환하고, 그렇지 않으면 프로퍼티 참조를 이어간다. 옵셔널 체이닝을 사용하면 정상적으로 title과 body를 불러오는 것을 확인할 수 있다."use client";
import { useState, useEffect } from "react";
export default function BlogPostExample() {
const [post, setPost] = useState(null);
useEffect(() => {
fetch("https://dummyjson.com/posts/1")
.then((res) => res.json())
.then((data) => {
setPost(data);
});
}, []);
return (
<article className='m-10'>
<h1>Title: {post?.title}</h1>
<br />
<p>Body: {post?.body}</p>
</article>
);
}
옵셔널 체이닝으로도 문제를 해결할 수는 있지만 만약 데이터가 너무 많아 호출에 시간이 걸린다면 그동안 사용자는 아무것도 없는 빈 화면을 보고 있어야 한다. 이는 사용자 경험에 좋지 못한 영향을 미치게 된다. 이때 우리는 데이터를 받아 올 때까지 loading spinner 같이 대체해서 보여줄 수 있는 요소를 추가해 주는 것이 좋다.
아래의 코드는 loading state를 추가하여 loading이 true 이면 "Loading..."이란 문구를, 데이터 호출이 완료되면 loading을 false로 변경하고 title과 body를 조건부 렌더링으로 보여주고 있다. 이렇게 loading state를 추가해 줌으로써 사용자에게 현재 데이터를 불러오고 있음을 인지시켜 줄 수 있게 되었다.
"use client";
import { useState, useEffect } from "react";
export default function BlogPostExample() {
const [post, setPost] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://dummyjson.com/posts/1")
.then((res) => res.json())
.then((data) => {
setPost(data);
setLoading(false);
});
}, []);
return (
<article className='m-10'>
{loading ? (
"Loading..."
) : (
<>
<h1>Title: {post.title}</h1>
<br />
<p>Body: {post.body}</p>
</>
)}
</article>
);
}
const [loading, setLoading] = useState<boolean>(true);
type Post = {
title: string;
body: string;
};
export default function Price() {
const [post, setPost] = useState<Post | null>(null);
// ...
return (
<article className='m-10'>
// ...
</article>
);
}
아래의 코드에서는 현재 윈도우의 너비를 불러오는 resize 이벤트를 컴포넌트가 마운트(DOM 객체가 생성되고 브라우저에 나타나는 것) 될 때 등록하고 있다. 그리고 너비가 변경될 때 마다 windowSize state를 업데이트하게 된다.
하지만 clean up function이 없다면 컴포넌트가 언마운트(컴포넌트가 DOM에서 제거되는 것) 되어 더 이상 보여지고 있지 않지만 여전히 이벤트 리스너는 연결되어 있기 때문에 clean up function을 통해 컴포넌트가 언마운트 될 때 이벤트 리스너를 제거해준다.
"use client";
import { useState, useEffect } from "react";
export function ExampleComponent1() {
const [windowSize, setWindowSize] = useState(1920);
useEffect(() => {
const handleWindowsSizeChange = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener("resize", handleWindowsSizeChange);
return () => {
window.removeEventListener("resize", handleWindowsSizeChange);
};
}, []);
return <div>Component 1</div>;
}
"use client";
import { useState, useEffect } from "react";
export function ExampleComponent1() {
const [windowSize, setWindowSize] = useState(1920);
useEffect(() => {
const handleWindowsSizeChange = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener("resize", handleWindowsSizeChange);
return () => {
window.removeEventListener("resize", handleWindowsSizeChange);
};
}, []);
return <div>Component 1</div>;
}
export function ExampleComponent2() {
const [windowSize, setWindowSize] = useState(1920);
useEffect(() => {
const handleWindowsSizeChange = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener("resize", handleWindowsSizeChange);
return () => {
window.removeEventListener("resize", handleWindowsSizeChange);
};
}, []);
return <div>Component 2</div>;
}
// ExampleComponent3
// ExampleComponent4
// ExampleComponent5
// ...
"use client";
import { useState, useEffect } from "react";
export const useWindowSize = () => {
const [windowSize, setWindowSize] = useState(1920);
useEffect(() => {
const handleWindowsSizeChange = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener("resize", handleWindowsSizeChange);
return () => {
window.removeEventListener("resize", handleWindowsSizeChange);
};
}, []);
return windowSize;
};
import { useWindowSize } from "@/hooks/useWindowSize";
export function ExampleComponent1() {
const windowSize = useWindowSize();
return <div>Component 1</div>;
}
export function ExampleComponent2() {
const windowSize = useWindowSize();
return <div>Component 2</div>;
}