ReScript in Korean

커스텀 훅 만들기

원문

리액트에는 즉시 사용 가능한 React.useState 또는 React.useEffect같은 몇가지 훅이 기본적으로 제공됩니다. 여기선 리액트 사용 사례에 대한 고차 수준의 훅을 만들고 사용하는법을 배웁니다.

왜 커스텀 훅을 쓰죠?

커스텀 훅을 사용하면 기존 컴포넌트 로직을 재사용할 수 있도록 별도의 함수로 추출할 수 있습니다.

React.useEffect section 문서에서 사용된 예제로 돌아가보죠. 여기서 우리는 친구가 온라인인지 오프라인인지를 나타내는 메시지를 표시하는 채팅 애플리케이션용 컴포넌트를 만들었습니다.

/* FriendStatus.res */
module ChatAPI = {
/* Imaginary globally available ChatAPI for demo purposes */
type status = { isOnline: bool };
@val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus";
@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>
}

이제 우리의 채팅 애플리케이션은 연락처 목록을 가지고 있습니다. 그리고 우리는 온라인 유저의 이름을 녹색으로 렌더링하고 싶습니다. 우리는 복사 & 붙여넣기로 위와 비슷한 로직으로 FriendListItem 컴포넌트를 만들었습니다. 그러나 중복이 대부분이라 그다지 이상적이지 못하네요

/* FriendListItem.res */
type state = Offline | Loading | Online;
/* module ChatAPI = {...} */
type friend = {
id: string,
name: string
};
@react.component
let make = (~friend: friend) => {
let (state, setState) = React.useState(_ => Offline)
React.useEffect(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange)
}
Some(cleanup)
})
let color = switch(state) {
| Offline => "red"
| Online => "green"
| Loading => "grey"
}
<li style={ReactDOMStyle.make(~color,())}>
{React.string(friend.name)}
</li>
}

우리는 FriendStatusFriendListItem 컴포넌트에서 같은 로직을 공유하고싶습니다.

우리는 리액트에서 컴포넌트 간 상태를 가진 로직을 공유하는 두가지 인기있는 전통적인 방법을 알고 있습니다. 그 두가지 방법은 Render-Props 와 Higher-order Components(HoC) 입니다. 우리는 이제 훅으로 같은 문제를 추가적인 컴포넌트 트리를 작성하지 않고 해결하는 방법을 알아보겠습니다.

커스텀 훅으로 추출하기

일반적으로 두 함수간에 로직을 공유하려면, 세번째 함수를 만들어 공통된 부분을 추출하는 방법이 있습니다. 두 컴포넌트도 함수 훅도 함수이기 때문에 이 방법도 잘 작동합니다.

커스텀 훅은 이름이 ”use”로 시작하고 다른 훅을 호출할 수 있는 함수입니다 예를 들어, 아래의 useFriendStatus는 우리의 첫 커스텀 훅입니다. (state 타입도 캡슐화하기 위해 새 파일 FriendStatusHook.res를 만듭니다)

/* FriendStatusHook.res */
/* module ChatAPI {...} */
type state = Offline | Loading | Online
let useFriendStatus = (friendId: string): state => {
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)
})
state
}

내부적으로 딱히 새로운 것은 없어요. 로직은 위에 작성된 컴포넌트로부터 복사된 것입니다. 컴포넌트에서와 마찬가지로 커스텀 훅의 가장 상위 레벨에서 다른 훅을 호출해야합니다.

리액트 컴포넌트와 다르게 커스텀 훅에서는 @react.component와 같은 특정 시그니쳐가 필요하지 않습니다. 우리는 훅이 무엇을 인수로 받고 무엇을 반환하는지 결정할 수 있습니다. 즉, 이건 일반적인 함수와 같습니다. 훅 규칙이 적용된다는 것을 한눈에 알 수 있도록 이름은 항상 use로 시작해야합니다.

useFriendStatus훅의 목적은 Friend의 상태를 구독하는 것입니다. 이것이 바로 friendId를 인수로 받고 Online, Offline 또는 Loading과 같은 상태를 반환하는 이유입니다.

let useFriendStatus = (friendId: string): status {
let (state, setState) = React.useState(_ => Offline);
/* ... */
state
}

이제 우리의 커스텀 훅을 만들어보죠.

커스텀 훅 사용하기

우리의 첫 목표는 FriendStatus 그리고 FriendListItem 컴포넌트에서 중복 로직을 제거하는 것이었습니다. 두 컴포넌트 모두 Friend의 온라인 상태를 알고싶어합니다.

이제 이 로직을 useFriendStatus 훅으로 추출했으니 사용할 수 있습니다.

/* FriendStatus.res */
type friend = { id: string };
@react.component
let make = (~friend: friend) => {
let onlineState = FriendStatusHook.useFriendStatus(friend.id);
let status = switch(onlineState) {
| FriendStatusHook.Online => "Online"
| Loading => "Loading"
| Offline => "Offline"
}
React.string(status);
}
/* FriendListItem.res */
@react.component
let make = (~friend: friend) => {
let onlineState = FriendStatusHook.useFriendStatus(friend.id);
let color = switch(onlineState) {
| Offline => "red"
| Online => "green"
| Loading => "grey"
}
<li style={ReactDOMStyle.make(~color,())}>
{React.string(friend.name)}
</li>
}

