TIL
React.Fragment, React Portals, useeffect, useRef, useReducer, contextAPI(useContext)를 학습했습니다. 요번 섹션은 꽤나 까다롭네요. 비교적 난이도가 올라간 듯한 느낌을 받았습니다.
div tag soup?
div tag soup이란 아래와 같은 형태의 못생긴 형태를 뜻합니다.
<div>
<div>
<div>
<p>hellow world!</p>
</div>
</div>
</div>
JSX는 하나의 태그만 리턴해야만 하고 주로 div로 감싸 리턴합니다. 그래서 위와 같이 불필요한 div 태그들이 생성됩니다. 그렇다면 어떻게 해야 할까요?
const Wrapper = props => {
return props.children;
};
export default Wrapper;
Wrapper 컴포넌트를 만들고 div 태그 대신 Wrapper로 감싸면 불필요한 div가 사라집니다. 이것을 직접 Wrapper로 선언하지 않아도 리엑트 내에서 <React.Fragment>
나 <>
의 형태로 제공합니다.
// 사용 예 1
return <React.Fragment>...</React.Fragment>;
// 사용 예 2
return <>...</>;
React Portals
리엑트 포털을 이용하면 렌더링 된 html을 다른 곳으로 옮길 수 있습니다.
예를 들어, 모달창이 평소에는 숨겨지고 잘못된 값을 입력할 때만 렌더링 된다고 했을 때, 웹 접근성과 의미론적으로 좋지 못한 결과를 낳습니다. 그래서 포털을 이용하여 body 태그의 바로 뒤에 넣는게 일반적입니다. 사용법은 아래와 같습니다.
// public/index.html
<body>
<div id="backdrop-root"></div>
<div id="overlay-root"></div>
<div id="root"></div>
</body>
root 태그 위에 두개의 div를 선언합니다.
// ErrorModal.js
// ReactDOM을 import 합니다.
import ReactDOM from 'react-dom';
// 원래 한군데에 있었던 jsx를 분리합니다.
const Backdrop = props => {
return <div className={classes.backdrop} onClick={props.onConfirm} />;
};
const ModalOverlay = props => {
return (
<Card className={classes.modal}>
<header className={classes.header}>
<h2>{props.title}</h2>
</header>
<div className={classes.content}>
<p>{props.message}</p>
</div>
<footer className={classes.actions}>
<Button onClick={props.onConfirm}>Okay</Button>
</footer>
</Card>
);
};
const ErrorModal = props => {
return (
// 메인 변수에서 포털을 연결해줍니다.
<React.Fragment>
{ReactDOM.createPortal(
<Backdrop onConfirm={props.onConfirm} />,
document.getElementById('backdrop-root'),
)}
{ReactDOM.createPortal(
<ModalOverlay
title={props.title}
message={props.message}
onConfirm={props.onConfirm}
/>,
document.getElementById('overlay-root'),
)}
</React.Fragment>
);
};
export default ErrorModal;
이러한 작업을 거치면 backdrop-root와 overlay-root 태그에 알맞은 값들이 들어갑니다.
Ref
props를 상호작용 할 때 사용합니다. 주로 애니메이션을 실행시킬때나 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때 사용합니다. 아래는 ref를 적용하기 전, state에 의해 props를 제어하는 방법입니다.
// addUser라는 값을 입력하는 input 값이 있는 컴포넌트 입니다.
const AddUser = props => {
const [enteredUsername, setEnteredUsername] = useState('');
const addUserHandler = event => {
event.preventDefault();
props.onAddUser(enteredUsername);
setEnteredUsername('');
};
const usernameChangeHandler = event => {
setEnteredUsername(event.target.value);
};
return (
<input
id="username"
type="text"
value={enteredUsername}
onChange={usernameChangeHandler}
/>
);
};
export default AddUser;
ref를 적용해보겠습니다.
import React, { useState, useRef } from 'react';
const AddUser = props => {
const nameInputRef = useRef(); // ref 선언
const addUserHandler = event => {
// ref에 접근
const enteredName = nameInputRef.current.value;
props.onAddUser(enteredName);
nameInputRef.current.value = '';
};
return (
<input id="username" type="text" ref={nameInputRef} />
// ref를 바인딩
);
};
export default AddUser;
주의할 점은 ref는 제어되지 않는 값이고 단지 참조하는 값
이라는 점입니다.
effect or side effects
useEffect
리엑트는 user와 interaction 하기위해 생겨났습니다. 그러나 그것 말고도 해야되는 일들이 있습니다. 예를 들어, HTTP의 요청을 처리한다던지, 백앤드 서버에 접근한다던지 하는 것들 입니다. 이런것은 리엑트나 화면에 그리는 일과는 관련이 없는 일입니다. 이런 일들은 일반적인 컴포넌트 밖에서 일어나야 합니다. 왜냐하면 state는 컴포넌트를 렌더링을 하기 때문에 request 요청이 컴포넌트 안으로 들어가면 무한루프나 버그가 생기게 될 수 있습니다. 이럴때 사용하는것이 useEffect입니다.
useEffect는 특정 state나 props를 바꿀 때 사용
합니다. 만약 의존성이 없다면 컴포넌트 랜더링 된 후 최초 한번만 실행됩니다.
Clean up function
useEffect(() => {
console.log('EFFECT RUNNING');
return () => {
console.log('EFFECT CLEANUP');
};
}, [enteredPassword]);
return 문이 clean up function 이라고 부릅니다. 이것은 처음 최초 실행되기 전까지는 실행이 되지 않지만, effect가 실행되면 effect의 내용이 시작되기전에 먼저 실행
됩니다.
useReducer
복잡한 state를 관리
할 때 유용합니다. 예를 들어, 다른 state를 기반으로 stete를 업데이트하는 경우, 하나의 state로 병합하는게 좋습니다. 물론 state로 처리를 해줘도 되고, useReducer를 사용해도 됩니다. 아래는 reducer 적용 예 입니다.
import React, { useState, useEffect, useReducer } from 'react';
// 리듀서 생성
const emailReducer = (state, action) => {
if (action.type === 'USER_INPUT') {
return { value: action.val, isValid: action.val.includes('@') };
}
if (action.type === 'USER_BLUR') {
return { value: state.value, isValid: state.value.includes('@') };
}
return { value: '', isValid: false };
};
const Login = props => {
// 리듀서 선언
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: '',
isValid: null,
});
const emailChangeHandler = event => {
dispatchEmail({ type: 'USER_INPUT', val: event.target.value });
};
const validateEmailHandler = () => {
dispatchEmail({ type: 'USER_BLUR' });
};
return (
<input
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
);
};
export default Login;
contextAPI
props를 a에서 d로 넘겨줄 때 a에서 b를 거쳐 c를 지나서 d로 넘겨줘야 하는 경우가 있습니다.(a→b→c→d) 이런 경우, d만 prop이 필요해도 b와 c에도 선언을 해야합니다. 이런 경우를 props chain이라고 부릅니다. contextAPI는 prop chain을 단점을 해소하기 위해 사용
합니다. 아래 코드는 contextAPI를 적용한 예입니다.
//store/auth-context.js
import React from 'react';
const AuthContext = React.createContext({
isLoggedIn: false,
});
export default AuthContext;
AuthContext에 isLoggedIn이라는 값을 넣어줍니다. context를 사용할 때는 provider(제공자)와 comsumer(소비자)가 있습니다. provider와 consumer를 선언 합니다.
// App.js
return (
<AuthContext.Provider value={{ isLoggedIn }}>
<MainHeader onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
// Navigation.js
import React from 'react';
import AuthContext from '../store/auth-context';
const Navigation = props => {
return (
<AuthContext.Consumer>
{ctx => {
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
</ul>
</nav>
);
}}
</AuthContext.Consumer>
);
};
export default Navigation;
Navigation안에 리턴 한것을 Cunsumer로 또다시 리턴.. 복잡해보이죠? 우아하게 useContext를 이용하여 바꿔봅시다.
UseContext
import React, { useContext } from 'react';
import AuthContext from '../store/auth-context';
import classes from './Navigation.module.css';
const Navigation = props => {
const ctx = useContext(AuthContext);
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
</ul>
</nav>
);
};
export default Navigation;
useContext를 이용하여 한결 깔끔해졌습니다!
실제 현업에서는 아래와 같이 사용한다고 합니다.
import React, { useState, useEffect } from 'react';
const AuthContext = React.createContext({
isLoggedIn: false,
onLogout: () => {},
onLogin: (email, password) => {},
});
export const AuthContextProvider = props => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const storedLoggedInInformation = localStorage.getItem('isLoggedIn');
if (storedLoggedInInformation === '1') {
setIsLoggedIn(true);
}
}, []);
const logoutHandler = () => {
localStorage.removeItem('isLoggedIn');
setIsLoggedIn(false);
};
const loginHandler = () => {
localStorage.setItem('isLoggedIn', '1');
setIsLoggedIn(true);
};
return (
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
onLogout: logoutHandler,
onLogin: loginHandler,
}}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthContextProvider } from './components/store/auth-context';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthContextProvider>
<App />
</AuthContextProvider>,
);
// App.js
import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
import React, { useContext } from 'react';
import AuthContext from './components/store/auth-context';
function App() {
const ctx = useContext(AuthContext);
return (
<React.Fragment>
<MainHeader />
<main>
{!ctx.isLoggedIn && <Login />}
{ctx.isLoggedIn && <Home />}
</main>
</React.Fragment>
);
}
export default App;
솔직히 여기까지는 이해를 못했습니다. AuthContext안에서 모든 것을 제어하고 있는 코드인 건 알겠는데… 보고는 얼추 ‘아 그렇구나’ 하겠는데 혼자 짜보세요 하면 못할것 같습니다. 다음 프로젝트 후에 다시 한번 봐야 겠습니다.🥲
contextAPI의 한계점
변경이 잦은 경우(1초에 여러번)에는 context가 적합하지 않습니다. (그럴때는 리덕스를 사용합니다) 또한 props을 모두 대체할 수 있는건 아닙니다. 따라서 상황에 따라 사용해야 합니다.
마지막으로 forwardRef에 관한 예시를 강사님이 보여주셨는데 역시나 이해가 안되더군요.. 애초에 처음 접하는데 완벽히 다 알거라는 기대는 하지 않았습니다. 그래서 다음에 다시 복습할 예정입니다.