함수형 컴포넌트는 기본적으로 리렌더링 시 함수안에 작성된 모든 코드가 다시 실행된다. 즉, 함수형 컴포넌트가 리렌더링될 때는 무조건 새롭게 컴포넌트 함수가 선언되고, state가 초기화되며, 메모리에 할당된다.
그렇기 때문에 함수형 컴포넌트에서는 기존에 가지고 있던 상태를 관리할 수 없었다. 하지만 hook이 브라우저에 매모리를 할당해 저장하게 되면서 함수형 컴포넌트에서도 상태 관리가 가능하게 되었다.
최상위 단계에서만 호출해야 한다.
react는 Hook들이 호출되는 순서를 저장한다. 그리고 매 렌더링마다 순서대로 Hook을 호출하는데, 만약 조건문이나, 반복문, 중첩 함수 내에서 어떠한 이유로 어떤 Hook이 호출되지 않을 경우 React가 기억하고 있는 Hook의 순서와 실행되는 Hook의 순서와 실행되는 Hook들의 순서가 달라져 버그 가능성이 높아지게 된다.
따라서 최상위 단계에서만 호출해야 하므로 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
React 함수형 컴포넌트 내에서만 호출 가능하다.
커스텀 훅의 이름은 use로 시작해야 한다.
useScroll, useToggle 관례에 가까운 규칙이지만 다른 개발자가 볼 때도 Custom Hook이라는것을 알아 볼 수 있다.useState()
const [state, setState] = useState(initialValue);useRef()
const inputElement = useRef();
<input type="text" ref={inputElement} />
useRef Hook을 통해 참조를 생성한다. 그리고 참조할 요소를 ref 속성으로 연결한다.
예 ) 특정 DOM 요소에 접근하기
import { useRef, useEffect } from 'react';
export default function AccessDom() {
const inputRef = useRef();
useEffect(() => {
// Javascript의 명령형 코드를 사용해 DOM 요소에 접근하면 React의 선언적 특성에 위배
// 직접 DOM을 조작하면 React의 가상 DOM과 실제 DOM 간의 상태가 일치하지 않을 수 있음
const inputElement = document.getElementById('myInput');
inputElement.focus();
// 컴포넌트가 Mount된 후에 input 요소에 접근
// inputRef는 단순히 참조 객체이기 때문에 DOM 요소에 접근하기 위해 inputRef.current 사용
inputRef.current.focus();
}, []);
return (
<>
{/* document.getElementById로 DOM 요소에 접근하는 좋지 않은 방식 */}
<input id='myInput' type='text' />
{/* useRef Hook을 사용한 안전한 DOM 접근 방식 */}
<input ref={inputRef} type='text' />
</>
);
}
const variable = useRef(초기값);
그리고 일반적인 지역 변수가 필요한 경우에는 useRef Hook에 인자로 초기값을 작성한다. 이 useRef로 만들어진 참조 변수는 리렌더링이 발생해도 계속 유지된다.
예 ) useRef ,useState,일반 변수의 상태 관리와 DOM 접근의 차이
import React, { useRef, useState } from "react";
export default function RefFunction2() {
const idRef = useRef(1);
const [id, setId] = useState(10);
let test = 10;
const plusIdRef = () => {
idRef.current += 1;
console.log(idRef.current);
test += 1;
console.log(test);
};
const plusIdState = () => {
setId(id + 1);
};
return (
<div>
<h1>Ref Sample</h1>
<h2>{test}</h2>
<h2>{idRef.current}</h2>
<button onClick={plusIdRef}>PLUS Ref</button>
<h2>{id}</h2>
<button onClick={plusIdState}>PLUS State</button>
</div>
);
}
useRef와 useState 비교이 코드는 React의 useRef와 useState를 활용하여 상태 관리와 DOM 접근의 차이를 보여줍니다. 각각의 사용 방식과 동작 방식을 이해하기 쉽게 설명하겠습니다.
useRefidRef로 선언되어 있으며, idRef.current를 통해 값을 저장하고 접근한다.useRef는 값이 변경되어도 컴포넌트를 다시 렌더링하지 않는다.useStateid로 선언되어 있으며, setId를 통해 상태를 업데이트한다.useState 값이 변경되면 컴포넌트를 다시 렌더링한다.testuseRef의 초기값:idRef.current = 1.useRef 값은 렌더링과 무관하다.useState의 초기값:id = 10.test 변수의 초기값:test = 10.화면 출력:
<h2>{test}</h2> => 10
<h2>{idRef.current}</h2> => 1
<h2>{id}</h2> => 10
PLUS Ref 버튼 클릭const plusIdRef = () => {
idRef.current += 1;
console.log(idRef.current);
test += 1;
console.log(test);
};
idRef.current 값 변경:idRef.current 값이 1 증가. 예를 들어, 1 → 2.idRef.current의 새로운 값이 출력된다.test 변수 값 변경:test 값이 1 증가. 예를 들어, 10 → 11.test의 새로운 값이 출력된다.화면은 여전히 다음과 같다:
<h2>{test}</h2> => 10 (변경되지 않음)
<h2>{idRef.current}</h2> => 1 (변경되지 않음)
<h2>{id}</h2> => 10
PLUS State 버튼 클릭const plusIdState = () => {
setId(id + 1);
};
setId 호출:id 값이 10 → 11로 변경된다.useState는 상태가 변경되면 컴포넌트를 리렌더링한다.id 값이 렌더링된다.화면 업데이트:
<h2>{test}</h2> => 10 (변경되지 않음)
<h2>{idRef.current}</h2> => 1 (변경되지 않음)
<h2>{id}</h2> => 11 (업데이트됨)
useRefidRef.current의 변경은 단순히 메모리에 저장되는 값만 업데이트한다.useStatetest)| 항목 | 렌더링 영향 | 사용 목적 | 코드 내 역할 |
|---|---|---|---|
useRef | X | 값 저장, DOM 접근 | idRef를 통해 값 저장 |
useState | O | 상태 관리, 화면 업데이트 | id를 통해 상태 관리 |
일반 변수 (test) | X | 단순 계산, 임시 값 저장 | 계산 중 임시 값 저장 |
초기 렌더링:
useRef, useState, test 초기값이 설정되고 화면에 렌더링된다.PLUS Ref 버튼 클릭:
idRef.current와 test 값은 변경되지만 화면은 리렌더링되지 않는다. (특정 DOM 요소에 안전하게 접근. 리렌더링 되어도 유지되는 지역 변수)PLUS State 버튼 클릭:
id 값이 변경되며 컴포넌트가 리렌더링된다.
useRef를 통해 만든 참조 객체
useRef를 통해 만든 참조 객체는 말 그대로 객체이기 때문에 ref 속성을 통해 inputRef 참조 객체로 연결된 input 요소에 직접 접근하려면 current 메서드를 사용해야 한다. 그러므로 input.current.focus를 통해 input 요소에 focus 설정을 할 수 있다.
useEffect()
useEffect = (callback 함수, [의존성 배열]); 함수형 컴포넌트에서 LifeCycle에 따라 발생할 이벤트들을 관리할 수 있다. 컴포넌트가 Mount, Unmount되거나 의존성 배열의 요소에 변화가 생겨 Update될 때, callback 함수가 호출된다.useMemo()
useCallback()
useReducer()
useState의 대체재로 사용한다.reducer)와 디스패치(dispatch)를 통해 상태를 업데이트한다.useContext()
// Context 생성
const MyContext = React.CreateContext(defaultValue);
// Context에 값을 전달
<MyContext.Provider value={/* 전달할 값 */} />
// Context 값 사용
const value = useContext(MyCOntext); CreateContext를 사용해 Context를 생성하고, Provider 컴포넌트에서 Context에 값을 전달한다. 그리고 useContext로 해당 Context의 값을 사용한다.useMemo()
const memoizedValue = useMemo(callback 함수, [의존성 배열]); 렌더링 과정에서 두 번째 인자로 받은 의존 배열(dependency) 내 값이예 )
function calc(a, b) {
return a + b
}
// 함수형 컴포넌트
const MyComponent() {
const result = calc(3,5)
return <p>{result}</p>
컴포넌트가 렌더링 됨 = 함수를 호출 ⇒ 함수 내부의 모든 변수 초기화
리렌더링 될 때 마다 MyComponent 호출,
변수 result가 초기화 되므로 매번 calc 함수 실행!
매번 calc되는것은 비효율적. 따라서 결과만 다져오게 끔 하도록 useMemo가 필요함.
⇒ useMemo를 사용하여 부하가 걸리는 함수의 결과값을 메모리에 저장한 뒤, 리렌더링이 될 때 그 결과값만 가져와서 재사용해 줌으로써 성능을 최적화
예 )임의로 큰 연산을 하는 computeExpensiveValue 함수를 작성
import React, { useState, useMemo } from 'react';
export default function ComputeExpensive() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
const computeExpensiveValue = (count) => {
console.log('임의로 큰 연산 진행 중...');
for (let i = 0; i < 100000; i++) {}
return count ** 2;
};
// useMemo를 사용하지 않는 경우
const expensiveValue = computeExpensiveValue(count);
// useMemo를 사용하는 경우
// const expensiveValue = useMemo(() => computeExpensiveValue(count), [count]);
return (
<>
<p>임의의 큰 연산 결과: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={() => setOtherState(otherState + 1)}>
otherState + 1
</button>
</>
);
}
위의 코드 설명 : 콘솔을 찍어 연산을 하는 함수가 호출되었다는 것을 알리고 인자로 받은 값의 거듭제곱한 값을 반환한다. 그리고 expensiveValue변수에 computeExpensiveValue 함수의 결과값 저장하는데, useMemo를 사용하는 경우, 사용하지 않는 경우를 비교할 것이다.
useMemo를 사용하지 않은 경우는 count가 업데이트 될때와 computeExpensiveValue 함수와 관련이 없는 otherState가 업데이트 될때 모두 computeExpensiveValue 함수가 실행되는 것을 볼 수 있다.
이제 useMemo hook을 사용해보자. 이제 함수의 결과값을 저장해, count의 값에 변화가 있을때만 함수가 실행 되는것을 볼 수 있다.
count state의 값을 업데이트 할 때만 computeExpensiveValue 함수가 호출되고, otherState 의 state 값이 업데이트될 때는 useMemo hook의 의존성 배열에 작성했던 count 값에 변화가 없으니 리렌더링 되어도 computeExpensiveValue 함수가 호출되지 않는다.
useCallback()
useMemo는 값을 메모이제이션(값을 최적화), useCallback은 함수를 메모이제이션(다시 rendering 될 때 함수를 다시 불러오는것을 막음)한다.const memoizedCallback = useCallback(callback 함수, [의존성 배열]);의존성 배열의 요소에 변화가 있으면 callback 함수가 실행되는것은 useMemo와 같다. 그리고 useCallback에 저장할 함수가 state에 의존적이지 않은 함수라면 빈 배열을 작성해도 된다.
예 ) 리스트를 렌더하고, 각 리스트 요소를 수정, 삭제하는 기능
import { useState, useCallback } from 'react';
export default function UseCallbackPrac() {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
const [editing, setEditing] = useState(null);
const [editText, setEditText] = useState('');
// item 문자열을 인자로 받아 editing, setEditText 상태의 값을 item 문자열로 업데이트
const handleEdit = useCallback((item) => {
setEditing(item);
setEditText(item);
}, []);
// itemToSave 문자열을 인자로 받아 items 상태의 배열 요소 중
// editing과 동일한 요소만 itemToSave로 변경하고
// 나머지 요소는 그대로 유지한 배열을 items 상태값으로 업데이트
const handleSave = useCallback(
(itemToSave) => {
setItems(items.map((item) => (item === editing ? itemToSave : item)));
setEditing(null);
},
[items, editing]
);
// itemToDelete 문자열을 인자로 받아 items 상태의 배열 요소 중
// itemToDelete와 동일하지 않은 요소로만 이루어진 배열을 items 상태값으로 업데이트
const handleDelete = useCallback(
(itemToDelete) => {
setItems(items.filter((item) => item !== itemToDelete));
},
[items]
);
return (
<ul>
{items.map((item) => (
<li key={item}>
{editing === item ? (
<input
type='text'
value={editText}
onChange={(e) => setEditText(e.target.value)}
/>
) : (
item
)}
{editing === item ? (
<button onClick={() => handleSave(editText)}>Save</button>
) : (
<>
<button onClick={() => handleEdit(item)}>Edit</button>
<button onClick={() => handleDelete(item)}>Delete</button>
</>
)}
</li>
))}
</ul>
);
}
useReducer()
const [state, dispatch] = useReducer(reducer, initialState);state: 현재 상태
dispatch: 상태를 업데이트하는 함수 (액션을 발생시키는 함수-액션은 상태를 변경하기 위한 정보를 담고 있는 객체)
reducer: 상태 업데이트 로직을 정의한 함수 (state를 업데이트하는 함수)
initialState : initialState 상태의 초기값
import React, { useReducer } from "react";
const initState = { value: 0 };
const reducer = (prevState, action) => {
console.log(prevState, action);
switch (action.type) {
case "INCREMENT":
return { value: prevState.value + 1 };
case "DECREMENT":
return { value: prevState.value - 1 };
case "RESET":
return initState;
default:
return { value: prevState.value };
}
};
export default function UseReducerEx() {
// reducer : state 를 업데이트 하는 함수
// dispatch : 액션 (state가 어떻게 변경되어야 하는지에 대한 힌트)을 발생시키는 함수
// state : 현재 상태
// useReducer는 [state, dispatch] 를 리턴함
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<h1>UseReducerEx</h1>
<h2>{state.value}</h2>
<button onClick={() => dispatch({ type: "INCREMENT" })}>Plus</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>Minus</button>
<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
</div>
);
}
useState() vs. useReducer()useState()는 단순한 상태 관리에 적합하다.useReducer()는 상태가 복잡하거나 업데이트 로직이 다양한 경우 적합하다.useReducer는 useState 기반이지만 무조건 useReducer가 좋은것은 아니다. 경우에 따라 유연하게 사용하기!
function useCustomHook(인자) {
커스텀 훅 로직
return 반환할 데이터
}hooks/ 디렉토리에 훅을 정의.use로 시작. ( 다른 개발자가 볼 때에도 이 함수가 커스텀 훅이라는것을 알려주기 위함 )useScroll.js, useToggle.jsutil Function함수와의 차이점 : Custom Hooks 과 util 함수 모두 코드 중복을 줄이고, 유지보수성을 높이기 위해 사용된다. 하지만 Custom Hooks은 React Hook을 사용한 특정 동작이나 기능을 제공하는 함수이고, util 함수는 React의 Hook을 사용하지 않고 어디서든 사용할 수 있는 독립적인 함수이다.
util Function 함수 util Function : 자주 사용되는 기능들을 모듈화하여 재사용할 수 있도록 별도의 파일에 저장해서 사용하는 함수예 ) useToggle Hook
import { useState } from 'react';
export default function useToggle(initValue = false) {
const [value, setValue] = useState(initValue);
const toggleValue = () => {
setValue(!value);
};
return [value, toggleValue];
}
useToggle Hook 은 어떤 요소나 컴포넌트를 토글 시키는 Hook 이다.
그래서 initialValue를 전달받아 해당 요소나 컴포넌트의 렌더 유무를 받을 건데 이 기본값을 false로 저장 했다.
그다음 initialValue를 기본값으로 가지고 있는 value state를 만든다. 그리고 toggleValue 라는 함수로 value의 상태값을 반전시키는 기능을 추가해 토글의 상태인 value와 toggleValue함수를 배열로 return하면 useToggle이라는 Custom Hook 을 만들 수 있다.
이 useToggle Hook을 사용하기 위해 src/component/Faq.js 파일을 만들고 기본 구조를 만든다.
import useToggle from '../hook/useToggle';
export default function Faq() {
const [isFaqOpen, setIsFapOpen] = useToggle();
return (
<>
<h3>custom hook (useToggle)</h13>
<div onClick={setIsFapOpen} style={{ cursor: 'pointer' }}>
Q. 리액트에서 커스텀 훅을 만들 수 있나요?
</div>
{isFaqOpen && <div>A. 네, 가능합니다.</div>}
</>
);
}
그리고 useToggle Hook에서 배열 형태로 반환했으니 배열로 구조분해해서 isFaqOpen, setIsFaqOpen으로 useState처럼 선언했다. 각자 토글의 상태와 토글의 상태를 반전시키는 함수이다.
마지막으로 div 태그에 onClick 속성으로 setIsFapOpen 함수를 선언하고, isFaqOpen이 true라면 가능하다는 설명이 작성된 div 태그를 렌더해 useToggle Hook을 사용하는 방법이다.
React Hooks는 클래스형 컴포넌트의 복잡한 상태 관리와 라이프사이클 관리 로직을 함수형 컴포넌트로 간결하게 구현할 수 있도록 돕는 강력한 도구이다. 각 Hook의 역할과 특징을 이해하고 상황에 맞게 적절히 사용하는 것이 중요하다.