이 코드는 원래의 예제와 동일합니까? 그렇습니다. 이것은 정확히 동일한 방식으로 작동합니다. 자세히 살펴보면, 우리가 동작을 변경하지 않은것을 알 수 있습니다. 우리가 한 것은 두 함수 사이의 공통 코드를 별도의 함수로 추출하는 것 뿐이었습니다. 커스텀 훅은 리액트 기능이 아니라 훅 디자인에서 자연스럽게 따르는 규칙입니다.

제 커스텀 훅의 이름은 항상 “use”로 시작해야할까요? 부탁이에요 그렇게 해주세요. 이 규칙은 매우 중요하거든요. 그것 없이는 특정 함수가 내부에 훅에 대한 호출을 포함하는지 아닌지 알 수 없습니다. 때문에 우리는 훅의 규칙(Rules of Hooks) 위반을 자동으로 확인할 수 없습니다.

동일한 훅을 사용하는 두 컴포넌트가 상태를 공유하나요? 아닙니다. 커스텀 훅은 상태 저장 로직을 재사용하는 메커니즘(Ex. 구독 설정 및 현재 값 기억)이지만 커스텀 훅을 사용할 때마다 내부의 모든 상태와 이펙트가 완전히 격리됩니다.

커스텀 훅은 어떻게 격리 된 상태가 되나요? 훅에 대한 각각의 호출은 격리 된 상태를 만듭니다. 왜냐하면 우리가 useFriendStatus 훅을 직접 호출하기 때문에 리액트 관점에서 컴포넌트는 useState와 useEffect만 호출합니다. 그리고 앞서 배운 것처럼 하나의 컴포넌트에서 useState와 useEffect를 여러 번 호출할 수 있으며 그것은 완전히 독립적입니다..

팁: 훅끼리 정보 넘기기

훅은 함수이기 때문에 우리는 훅 사이에 정보를 전달할 수 있습니다.

이를 설명하기 위해 우리는 가상 채팅 예제의 다른 컴포넌트를 사용할 것입니다. 현재 선택한 친구가 온라인 상태인지 여부를 표시하는 채팅 메시지 셀렉터입니다.

type friend = {id: string, name: string}
let friendList = [
{id: "1", name: "Phoebe"},
{id: "2", name: "Rachel"},
{id: "3", name: "Ross"},
]
@react.component
let make = () => {
let (recipientId, setRecipientId) = React.useState(_ => "1")
let recipientOnlineState = FriendStatusHook.useFriendStatus(recipientId)
let color = switch recipientOnlineState {
| FriendStatusHook.Offline => Circle.Red
| Online => Green
| Loading => Grey
}
let onChange = evt => {
let value = ReactEvent.Form.target(evt)["value"]
setRecipientId(value)
}
let friends = Belt.Array.map(friendList, friend => {
<option key={friend.id} value={friend.id}>
{React.string(friend.name)}
</option>
})
<>
<Circle color />
<select value={recipientId} onChange>
{React.array(friends)}
</select>
</>
}

현재 선택된 친구 ID를 recipientId 상태 변수로 놓고 사용자가 <select> UI에서 다른 친구를 선택하면 상태를 업데이트 합니다.

useState 훅 호출은 recipientId 상태 변수의 최신 값을 제공하므로 이를 커스텀 훅인 FriendStatusHook.useFriendStatus에 인자로 전달할 수 있습니다.

let (recipientId, setRecipientId) = React.useState(_ => "1")
let recipientOnlineState = FriendStatusHook.useFriendStatus(recipientId)

이를 통해 현재 선택된 친구가 온라인 상태인지 여부를 알 수 있습니다. 다른 친구를 선택하고 recipientId 상태 변수를 업데이트하면 우리가 만든 FriendStatus.useFriendStatus 훅이 이전에 선택한 친구의 구독을 취소하고 새로 선택한 친구의 상태를 구독합니다.

상상력을 발휘해보세요

커스텀 훅은 로직 공유의 유연성을 제공합니다. Form 처리, 애니메이션, 선언적 서브스크립션, 타이머 등 일반적인 사용사례와 이를 포함하지 않은 더 많은 사용 사례에 적용 가능한 커스텀 훅을 작성할 수 있습니다. 또한 리액트의 내장 기능만큼 사용하기 쉬운 훅을 만들 수 있습니다.

너무 일찍 추상화하지 않도록 하세요. 충분한 상태 저장 로직 처리가 관련되어 있을 때 컴포넌트가 상당히 커지는 것은 매우 일반적입니다. 이것은 정상적인 현상입니다. 바로 훅을 만들어 분할할 필요는 없습니다. 그러나 커스텀 훅이 간단한 인터페이스 뒤에 복잡한 논리를 숨기거나 복잡한 컴포넌트를 풀어볼 수 있는 케이스를 발견하는 것도 좋습니다.