반응형
📘 『소플의 처음 만난 리액트』를 읽고 정리한 글입니다.
훅(Hook)이 뭘까?
- 기존의 함수 컴포넌트는 클래스 컴포넌트와는 다르게 코드도 굉장히 간결하고 별도로 state를 정의하거나 컴포넌트의 생명주기 함수를 사용할 수 없었다.
- 이런 기능을 지원하기 위해 나온 것이 바로 훅이다.
- 훅라는 단어는 갈고리를 뜻하는데, 원래 존재하는 어떤 기능에 갈고리를 거는 것처럼 끼어 들어가 같이 수행되는 것을 의미한다.
- 이러한 훅의 이름은 모두 use로 시작한다.
- 물론 개발자가 직접 커스텀 훅을 만들어 사용할 경우 이름을 개발자 마음대로 지을 수도 있지만 그러한 경우에도 이름 앞에 use를 명시해 주는 것이 올바르다.
useState
- state를 사용하기 위한 훅이다.
function Counter(props) {
var count = 0;
return (
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick={() => count++}>
클릭
</button>
</div>
);
}
- 상기 코드는 버튼을 클릭하면 카운트 값을 증가시킬 수는 있지만 재렌더링이 일어나지 않아 증가된 카운트 값이 화면에 표시되지 않는다.
- 따라서 아래의 코드와 같이 state를 사용해서 값이 바뀔 때마다 재렌더링이 되도록 해야 할 필요성이 있다.
import React, { useState } from "react";
function Counter(props) {
const [count, setCount] = useState(0);
return (
<div>
<p>총 {count}번 클릭했습니다.</p>
<button onClick={() => setCount(count + 1)}>
클릭
</button>
</div>
);
- 위의 Counter(props) 메소드는 버튼이 눌렸을 때 setCount() 함수를 호출해서 카운트를 증가시킨다.
- 그리고 count의 값이 변경되면 컴포넌트가 재렌더링되면서 화면에 증가된 카운트 값을 표시할 수 있다.
- 클래스 컴포넌트에서는 setState() 함수 하나를 사용해서 모든 state 값을 업데이트할 수 있었지만 함수 컴포넌트에서는 변수 각각에 대해 set 함수가 따로 존재한다.
useEffect
- 사이드 이펙트를 수행하기 위한 훅이다.
- 사이드 이펙트라고 하면 부정적인 느낌을 가지고 있지만 리액트에서의 사이드 이펙트는 부정적인 의미는 아니다.
- 리액트에서 말하는 사이드 이펙트는 서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 등의 일반적인 이펙트를 의미한다.
- 클래스 컴포넌트에서 제공하는 생명주기 함수인 componentDidMount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능을 하나로 통합해서 제공하며 다음과 같이 사용한다.
useEffect(이펙트 함수, 의존성 배열);
- 배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 이펙트 함수가 실행된다.
- 기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링 된 이후와 업데이트로 인한 재렌더링 이후에 실행된다.
- 만약 이펙트 함수가 마운트, 언마운트 시에 단 한 번씩만 실행되게 하고 싶으면 의존성 배열에 빈 배열을 넣으면 된다.
- 이렇게 하면 이펙트가 props나 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러 번 실행되지 않는다.
- 의존성 배열 없이 useEffect()를 사용하면 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미로 받아들인다.
- 그래서 컴포넌트가 처음 렌더링 될 때를 포함해서 매번 렌더링 될 때마다 이펙트가 실행되는데, 결과적으로 componentDidMount(), componentDidUpdate()와 동일한 역할을 하게 된다.
- 그렇다면 componentWillUnmount()은 어떻게 수행할까?
- 결과만 말하자면 useEffect()에서 리턴하는 함수는 컴포넌트가 마운트 해제될 때 호출된다.
- useEffect()는 자주 사용하기 때문에 구조를 꼭 기억해두어야 한다.
useMemo
const memoizedValue = useMemo(() => {
return 함수(의존성 변수1, 의존성 변수2);
}, [의존성 변수1, 의존성 변수2]);
- Memoized value를 반환하는 훅이다.
- 의존성 배열에 들어 있는 변수가 변했을 경우에만 함수를 호출하여 새로운 결괏값을 반환하며, 그렇지 않은 경우는 기존 함수의 결괏값을 그대로 반환한다.
- 컴포넌트가 다시 렌더링 될 때마다 연산량이 높은 작업의 반복을 피할 수 있다.
- 렌더링이 일어나는 동안 실행해서는 안 될 작업을 useMemo에 넣으면 안된다는 점을 유의해야 한다.
- 의존성 배열을 넣지 않을 경우 렌더링이 일어날 때마다 함수가 실행되며, 의존성 배열에 빈 배열을 넣을 경우 컴포넌트 마운트 시에만 함수가 실행된다.
useCallback
- useMemo와 유사하지만 한 가지 차이점은 값이 아닌 함수를 반환한다.
- useCallback에서 파라미터로 받는 이 함수를 콜백이라고 부른다.
- 의존성 배열에 따라 Memoized value를 반환한다는 점에서는 useMemo와 완전히 동일하다.
- 만약 useCallback을 사용하지 않고 컴포넌트 내에 함수를 정의한다면 매번 렌더링이 일어날 때마다 함수가 새로 정의된다.
- 따라서 해당 훅을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하여 반복을 없애주는 것이 올바르다.
useRef
- 레퍼런스를 사용하기 위한 훅이다.
- 리액트에서 레퍼런스란 특정 컴포넌트에 접근할 수 있는 객체를 의미한다.
- useRef 훅은 레퍼런스 객체를 반환한다.
- 레퍼런스 객체에는 .current라는 속성이 있는데 이는 현재 참조하고 있는 엘리먼트를 의미한다고 보면 된다.
const refContainer = useRef(초깃값);
- 위 코드처럼 사용하면 된다.
function TextInputWithFocusButton(props) {
const inputElem = useRef(null);
const onButtonClick = () => {
inputElem.current.focus();
};
return (
<div>
<input ref={inputElem} type="text"/>
<button onClick={onButtonClick}>Focus the input</button>
</div>
);
}
- 상기 코드는 버튼을 클릭하면 input 태그를 포커싱하는 예제이다.
- 매번 렌더링 될 때마다 항상 같은 ref 객체를 반환한다.
- 주의해야 할 점은 useRef 훅은 내부의 current 속성이 변경되더라도 재렌더링을 일으키지 않는다는 것이다.
- 따라서 ref에 DOM node가 연결되거나 분리될 경우 어떤 코드를 실행하고 싶다면 callbackref 방식을 사용해야 한다.
- 이는 DOM node의 ref 속성에 useRef 레퍼런스 객체 대신 useCallback 반환 함수를 넣어주는 것이다.
- 이렇게 되면 자식 컴포넌트가 변경되었을 때 알림을 받을 수 있고, 이를 통해 다른 정보들을 업데이트할 수 있다.
function MeasureExample(props) {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<h1 ref={measureRef}>안녕, 리액트</h1>
<h2>위 헤더의 높이는 {Math.round(height)}px입니다.</h2>
</div>
);
}
- 위의 예제에서는 h1 태그의 높이값을 업데이트 하고 있다.
- useCallback의 의존성 배열로 빈 배열이 들어가 있으므로 마운트, 언마운트 시에만 콜백 함수가 호출된다.
훅의 규칙
훅은 무조건 최상위 레벨에서만 호출해야 한다.
- 여기서 말하는 최상위 레벨은 리액트 함수 컴포넌트의 최상위 레벨을 의미하는데, 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안 된다는 의미이다.
- 이 규칙에 따라 훅은 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출되어야 한다.
리액트 함수 컴포넌트에서만 훅을 호출해야 한다.
- 일반 자바스크립트 함수에서 훅을 호출하면 안 된다.
- 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있다.
커스텀 훅
- 커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함이다.
- 또한 파라미터로 무엇을 받을지, 어떤 것을 반환할지 개발자가 직접 정할 수 있다.
- 컴포넌트 명은 꼭 use로 시작하도록 하여 해당 컴포넌트에서 훅을 호출함을 명시해야 한다.
function UserStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
if (isOnline === null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
- 상기 코드에는 커스텀 훅을 적용하고자 한다.
- isOnline이라는 state에 따라서 사용자의 상태가 온라인인지 여부를 텍스트로 보여주는 컴포넌트이다.
function UserListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
return (
<li style={{ color : isOnline ? 'green' : 'black'}}>
{props.user.name}
</li>
);
}
- 위의 예제는 온라인 상태의 사용자의 이름만 초록색으로 표시하는 코드이다.
- 코드를 보면 userStatus 컴포넌트와 useState(), useEffect() 훅을 사용하는 부분이 동일하다.
- 즉 여러 곳에서 중복되는 코드이므로 추출하여 다음과 같이 커스텀 훅으로 만들 수 있다.
function useUserStatus(userId) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
return isOnline;
}
- state와 관련된 중복 로직을 useUserStatus라는 커스텀 훅으로 추출해낸 것이다.
- 이제 다음 코드와 같이 UserStatus와 UserListItem에 커스텀 훅을 적용하면 된다.
function UserStatus(props) {
const isOnline = useUserStatus(props.user.id);
if (isOnline === null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
function UserListItem(props) {
const isOnline = useUserStatus(props.user.id);
return (
<li style={{ color : isOnline ? 'green' : 'black'}}>
{props.user.name}
</li>
);
}
- 위처럼 같은 커스텀 훅을 사용하는 두 개의 컴포넌트는 state를 공유하는 것일까?
- 아니다. 단순히 state 연관 로직을 재사용이 가능하게 만든 것 뿐이다.
- 여러 개의 컴포넌트에서 하나의 커스텀 훅을 사용하더라도 컴포넌트 내부의 state나 effects는 전부 분리되어 있다.
const userList = [
{ id : 1, name : 'Inje' },
{ id : 2, name : 'Mike' },
{ id : 3, name : 'Steve' }
];
function ChatUserSelector(props) {
const [userId, setUserId] = userState(1);
const isUserOnline = useUserStatus(userId);
return (
<div>
<Circle color={isUserOnline ? 'green' : 'red'}/>
<select
value={userId}
onChange={event => setUserId(Number(event.target.value))}
>
{userList.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</div>
);
}
- 훅 간의 데이터를 공유하는 예제 코드이다.
- userId가 변경될 때마다 setUserId() 함수가 호출되고, useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독한다.
오늘도 실습 한 번 해보자
function UseCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increaseCount = () => setCount((count) => count + 1);
const decreaseCount = () => setCount((count) => Math.max(count - 1, 0));
return [count, increaseCount, decreaseCount];
}
export default UseCounter;
- UseCounter.jsx 컴포넌트를 작성해 보자.
- 해당 컴포넌트에 주어지는 props 값이 최초 카운트 수가 된다.
- 이후로 increaseCount(), decreaseCount() 메소드를 선언하여 이벤트 함수와 함께 반환한다.
import { useEffect, useState } from "react";
import UseCounter from "./UseCounter";
const MAX_CAPACITY = 10;
function ManageTeamMembers(props) {
const [isFull, setIsFull] = useState(false);
const [count , increaseCount, decreaseCount] = UseCounter(0);
useEffect(() => {
console.log("useEffect() 메소드가 호출되었습니다.");
console.log(`정원 도달여부 ${isFull}`);
});
useEffect(() => {
setIsFull(count >= MAX_CAPACITY);
console.log(`최근 개수: ${count}`);
}, [count]);
return (
<div style={{ padding : 16 }}>
<p>{`팀에 가입한 멤버는 총 ${count}명 입니다.`}</p>
<button onClick={increaseCount} disabled={isFull}>가입</button>
<button onClick={decreaseCount}>탈퇴</button>
{isFull && <p style={{ color : "red" }}>정원이 가득찼습니다.</p>}
</div>
);
}
export default ManageTeamMembers;
- 이번엔 ManageTeamMembers.jsx 컴포넌트를 작성해 보자.
- useEffect()를 선언함으로써 Team 컴포넌트가 렌더링 될 때마다 useEffect() 메소드의 호출여부와 팀의 멤버 정원 도달 여부를 콘솔에 찍는다.
- 또한 카운트 상태가 변할 때마다 최근 개수를 콘솔에 찍는다.
- 가입 버튼을 클릭하면 멤버의 카운트 수가 증가하며, 정원이 꽉 찼을 경우 비활성화된다.
- 탈퇴 버튼을 클릭하면 멤버의 카운트 수가 감소한다.
- 또한 정원이 꽉 찰 경우 빨간 글씨로 "정원이 가득 찼습니다." 문구가 노출된다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import ManageTeamMembers from './component/ManageTeamMembers';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ManageTeamMembers />
</React.StrictMode>
);
reportWebVitals();
- 작성한 컴포넌트를 index.js에 추가해 주자.
작성한 화면을 확인해 보자
- 요런 느낌
- 콘솔에는 다음과 같이 출력된다.
- ManageTeamMembers.jsx 파일의 MAX_CAPACITY 변수 값에 도달할 경우 정원 도달여부가 true로 바뀐다.
마치며
훅은 굉장히 중요하고도 복잡한 기능이다.
본인도 사실 완벽히 숙지하지는 못했다.
반복만이 살 길이다. 결국 무언가를 만들어보는 게 가장 중요하고, 잘못된 부분은 바로잡아가면 된다고 생각한다. 😎
이미지 출처
[React] Component 를 사용하는 기본적인 방법
컴포넌트는 React의 핵심 개념 중 하나이며, 이는 사용자 인터페이스(UI)를 구축하는 기반이다. 컴포넌트를 만들어보자. 우선 여기까지는 순수 JS의 선언 및 정의에 대한 구문이다. 리액트는 컴포
velog.io
소플 - soaple.io
소플
www.soaple.io
'개발 > 리액트' 카테고리의 다른 글
리액트 - 화면 개발(feat.Material UI) (8) | 2025.01.29 |
---|---|
리액트 핵심만 훑어보자 #9 이벤트 처리 (46) | 2024.11.24 |
리액트 핵심만 훑어보자 #7 리액트 컴포넌트의 생명주기 (41) | 2024.11.17 |
리액트 핵심만 훑어보자 #6 상태 (5) | 2024.11.11 |
리액트 핵심만 훑어보자 #5 컴포넌트 (12) | 2024.11.10 |