해당 내용은 https://www.udemy.com/course/best-react/ 강의를 들으며 정리하고 스스로 공부 한 내용을 기록 하였습니다.
useReducer()
useReducer()
훅은 useState
훅과 약간 비슷하다.
이 역시 리액트에 내장된 훅으로 useState
보다는 조금 더 강력한 기능을 지원하기 때문에 여러 state들이 함께 속해 있을 때, 여러 state가 같이 바뀌거나 서로 관련된 경우 일 때 등의 복잡한 상황일 때 종종 쓰인다.
그렇다고 더 많은 기능을 지원한다고 해서 매번 useReducer()
쓰는 것은 좋지가 않다. 이 훅을 사용 하기 위해 설정 하는 부분도 복잡할 뿐 더러 간단한 useState
훅 보다는 더 복잡하기 때문이다.
다른 state 를 기반으로 하는 state를 업데이트 하게 되는 경우에는 하나의
state
로 병합 해도 좋다.
useReducer()
사용하기const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
예시를 통해 해당 부분에 대해 좀 더 확인해보도록 하자.
해당 예시는 강의의 코드 중 하나로 로그인 사이트에서 email의 value (이메일 입력 값), valid (유효성) 를 병합하여 useReducer 로 사용한 부분이다.
import React, { useState, useReducer } from "react";
// 컴포넌트 내부에 쓰이는 어떤 데이터도 필요하지 않기 때문에 컴포넌트 함수 외부에 작성 가능
const emailReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.val, isValid: action.val.includes("@") };
}
if (action.type === "INPUT_BLUR") {
// state 는 늘 최신의 스냅샷
return { value: state.value, isValid: state.value.includes("@") };
}
return { value: "", isValid: false };
};
const Login = (props) => {
// const [enteredEmail, setEnteredEmail] = useState("");
// const [emailIsValid, setEmailIsValid] = useState();
...
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const emailChangeHandler = (event) => {
// 보통 객체로 전달 또한 해당 부분은 페이로드가 필요할테니 추가 (=val)
dispatchEmail({ type: "USER_INPUT", val: event.target.value });
...
};
...
const validateEmailHandler = () => {
dispatchEmail({ type: "INPUT_BLUR" });
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
...
</form>
</Card>
);
};
export default Login;
위에서도 언급했지만 useReducer 훅이 useState보다 강력한 기능을 지원한다고 해서 많이 쓰는 것은 좋지 않다.
그럼 언제 useState 를 쓰고 언제 useRedcuer 를 쓰면 될까?
너무 많은 일들을 처리해야 하는 경우 혹은 관련 state 스냅샷 들이 서로 독립적이고 같이 업데이트가 잘 안된다면 그 경우에는 useReducer 를 선호한다.
항상 useRedcuer 훅을 써야 하는 것은 아니다.
두 개의 서로 다른 값을 전환하기만 하는 단순한 state가 있는 경우 useReducer 를 쓰기엔 너무 과하다.
앱이 커지면 커질수록 컴포넌트가 많아지며 컴포넌트간에 데이터를 주고받기 위해 부모 컴포넌트에 데이터를 보내주고 받아와야하는데, 이 과정이 매우 복잡해져 불편해질 수 있다.
즉, 실제로 필요한 데이터를 부모를 통해서 받는 것이 아니라 해당 컴포넌트에서 바로 사용하길 원하는데 이를 해소하기 위해 컴포넌트 전체에서 사용 할 수 있는 즉 내부적인 state 저장소가 있다. 바로 React Context 이다.
context 를 담아두는 폴더는 보통 src 하위 폴더에 두며 케밥 케이스를 이용한다.
React.createContext()
를 호출한다.const AuthContext = React.createContext({
isLoggedIn: false
});
어떤 컴포넌트에서든 접근이 가능 하도록 공급 및 소비(연동, 리스닝) 가능하도록 리액트에게 Context가 있음을 알린다.
공급 한다 는 것은 JSX코드로 감싼다는 것을 의미한다. 감싸져야 리스닝 또한 가능하다.
// App.js (최상위 부모)
return (
<Fragment>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</Fragment>
);
예시의 코드는 작은 규모를 가지고 있지만.. 우리는 이 isLoggedIn
이 어디에서나 다 필요하다는 것을 확인 할 수 있다.
이 때, <Fragment>
아래에 위에 생성해준 AuthContext
로 감싸준다.
또한 ‘공급’ 한다는 것을 알려야 하기 때문에 최종적으로 <AuthContext.Provider>
로 감싸주는 형태가 된다.
→ Wrapper
컴포넌트의 기능을 하기 때문에 React.Fragment
의 기능을 할 수 있다. (해당 태그를 지워도 된다는 뜻)
이제 모든 컴포넌트는 해당 컨텍스트에 접근 할 수 있다.
Consumer
를 이용할 때 아래에 자식 부분이 있다.
{(ctx) => { return jsx code}}
여기서 ctx는 해당 Context의 data를 의미한다.
import React from 'react';
import classes from './Navigation.module.css';
import AuthContext from '../../store/auth-context';
const Navigation = (props) => {
return (
// 해당 컨텍스트 데이터가 필요한 곳에 호출
<AuthContext.Consumer>
{(ctx) => {
return //기존 JSX코드
<nav className={classes.nav}>
<ul>
// props.isLoggedIn -> ctx.isLoggedIn 에 접근 가능
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
...
</ul>
</nav>
}}
</AuthContext.Consumer>
);
};
export default Navigation;
💡 기본 값이 있으면 공급자는 필요 없다.
지금 같은 상황에 AuthContext 에는 기본 값을 가지고 있다.
const AuthContext = React.createContext({
isLoggedIn: false
});
이렇게 기본 값을 가지고 있다면 Provider 가 필요가 없다.
하지만 현재는 연습하는 단계이므로 Provider를 사용해보자.
<AuthContext.Provider
value={{
isLoggedIn: false,
}}
>
이렇게 작성하면 반응형이 되지 않으므로 상황에 맞추어 반응형으로 되도록 작성해준다.
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
}}
>
해당 예시에서는 isLoggedIn
이라는 state를 컴포넌트가 가지고 있기 때문에 이를 이용했다.
이렇게 되면 isLoggedIn
이 변경될 때 마다 리액트에 의해 업데이트 된다. 그렇게 되면 새로운 컨텍스트 객체는 모든 리스닝 컴포넌트로 전달이 된다.
→ 해당 컨텍스트를 소비하는 모든 컴포넌트에 전달 된다.
해당 방법은 함수를 만들어야하고 JSX코드를 반환해야하고 (consumer에서..) 코드가 별로 예쁘지 않다.
해당 방법에 대한 대안 방법이 있다. 바로 컨텍스트 훅을 이용 하는 것.
컨텍스트에게 사용하려는 컨텍스트를 가리키는 포인터 전달한다.
const ctx = useContext(AuthContext);
예시)
const Navigation = (props) => {
const ctx = useContext(AuthContext);
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={props.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
};
context는 단순 값 뿐 만 아니라 함수도 전달이 가능하다.
// App.js
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
onLogout: logoutHandler,
}}
>
);
이처럼 함수의 포인터를 가리키면 된다. 이렇게 작성해주면 AuthContext
를 리스닝하는 모든 컴포넌트는 logoutHandler
를 활용할 수 있다.
대부분의 경우에는 props를 사용하여 컴포넌트에 데이터를 전달한다.
props는 컴포넌트를 구성하고 그것들을 재사용할 수 있도록 하는 매커니즘이기 때문이다.
만약 많은 컴포넌트를 통해 전달하고자 하는 것이 있는 경우, 예를 들어 네비게이션처럼 매우 특정적인 일을 하는 컴포넌트나 항상 사용자를 로그아웃시키는 버튼 등의 여러 컴포넌트로 전달하는 경우에만 컨텍스트를 사용하는 게 더 나을 것이다.
컴포넌트 구성의 변경이 잦은 경우에는 적합하지 않다.
예를 들어 매초 또는 1초에 여러번 state가 변경되는 경우에 리액트 컨텍스트는 이에 대해 최적화 되어 있지 않다.
만약 앱 전체에 걸쳐 또는 컴포넌트 전체에 걸쳐 state가 자주 변경되는 경우에, 컨텍스트를 사용하고 싶은데 props는 적합하지 않을 떄 Redux를 사용한다.
useEffect()
훅에서 참조하는 모든 항목 들 중 컴포넌트 함수 외부에서 오는 데이터들은 의존성 배열에 추가해야 한다.useReducer()
혹은 useState()
에 의해 노출된 state업데이트 함수는 변경되지 않도록 리액트가 보장하기 때문에 의존성으로 추가 할 필요는 없다.