ReScript in Korean

useEffect

원문

Effect 훅은 함수 컴포넌트에서 사이드 이펙트를 다룰 수 있게 해줍니다.

이펙트는 무엇인가요?

이펙트(Side-Effect)의 일반적인 예는 데이터 페칭이나 서브스크립션 설정 그리고 리액트 컴포넌트에서 DOM을 수동으로 변경하는 것입니다.

리액트 컴포넌트에는 두가지 종류의 사이드 이펙트가 있습니다. 클린업 과정이 필요하지 않은 것과, 클린업이 필요한 것. 나중에 예제를 보며 차이점을 살펴보겠지만 우선 인터페이스가 어떻게 생겼는지 살펴봅시다.

기본 사용법

/* 렌더링이 완료될 때마다 실행 */
React.useEffect(() => {
/* 이펙트 실행 */
None /* 또는 Some(() => {}) */
})
/* 컴포넌트가 마운트 된 후 단 한번만 실행 */
React.useEffect0(() => {
/* 이펙트 실행 */
None // 또는 Some(() => {})
})
/* `prop1`이 변경될 때마다 실행 */
React.useEffect1(() => {
/* prop1에 값에 기반한 이펙트 실행 */
None
}, [prop1])
/* `prop1` 또는 `prop2` 가 변경될 때마다 실행 */
React.useEffect2(() => {
/* prop1 / prop2 값을 기반으로 이펙트 실행 */
None
}, (prop1, prop2))
React.useEffect3(() => {
None
}, (prop1, prop2, prop3));
/* 의존성개 따라 useEffect4에서 useEffect7 까지 사용할 수 있습니다. */
/* useEffect3처럼 튜플로 인자를 받습니다. */

React.useEffect는 사이드 이펙트성이 강한 명령형 코드를 포함하고 option<unit => unit> 를 값으로 반환합니다. 반환하는 값은 잠재적인 클린업 함수입니다.

useEffect 호출은 추가적인 의존성 배열을 인자로 받습니다. (React.useEffect1 / React.useEffect2...7 함수를 보세요). effect 함수는 제공되는 의존성중 하나가 변경될 때마다 실행됩니다. 이것이 왜 유용한지 자세한 부분은 여기서 다룰게요.

참고 의존성 리스트에 React.useEffect1배열을 받고 useEffect2와 다른 것들(useEffect3~7)은 왜 튜플 (Ex (prop1, prop2)) 을 받는지 궁금하실 수 있습니다. 그 이유는 튜플은 여러 값에 서로 다른 타입이 가능하기 때문입니다. 반면에 배열은 오직 같은 타입의 값믄 받습니다. useEffect2는 doing React.useEffect1(fn, [1, 2])로 대체될 수 있지만 React.useEffect1(fn, [1, "two"]) 이렇게 서로 다른 타입을 요소는 허용되지 않습니다.

React.useEffect는 렌더링이 완료될 때마다 실행되는 반면 React.useEffect0는 첫번째 렌더링 이후에만 실행됩니다. (컴포넌트가 마운트 됐을 때)

예제

클린업 없는 이펙트

때때로, 우리는 리액트가 DOM을 업데이트한 이후 어떤 코드가 실행되길 원합니다. 예를 들어 네트워크 요청이나 수동 DOM 변형, 그리고 로깅 작업은 클린업이 필요하지 않은 이펙트의 몇가지 예시입니다. 우리는 이런 이펙트들을 즉시 실행하고 그 이후를 기억하지 않아도 됩니다.

다음 예시처럼, document.title를 매 렌더마다 업데이트 하는 카운터 컴포넌트를 작성해봅시다.

/* Counter.re */
module Document = {
type t;
@bs.val external document: t = "document";
@bs.set external setTitle: (t, string) => unit = "title"
}
@react.component
let make = () => {
let (count, setCount) = React.useState(_ => 0);
React.useEffect(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, );
let onClick = (_evt) => {
setCount(prev => prev + 1)
};
let msg = "You clicked" ++ Belt.Int.toString(count) ++ "times"
<div>
<p>{React.string(msg)}</p>
<button onClick> {React.string("Click me")} </button>
</div>
}

이펙트에 count를 의존적으로 만들려면 다음처럼 useEffect를 사용합니다.

React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, [count]);

이제 렌더링 될 때마다 이펙트를 실행하지 않고 count가 이전 렌더링과 값이 다를 때문 이펙트가 실행됩니다.

클린업과 이펙트

우리는 앞에서 클린업이 필요없는 사이드이펙트를 표현하는 방법을 알아봤습니다. 그러나 어떤 이펙트들은 클린업이 필요합니다. 예를 들어, 우리는 외부 데이터 소스를 구독하고 싶을 수 있습니다. 이런 경우, 메모리 누수를 발생시키지 않기 위해 꼭 클린업이 필요합니다!

정상적으로 구독하고 나중에 구독을 우아하게 취소하는 구독 API 예제를 보겠습니다.

/* FriendStatus.res */
module ChatAPI = {
/* Imaginary globally available ChatAPI for demo purposes */
type status = { isOnline: bool };
@bs.val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus";
@bs.val external unsubscribeFromFriendStatus: (string, status => unit) => unit = "unsubscribeFromFriendStatus";
}
type state = Offline | Loading | Online;
@react.component
let make = (~friendId: string) => {
let (state, setState) = React.useState(_ => Offline)
React.useEffect(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
})
let text = switch(state) {
| Offline => friendId ++ " is offline"
| Online => friendId ++ " is online"
| Loading => "loading..."
}
<div>
{React.string(text)}
</div>
}

이펙트 의존성

몇몇 경우에서 클린업 또는 모든 렌더 이후 적용되는 이펙트 때문에 성능 문제가 생길 수 있습니다. useEffect가 하는 작업을 확인하기 위해 구체적인 예를 보겠습니다.

/* from a previous example above */
React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None;
}, [count]);

여기 우리는 [count]useEffect1의 의존성으로 넣었습니다. 무엇을 의미하는 것일까요? 만약 count의 값이 5이고 우리 컴포넌트가 여전히 5로 리렌더링 되었다면 리액트는 이전에 렌더링했던 값인 [5]와 다음에 렌더링할 [5]를 비교합니다. 배열 내의 모든 항목의 값이 같기 때문에 (5 === 5), 리액트는 이펙트를 건너 뜁니다. 이것이 우리의 최적화입니다.

카운트가 6으로 업데이트 되고 렌더가 될 때 리액트는 이전 배열 값인 [5]를 다음 렌더링할 때 새로운 배열인 [6]과 비교합니다. 이번에는 리액트는 5 !== 6 이기 때문에 이펙트를 실행합니다. 만약 여러 아이템이 배열에 있다면 리액트는 그중 하나라도 값이 달라졌을 경우 이펙트를 재실행 합니다.

이것은 클린업 단계가 있는 이펙트에도 마찬가지로 적용됩니다.

/* from a previous example above */
React.useEffect1(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
}, [friendId]) /* 만일 friendId 가 변화할 경우에만 다시 구독합니다 */

중요 이런 최적화를 사용하려면, 컴포넌트 스코프의 모든 값을 의존 배열에 넣어야 합니다. (props 또는 state 모두) 그렇지 않으면 당신의 코드는 이전 렌더링의 오래된 값을 참조할 수도 있습니다.

만약 이펙트와 클린업을 단 한 번만 실행시키고 싶다면(마운트될 때와 언마운트될때) React.useEffect0을 사용하세요.

만약 최적화 관련 주제에 대해 더 관심이 있으시다면 ReactJS의 Performance Optimization Docs 문서에 더 많은 정보가 있습니다.