import { useEffect } from 'react';
useEffect(setup, dependencies?)
Side Effect를 수행하는 콜백 함수입니다.
dependencies가 빈 배열([])이면 컴포넌트가 처음 렌더링(Mounting) 될 때만 실행됩니다.
dependencies를 생략한다면 리렌더링(Updating) 될 때마다 실행됩니다.
dependencies배열 안에 있는 값이 있을 경우 해당 값이 변할 때에만 재실행됩니다.
리엑트에서 사이드 이펙트는 컴포넌트의 동작에 따라 파생되는 효과 정도로 해석이 가능합니다.
리액트 라이브러리는 사용자 입력에 반응하여 필요할 때 UI를 다시 렌더링하는 주요 임무가 있습니다.
사용자 입력에 반응한다는 것은 state나 이벤트, props를 관리하여 사용자와 상호 작용하고 화면에 무언가를 가져오는 것입니다.
예를 들어 버튼이 클릭되거나 텍스트가 입력되는 것이 있습니다.
Side Effect는 위에서 설명한 화면에 무언가를 가져오는 것과 직접적인 관련이 아닌 애플리케이션에서 일어나는 다른 모든 것을 뜻합니다.
즉, 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 흔히 Side Effect라고 일컽습니다.
예를 들면 http request를 보내는 것 또는 브라우저 저장소에 무언가를 저장하는 것입니다.
http request를 보내어 응답을 받았을 때 화면에 무언가를 그리도록 할 수는 있습니다.
하지만 request 보내는 것 자체나, 잠재적 오류를 처리하는 것 등은 화면에 직접 무언가를 그리는 작업이나 그와 비슷한 작업들과 관련되어 있지 않습니다.
Side Effect를 사용하는 컴포넌트에 대해 이야기 해보겠습니다.
컴포넌트는 클릭 등의 사용자 입력에 반응함으로써 실행되고 화면에 무언가를 가져오는 함수입니다.
컴포넌트는 리액트 라이브러리의 주요 임무에 맞춰 움직입니다.
따라서 Side Effect는 일반적인 컴포넌트 평가의 밖에서 일어나야 하는 일입니다.
Side Effect가 컴포넌트의 평가 안에서 일어난다면 문제가 생길 수 있습니다.
예를 들어 http request에 대한 응답으로 어떤 state를 변경한다면 무한 루프로 빠질 수도 있습니다.
request를 보내면 request에 대한 response으로 이 함수를 재평가하는 state를 변경하게 되고
함수가 다시 실행될 때마다 리퀘스트를 보내게 되면서 무한 루프에 빠질 수 있습니다.
그래서 버그나 무한 루프가 발생할 가능성이 높거나 http request가 너무 많이 보내질 수도 있는 Side Effect는 직접적으로 컴포넌트 함수에 들어가면 안됩니다.
useEffect 훅은 이런 사이드이펙트를 처리하는 좋은 도구입니다.
// useEffect(모든 컴포넌트 평가 후에 실행되어야 하는 함수, 의존성 배열)
// 의존성이 변경된다면 함수가 실행
useEffect(() => { ... }, [의존성])
useEffect 훅을 사용하면 컴포넌트가 리렌더링 될때 실행되지 않게 할 수 있으며, 의존성을 사용해서 원하는 경우에 Side Effect 코드를 실행시킬 수 있습니다.
아래의 코드는 아이디와 비밀번호를 입력하면 localStorage에 로그인 정보를 저장하고, isLoggedIn state가 true로 바꿉니다.
그래서 새로고침을 할 때 앱이 다시 시작하더라도 localStorage에서 로그인 정보를 불러와서 로그인 상태를 유지하게 합니다.
import React, { useState } from 'react';
import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// localStorage에서 로그인 정보 가져오기
const toredUserLoggedInInformation = localStorage.getItem("isLoggedIn");
// 로그인 정보 확인
if (toredUserLoggedInInformation === "1") {
setIsLoggedIn(true);
}
// 로그인 버튼 클릭
const loginHandler = (email, password) => {
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
// 로그아웃 버튼 클릭
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
<>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</>
);
}
export default App;
//Too many re-renders. React limits the number of renders to prevent an infinite loop.
하지만 위의 코드를 실행하면 무한 루프에 빠지게 됩니다.
렌더링할 때 localStorage에 로그인 정보를 확인하고 isLoggedIn state를 변경합니다.
state가 변경되면 컴포넌트는 리렌더링되고 다시 localStorage에 로그인 정보를 확인하고 isLoggedIn state를 변경합니다.
의존성이 없는 useEffect는 앱이 시작될 때 한 번만 실행됩니다.
따라서 localStorage에 로그인 정보를 확인하고 isLoggedIn이 변경되어도 다음 렌더링에서 useEffect는 isLoggedIn를 변경시키지 않아서 무한 루프에 빠지지 않습니다.
import React, { useState } from 'react';
import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// 첫 렌더링에만 실행
useEffect(() => {
// localStorage에서 로그인 정보 가져오기
const toredUserLoggedInInformation = localStorage.getItem("isLoggedIn");
// 로그인 정보 확인
if (toredUserLoggedInInformation === "1") {
setIsLoggedIn(true);
}
}, []);
// 로그인 버튼 클릭
const loginHandler = (email, password) => {
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
// 로그아웃 버튼 클릭
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
<>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</>
);
}
export default App;
이번 예시는 UI와 직접적인 관련이 없는 데이터 저장소 접근을, Side Effect로 useEffect를 사용했습니다.
아래의 코드는 이메일과 비밀번호 입력을 받습니다.
만약 입력한 이메일과 비밀번호가 유효성 검사에 적합하지 않다면, 제출 버튼은 비활성화됩니다.
import React, { useState } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const Login = (props) => {
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
// email 인풋에 입력될 때 이벤트
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
// 유효성 검사
setFormIsValid(
event.target.value.includes("@") && enteredPassword.trim().length > 6
);
};
// password 인풋에 입력될 때 이벤트
const passwordChangeHandler = (event) => {
setEnteredPassword(event.target.value);
setFormIsValid(
// 유효성 검사
event.target.value.trim().length > 6 && enteredEmail.includes("@")
);
};
// 인풋의 포커스가 해지될 때 이벤트
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
// 인풋의 포커스가 해지될 때 이벤트
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
// form이 제출 될 때 이벤트
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
/* 유효성 검사 통과되어야 Button 활성화 */
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
위의 코드와 로직은 비슷하지만 이메일 또는 비밀번호 필드의 키 입력(메인 이펙트)에 대한 응답으로 해당 폼의 유효성을 검사하고 업데이트하는 Side Effect를 useEffect로 처리해보겠습니다.
import React, { useEffect, useState } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const Login = (props) => {
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
// 유효성 검사
useEffect(() => {
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, [enteredEmail, enteredPassword]);
// email 인풋에 입력될 때 이벤트
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
// 기존 유효성 검사 제거
};
// password 인풋에 입력될 때 이벤트
const passwordChangeHandler = (event) => {
setEnteredPassword(event.target.value);
// 기존 유효성 검사 제거
};
// 인풋의 포커스가 해지될 때 이벤트
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
// 인풋의 포커스가 해지될 때 이벤트
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
// form이 제출 될 때 이벤트
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
/* 유효성 검사 통과되어야 Button 활성화 */
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
emailChangeHandler와 passwordChangeHandler에서 중복으로 사용하는 유효성 검사 코드를 useEffect에서 해결했습니다.
useEffect의 의존성 배열에 enteredEmail과 enteredPassword을 입력해서 인풋이 바뀔 때마다 useEffect가 실행되도록 하였습니다.
클린업 함수는 useEffect의 첫번째 parameter로 넣어준 함수의 return 함수를 말합니다.
클린업 함수는 useEffect의 모든 새로운 사이드이펙트 함수가 실행되기 전이나(Updating), 컴포넌트가 제거되기 전(Unmounting)에 실행됩니다.
또한 사이드이펙트 함수가 처음으로 실행되기 전에는 실행되지 않습니다.
즉, 클린업 함수는 컴포넌트의 첫 렌더링(Mounting)에서 실행되는 useEffect를 제외하고 useEffect가 다시 실행되기 전에 실행됩니다.
useEffect(() => {
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, [enteredEmail, enteredPassword]);
위의 예시 코드에서 input에 입력을 하면 enteredEmail과 enteredPassword state가 변화됩니다.
state가 변화하면서 useEffect도 실행되고, 만약 백엔드로 http 요청을 보낸다고 가정하면 불필요한 네트워크 트래픽이 발생합니다.
일정량의 키 입력을 수집하거나 키 입력 후 일정 시간 동안 중지되는 경우에만 Side Effect를 실행한다면 매번 state가 바뀌며 불필요한 네트워크 트래픽을 발생하는 것을 줄일 수 있습니다.
디바운싱이란 사용자가 여러번의 요청을 보내는 경우에 일정시간을 두어, 맨 처음 혹은 마지막에 한 번만 실행하고, 나머지 일정시간 동안에는 이벤트들을 무시하는 기법입니다.
useEffect의 Clean Up을 사용하면 쉽게 디바운싱을 구현하여 해결 할 수 있습니다.
enteredEmail과 enteredPassword state가 변화하면 useEffect가 실행됩니다.
useEffect는 setTimeout을 실행시키고 0.3초 후에 유효성 검사를 진행합니다.
만약 setTimeout을 실행 전에 enteredEmail과 enteredPassword state가 변화한다면 새로운 useEffect()가 실행되기 전에 이전 useEffect()의 Clean Up 함수가 실행됩니다.
Clean Up 함수인 clearTimeout은 실행 전인 setTimeout을 취소합니다.
useEffect(() => {
// 0.3초 후 유효검사 실행을 예약
const identifier = setTimeout(() => {
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, 300);
// identifier가 실행되기 전에 clean up 함수가 불리면 예약된 identifier를 취소
return () => {
clearTimeout(identifier);
};
}, [enteredEmail, enteredPassword]);
clearTimeout(setTimeout()이 반환한 값)
전역 clearTimeout() 메서드는 setTimeout()으로 생성한 타임아웃을 취소합니다.
만약 useEffect가 http 요청을 보내는 함수였다면 Clean Up 함수로 구현한 디바운싱 덕분에 한 번만 보내게 됩니다.