useEffect???
componentDidMount와 componentDidUpdate, componentWillUnmount 클래스 컴포넌트에서 제공하는
Lifecycle API를 useEffect를 사용하여 대신할 수 있다!!
render가 발생할 때마다 (componentDidMount ⇒ 초기, componentDidUpdate ⇒ 매번) effect가 실행된다.
여기서 두 번째 파라미터인 inputs를 통해 특정 상태가 update 되었을 때만 effect가 실행되게 설정할 수 있다.
만약 useEffect의 inputs에 빈 배열을 넘겨주면 최초(componentsDidMount)에만 실행된다.
componentWillUnmount를 실행하기 위해선 return을 사용하여 기능 수행이 가능하다.
useEffect(() => {
window.addEventListener("mousemove", logMousePosition);
return () => {
window.removeEventListener("mousemove", logMousePosition);
};
}, []);
effect 함수에 return 값이 있는 경우 hook의 cleanup 함수로 인식하고
다음 effect가 실행되기 전 실행해준다. 여기서 componentWillUnmount는 컴포넌트가 사라지기 전
한 번만 실행했다면, cleanup 함수는 새로운 effect 실행 전에 매번 호출된다.
만약 빈 배열을 두 번째 파라미터로 지정한다면, unmount 될 때 한번만 실행된다.
주로, 마운트 시 하는 작업들은 다음과 같다.
- props로 받은 값을 컴포넌트의 로컬 상태로 설정
- 외부 API 요청(REST API 등등)
- 라이브러리 사용
- setInterval을 통한 반복 작업 or setTimeout을 통한 작업 예약
그리고 언마운트 시 하는 작업들은 다음과 같다.
- setInterval, setTimeout을 사용하여 등록한 작업들 clear 하기
- 라이브러리 인스턴스 제거
파라미터를 좀 더 깊게 알아보기
두 번째 파라미터인 배열에 따라 어떻게 수행되는지 좀 더 깊게 알아보자
파라미터에 특정 값 넣기
파라미터에 특정 값을 넣어보면, 컴포넌트가 처음 마운트 될 때마다 호출되고, 지정한 값이 바뀔 때
호출된다. 그리고 언마운트 시에도 호출되고, 값이 바뀌기 직전에도 호출된다.
import React, { useEffect } from 'react';
function User({ user, onRemove, onToggle }) {
useEffect(() => {
console.log('user 값이 설정됨');
console.log(user);
return () => {
console.log('user 가 바뀌기 전..');
console.log(user);
};
}, [user]);
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map(user => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default UserList;
useEffect 안에서 사용하는 state나 props가 있다면, useEffect의 파라미터에 넣어주는 게 규칙이다.
만약 useEffect 안에 사용하는 state나 props를 파라미터 안에 넣지 않게 된다면 useEffect에 등록한
함수가 실행될 때 최신 state나 props를 가리키지 않게 된다.
파라미터 생략하기
파라미터를 생략하면, 컴포넌트가 리 렌더링 될 때마다 호출된다!
import React, { useEffect } from 'react';
function User({ user, onRemove, onToggle }) {
useEffect(() => {
console.log(user);
});
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map(user => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default UserList;
※ 참고로고로 리액트 컴포넌트는 기본적으로 부모 컴포넌트가 리 렌더링 되면 자식 컴포넌트 또한
리렌더링 된다. 바뀐 내용이 없다고 하더라도...
파라미터 의존성
state, props
위 useEffect에서 state와 props를 사용한다면 파라미터에 적어주는 것이 규칙이라고 했다.
하지만
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
위 코드에선 count를 초당 1씩 증가시키게 하기 위해서 count를 파라미터에 넣는다.
하지만 여기서의 count의 역할은 이전 count 값을 나타내는 것이다.
[count]를 통해 count를 사용할 때의 규칙을 지켰다. 근데레레레 그로 인해 setCount로 count값이
변경되면 또다시 useEffect가 실행되어버린다?!!
그래서 값이 바뀌면 return을 통해 기존 Interval을 없애준다.
하지만 이 말은 즉, count 값이 바뀔 때마다 인터벌은 해체되고 다시 설정된다.
여기서 파라미터를 없앴지만 구현하는 방법은 있을까??
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Wow! 해결해따! 위와 같이 setCount처럼 useState로 만든 상태 변화 함수는 이전 값을 파라미터로
받아올 수 있다.
하지만 만약 1씩 증가는 것이 아닌 step이라는 상태를 통해 원하는 값만큼 증가시키면 어떨까??
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
위 count를 파라미터로 주었을 때와 같은 상황이 발생해버린다.
step이 변하면 다시 인터벌을 시작하게 된다. 의존성으로 인해...
물론 이것을 원한다면 상관없지만 step이 바뀐다고 인터벌 시계가 초기화되지 않는 것을 원한다면?
즉, 파라미터에서 step을 제거하고 싶다면 어떻게 해야 할까??
이때, 두 state 모두 useReducer로 교체해야 한다.
리듀서는 컴포넌트에서 일어나는 "액션"의 표현과 그 반응으로 state가 어떻게 업데이트되어야 할지
분리한다.
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // setCount(c => c + step) 대신에
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
리액트는 컴포넌트가 유지되는 한 dispatch 함수가 항상 같다는 것을 보장해준다.
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
리덕스는 위와 같이 스토어와 액션을 선언하고 사용하는 컴포넌트에서 액션을 디스 패치하여
useEffect에서 step 상태로부터 분리시켰다.
위와 같은 과정으로 useEffect가 이전 상태를 기준으로 state를 설정할 필요가 있을 때 의존성을
어떻게 제거해야 하는지 공부했다.
But 다음 상태를 계산하는데, props가 필요하다면??
step이 <Count step={1} /> 이렇게 props로 주어 props.step이라면??
의존성 파라미터에 props.step을 설정하는 것을 피할 수 없을 것이다.....
※사실 피하려면 피할 순 있다...!
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error();
}
}
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
리듀서를 컴포넌트 안에 정의하여 props를 읽도록 하면 된다.
하지만 이 방식은 최적화를 무효화할 수 있기 때문에 난발해선 안된다.
반드시 필요하면 이렇게 구현은 가능하다!
'React > 기능' 카테고리의 다른 글
[React] useLayoutEffect (0) | 2022.02.08 |
---|---|
[React] useRef (0) | 2022.02.08 |
[React] useState (0) | 2022.02.08 |
[React] Route 부가적인 부분 (history, withRouter..) (0) | 2022.02.08 |
[React] Route (0) | 2022.02.08 |