useEffect
처음 화면에 나타나게 되거나 사라질때, 업데이트될때 작업을 진행하게 할 수 있다.
간단한 예를 먼저한번 보자
//UserList.js
import React, {useEffect} from 'react';
function User({user, onRemove,onToggle}) {
const {username,email,id, active} = user;
useEffect( () =>{ //mount
console.log('컴포넌트가 화면에 나타남');
//props -> state
//REST API
return () =>{ //unmount
console.log('컴포넌트가 화면에서 사라짐');
}
//clearInternval,clearTimeout
}, [])
위 코드는 이전에 만들었던 예제에 useEffect를 보기위해 간단히 추가해본 코드이다. 전 글을 보면 코드 전체를 볼 수 있다. 위처럼 놔두면 mount 즉, 컴포넌트가 시작될때 콘솔창에 로그를 띄우고, 사라질때도 화면에 나타남을 알 수 있다.
등록과 삭제를 할때마다 콘솔에 추가가 되고, 시작할 때는 3개가 동작하도록 되어있기에 콘솔창에 3번 반복되고 시작한다.
//UserList.js
import React, {useEffect} from 'react';
function User({user, onRemove,onToggle}) {
const {username,email,id, active} = user;
useEffect(() =>{
console.log('user값이 바뀐 후')
console.log(user);
return () => { //컴포넌트가 사라질대 실행
console.log('user값이 바뀌기 전')
console.log(user);
}
}, [user]); //값을 쓰고싶으면 deps(옆에 [user])값을 넣어주어야 최신값을 불러올 수 있다.
이때 deps값을 꼭 넣어줘야 최신화가 잘 된다는것을 기억하자.
useMemo
성능을 최적화할 때 사용한다.
//App.js
import React, {useRef,useState} from 'react';
import './App.css';
import CreateUser from './CreateUser';
import UserList from './UserList';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
..........
const count = countActiveUsers(users);
return (
<>
< CreateUser
username = { username }
email = { email }
onChange = { onChange}
onCreate = { onCreate }
/>
<UserList users = {users} onRemove ={onRemove} onToggle={onToggle}/>
<div>활성 사용자 수 : {count}</div>
</>
);
}
export default App;
위와같이 코드를 추가해서 초록불이 들어와 있는경우만 세주는 기능을 추가해 보았다.
이 경우, 위처럼 a를 작성할때마다 불필요하게 함수를 계속 호출하고 있음을 알 수 있다.
이때 사용할 수 있는게 이 useMemo이다.
const count = useMemo(()=> countActiveUsers(users), [users]); //deps
위 코드에서 count부분을 위처럼 바꾸면 된다. 첫 파라메터로는 함수가 들어와야 한다.
즉, [users] 배열이 바뀔때마다 활성화를 하도록 설정이 된 상태이다.
useCallback
함수의 재사용이 가능하게 된다.
위에 만들었던 onChange와 같은 함수들이 랜더링 될때마다 새로 생성되는데, 재사용이 되도록 하게끔 하는 것이다.
const onChange = (e) =>{
const {name,value} = e.target;
setInputs({
...inputs,
[name] : value // email이 들어오면 email이 바뀌게 된다
});
즉 위코드를
const onChange = useCallback((e) =>{
const {name,value} = e.target;
setInputs({
...inputs,
[name] : value // email이 들어오면 email이 바뀌게 된다
});
}, [inputs]);
아래처럼 useCallback으로 묶고, 그안에 바뀔때 사용되는 값을을 deps로 넣어주면 된다. 이때 deps를 넣어주지 않으면 함수가 최신화된 배열의 값이 아닌, 만들어졌을때 배열의 값을 가리키게 된다.
즉, 위 작업을 통해 함수들은 특정값이 바뀌었을때만 실행되게 된다.
그렇다면 또다른 문제를 하나 더 보도록 하자. 지금 짠 프로그램은 문제가 하나있다.
위 React Dev Tools를 이용하여 프로그램을 분석해보자. 설치한 후에 개발자옵션에서 console툴부분에 리액트 관련 항목을 볼 수 있게 해주는 도구이다.
요소를 볼때, 내가 알파벳을 칠대마다 네모난 박스가 생기는것을 볼 수 있다. 즉, 내가 제출을 누를때마다 값이 바뀌는 것이아닌, 값을 입력하거나 삭제할때도 위 기능이 작동하는 것이다.
이는 React.memo를 사용하여 고칠 수 있다.
React.memo
//UserList.js
import React, {useEffect} from 'react';
const User = React.memo(function User({user, onRemove,onToggle}) {
const {username,email,id, active} = user;
useEffect(() =>{
console.log('user값이 바뀐 후')
console.log(user);
return () => { //컴포넌트가 사라질대 실행
console.log('user값이 바뀌기 전')
console.log(user);
}
}, [user]); //값을 쓰고싶으면 deps(옆에 [user])값을 넣어주어야 최신값을 불러올 수 있다.
return (
<div>
<b style ={{
color : active ? 'green' : 'black',
cursor : 'pointer'
}}
onClick={() => onToggle(id)}
>
{username}
</b><span>({email})</span>
<button onClick={() =>onRemove(id)}>삭제</button> {/* onRemove를 id를 파라미터로 실행 */}
</div> /*꼭 함수로 호출해줘야 전체가 사라지지 않음*/
)
});
function UserList({users, onRemove, onToggle}) {
return (
<div>
{
users.map(
(user,index) =>(
<User user={user}
key = {user.id}
onRemove={onRemove}
onToggle = {onToggle}
/>)
//(user,index) =>( <User user={user} key = {index}/>)
)
}
</div>
);
}
export default React.memo(UserList)
우선 export를 통하여 내보내는 곳을 React.memo로 감싸주고, User함수도 감싸준다.
이러면 props가 바뀔때마다 랜더링이 되게 된다.
하지만 이경우, 입력을 할때마다 아이디(별명)부분은 리랜더링이 되지 않지만, 다른 버튼을 눌렀을때는 여전히 리랜더링이 되는걸 알 수 있는데,
const onRemove = useCallback(id =>{
setUsers(users.filter(user => user.id !== id));
},[users]);
//App.js
이는 위에서 볼 수 있듯이, 함수가 [users]에서 값을 직접 받아오기에 랜더링이 되는 것이다.
const onRemove = useCallback(id =>{
setUsers(users => users.filter(user => user.id !== id));
},[]);
이를 해결하기위해 함수형으로 호출하게 하면 된다.
최적화가 잘 된 모습을 확인할 수 있다.
export default React.memo(UserList, (preProps,nextProps) => nextProps.users === preProps.users);
위 코드를 확인해서 Props들이 바뀌지 않으면 리랜더링을 방지할수도 있다.
useReducer
useState와 유사하게 변하는 값을 관리할 수 있다.
useState가 값을 바꿀때 직접 지정해 준다면, useReducer는 action이라는 객체를 이용하여 변하게 한다. 이를 통해 상태 업데이트 로직을 컴포넌트 밖으로 분리하게 할 수 있다.
reducer : 상태를 업데이트하는 함수
function reducer(state,action) {
switch ( action.type) {
case 'INCREMENT':
return state +1;
case 'DECREMENT':
return state -1;
default :
return state;
}
}
reducer는 위와 같은 형태를 지닌다.
action.type를 읽은 후에 그 상태에 따른 값을 관리를 하게 한다.
이를 이용한 useReducer사용은 아래와 같다.
const [number,dispatch] = useReducer(reducer,0);
number는 현재상태, dispatch는 코드를 발생시킨다고 생각하면 될것 같다.
import react, {useState} from "react";
function Counter() {
const [number,setNumber] = useState(0);
const onIncrease = () =>{
setNumber(number +1)
}
const onDecrease = () =>{
setNumber(number -1)
}
return (
<div>
<h1>{number}</h1>
<button onClick ={onIncrease}>+1</button>
{/* onIncrease()로 쓰면 안된다*/}
<button onClick ={onDecrease}>-1</button>
</div>
)
}
export default Counter;
자, 이전에 사용했던 Counter예제가 있는데 이를 useReducer를 사용해서 바꿔볼 것이다.
import react, {useReducer} from "react";
function reducer(state,action) {
switch (action.type) {
case 'INCREMENT':
return state +1;
case 'DECREMENT':
return state -1;
default :
throw new Error('Unhandled action')
}
}
function Counter() {
const [number, dispatch] = useReducer(reducer,0);
const onIncrease = () =>{
dispatch({
type : 'INCREMENT'
})
}
const onDecrease = () =>{
dispatch({
type : 'DECREMENT'
})
}
return (
<div>
<h1>{number}</h1>
<button onClick ={onIncrease}>+1</button>
{/* onIncrease()로 쓰면 안된다*/}
<button onClick ={onDecrease}>-1</button>
</div>
)
}
export default Counter;
위에서 사용했던 useReducer을 사용하여 똑같은 counter 예제를 만들어 보았다.
이번에는 useState를 사용해서 이전에 만들었던 이름,별명을 등록하는 사이트를 바꿔보겠다.
코드를 확인하려면 밑에 더보기를 클릭하여 보자.
//App.js
import React, {useRef,useReducer,useMemo,useCallback} from 'react';
import './App.css';
import CreateUser from './CreateUser';
import UserList from './UserList';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
inputs : { //두가지 정보를 관리하기 위해 useState사용
username : '',
email:'',
},
users : [
{
id : 1,
username : 'mingyu',
email : 'mingyu@naver.com',
active : true //항목 수정을 위해 추가
},
{
id : 2,
username : 'mingyu2',
email : 'mingyu2@naver.com',
active : false
},
{
id : 3,
username : 'mingyu3',
email : 'mingyu3@naver.com',
active : false
}
]
}
function reducer(state,action) {
switch (action.type){
case 'CHANGE_INPUT':
return {
...state,
inputs : {
...state.inputs,
[action.name] : action.value
}
};
case 'CREATE_USER':
return {
inputs : initialState.inputs, //inputs를 날리는 작업
users : state.users.concat(action.user) //더하는거
}
case 'TOGGLE_USER':
return{
...state,
users : state.users.map(user =>
user.id === action.id
? { ...user,active : !user.active}
: user
)
}
case 'REMOVE_USER':
return{
...state,
users : state.users.filter(user => user.id !== action.id)
}
default:
throw new Error('unhandled action');
}
}
function App() {
const [state,dispatch] = useReducer(reducer,initialState);
const nextId = useRef(4); // 3까지는 적어두었기 때문
const { users } = state;
const { username,email } = state.inputs;
const onChange = useCallback(e => {
const { name , value } = e.target;
dispatch({
type : 'CHANGE_INPUT',
name,
value
})
}, [])
const onCreate = useCallback(()=> {
dispatch({
type : 'CREATE_USER',
user : {
id : nextId.current,
username,
email,
}
});
nextId.current +=1
}, [username,email])
const onToggle = useCallback(id => {
dispatch({
type : 'TOGGLE_USER',
id
});
},[]);
const onRemove = useCallback(id =>{
dispatch({
type : 'REMOVE_USER',
id
});
},[]);
const count = useMemo(()=> countActiveUsers(users), [users])
return (
<>
< CreateUser
username = {username}
email = {email}
onChange = {onChange}
onCreate={onCreate}
/>
<UserList users = {users} onToggle={onToggle} onRemove={onRemove}/>
<div>활성 사용자 수 : {count}</div>
</>
);
}
export default App;
코드가 상당히 많이 바뀌었는데 동작원리는 같으니 위에서부터 차근차근 읽어보면 좋을 것 같다.
그렇다면 언제 useState를 쓰고 언제 useReduce를 사용하는것이 좋을까?
값이 한개고 명확하다면 useState를 쓰는것이 좋지만, 값이 여러개고 조건등이 붙는다면 useReduce를 사용하는것이 좋다.
Custom Hook 만들기
const onChange = useCallback(e => {
const { name , value } = e.target;
setInputs({...inputs, [name] : value });
}
예를들어 위같은 컴포넌트는 자주 사용되게 된다.
이런 input상태를 관리하는 custom Hook을만들어보도록 하겠다.
//useInputs.js
import {useState,useCallback} from 'react';
function useInputs(initialForm) {
const [form,setForm] = useState(initialForm);
const onChange = useCallback(e => {
const {name,value} = e.target;
setForm(form => ({...form, [name] : value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form,onChange,reset];
}
export default useInputs;
위와같은 js파일을 새로 만들어보자. 그 이후, App.js에서 Change부분을 삭제하고 해당 Hook에서 사용하는 방법으로 바꾸어도 작동은 동일할 것이다.
코드가 긴 관계로 접어서 넣어두겠다.
//App.js
import React, {useRef,useReducer,useMemo,useCallback} from 'react';
import './App.css';
import CreateUser from './CreateUser';
import UserList from './UserList';
import useInputs from './useInputs';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
users : [
{
id : 1,
username : 'mingyu',
email : 'mingyu@naver.com',
active : true //항목 수정을 위해 추가
},
{
id : 2,
username : 'mingyu2',
email : 'mingyu2@naver.com',
active : false
},
{
id : 3,
username : 'mingyu3',
email : 'mingyu3@naver.com',
active : false
}
]
}
function reducer(state,action) {
switch (action.type){
case 'CREATE_USER':
return {
inputs : initialState.inputs, //inputs를 날리는 작업
users : state.users.concat(action.user) //더하는거
}
case 'TOGGLE_USER':
return{
...state,
users : state.users.map(user =>
user.id === action.id
? { ...user,active : !user.active}
: user
)
}
case 'REMOVE_USER':
return{
...state,
users : state.users.filter(user => user.id !== action.id)
}
default:
throw new Error('unhandled action');
}
}
function App() {
const [state,dispatch] = useReducer(reducer,initialState);
const [form,onChange,reset] = useInputs({
username : '',
email : '',
});
const { username,email} = form; //form에서 추출
const nextId = useRef(4); // 3까지는 적어두었기 때문
const { users } = state;
const onCreate = useCallback(()=> {
dispatch({
type : 'CREATE_USER',
user : {
id : nextId.current,
username,
email,
}
});
nextId.current +=1
reset();
}, [username,email,reset]) //reset도 추가해줘야함
const onToggle = useCallback(id => {
dispatch({
type : 'TOGGLE_USER',
id
});
},[]);
const onRemove = useCallback(id =>{
dispatch({
type : 'REMOVE_USER',
id
});
},[]);
const count = useMemo(()=> countActiveUsers(users), [users])
return (
<>
< CreateUser
username = {username}
email = {email}
onChange = {onChange}
onCreate={onCreate}
/>
<UserList users = {users} onToggle={onToggle} onRemove={onRemove}/>
<div>활성 사용자 수 : {count}</div>
</>
);
}
export default App;
물론 useState가아닌 useReducer로도 구현할 수 있다.
//useInputs.js
import {useReducer,useCallback} from 'react';
function reducer(state,action) {
switch (action.type){
case 'CHANGE':
return {
...state,
[action.name] : action.value
}
case 'RESET' :
return {
...state,
action
}
}
}
function useInputs(initialForm) {
const [state,dispatch] = useReducer(reducer,initialForm);
const onChange = useCallback(e => {
const {name,value} = e.target;
dispatch({
type : 'CHANGE',
name,
value
})
}, []);
const reset = useCallback(() => {
dispatch({
type : 'RESET',
initialForm
})
},[]);
return [state,onChange,reset];
}
export default useInputs;
'FrontEnd > React' 카테고리의 다른 글
15_리액트_유용한 tool (0) | 2021.12.24 |
---|---|
13_리액트_Context API,immer (0) | 2021.12.24 |
11_리액트_배열 랜더링 (0) | 2021.12.22 |
10_리액트_input상태,useRef (0) | 2021.12.22 |
10_컴포넌트_JSX,props,useState (0) | 2021.12.20 |