항해 플러스 과제를 진행하기 전 React Hooks에 대해서 미리 공부하려 한다.
이미 알고 있었고 많이 사용해 본 것들이지만, 공부하면서 새롭게 알게 된 사실들도 있어서 포스팅까지 하게 되었다.
특히 아무 생각 없이 사용했던 방식들을 "왜" 이렇게 써야만 하는지 알 수 있었다!
1. React Hooks
React 16.8에 도입된 기능으로,
함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 사용할 수 있게 해주는 함수들을 말한다.
Hooks을 사용하면 클래스 컴포넌트를 작성하지 않고도 React의 다양한 기능을 활용할 수 있다.
1) 등장배경
React Hooks는 클래스 컴포넌트에서 발생하던 여러 문제를 해결하기 위해 등장했다.
- 컴포넌트 간 상태 로직 재사용의 어려움
- 복잡한 컴포넌트의 이해와 관리의 어려움
- 클래스 this 키워드로 인한 혼란
- 생명주기 메서드의 비일관성
useState, useEffect, useMemo, useCallback과 같은 다양한 훅이 등장하면서
함수형 컴포넌트에서 상태 관리와 생명주기 로직을 간결하게 구현할 수 있게 되었다.
또한, 커스텀 훅을 통해 컴포넌트 간 상태 로직을 쉽게 재사용할 수도 있어 코드의 재사용성과 가독성도 크게 향상되었다.
2) React Hooks의 규칙
React Hooks을 사용할 때는 반드시 아래 두 가지 규칙을 지켜야 한다.
① 최상위에서 호출
Hooks은 항상 react 함수 컴포넌트의 최상위 레벨에서 호출되어야 한다.
반복문, 조건문, 중첩된 함수 내에서는 Hooks을 호출하면 안 된다.
function MyComponent() {
if (condition) {
const [count, setCount] = useState(0); // 조건문 내에서 Hooks 호출 ❌
}
return <div>{count}</div>;
}
React는 컴포넌트가 렌더링 될 때 Hooks의 호출 순서를 추적해 상태를 관리한다.
만일 반복문이나 조건문 안에서 Hooks이 호출되면, 호출 순서가 달라지게 되어 상태를 제대로 추적할 수 없게 된다.
만일 위 예시처럼 되어 있다면,
condition 값에 따라 Hooks이 호출되지 않을 수 있기 때문에 React가 상태 변경을 제대로 관리하지 못하게 된다.
② React 함수 내에서 호출
React 함수 컴포넌트 내 혹은 커스텀 Hooks 내에서만 호출해야 한다.
일반 자바스크립트 함수에서는 Hooks을 호출하지 않아야 한다.
function nonReactFunction() {
const [count, setCount] = useState(0); // 일반 함수 내에서 Hooks 호출 ❌
}
React Hooks의 경우 리액트 컴포넌트 안에서만 호출되고 관리할 수 있기 때문이다.
위 예시의 경우 React가 상태 변경을 추적할 수 없기 때문에 오류가 발생한다.
위 두 가지 규칙이 제대로 지켜지고 있는 확인하는 가장 쉬운 방법은 ESLint를 사용하는 것이다.
해당 플러그인을 사용하면, Hooks 사용 규칙을 자동으로 검사하고, 규칙을 어겼을 때 경고를 표시해 준다.
3) 주요 Hooks
① useState
React의 가장 기본적인 Hook으로 함수형 컴포넌트에서 상태를 관리할 수 있게 해 준다.
상태란 컴포넌트 내부에서 값이 변할 수 있는 데이터를 의미하며, 리렌더링을 통해 UI가 자동으로 업데이트된다.
const [state, setState] = useState(initialState);
가장 기본 형태는 위와 같다.
배열 구조 분해를 통해 현재 상태값과 상태를 업데이트하는 함수를 제공한다.
state = 현재 상태값
setState = 상태 업데이트 함수
initialState = 초기값
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName,
}));
};
useState를 사용할 때 주의해야 할 점은 바로 불변성 유지이다.
배열이나 객체 상태를 관리할 때, state는 불변성을 유지해야 한다.
React는 상태 변경이 감지되었을 때, 컴포넌트를 리렌더링 하게 된다.
이때 상태 변경 여부는 얕은 비교를 기반으로 하게 된다.
배열이나 객체의 경우, 참조가 변경되었는지를 기준으로 변경하게 되는 것이다.
배열이나 객체를 직접적으로 수정하게 되면 참조가 동일하게 유지되기 때문에 React는 상태 변경을 감지하지 못한다.
따라서 기존의 상태를 직접 변경하는 것이 아니라 새로운 상태를 변경해야 복사해야 한다.
② useReducer
React에서 상태 관리를 할 때 사용하는 Hook 중 하나로, 복잡한 상태 로직을 더 효율적으로 관리할 수 있도록 도와준다.
useState와 유사하지만, 상태 변경이 복잡하거나 여러 단계로 나뉘는 경우에 유용하다.
특히 여러 하위 값을 포함하는 객체를 다루거나, 다음 상태가 이전 상태에 의존적인 경우에 유용하다.
Redux 패턴과 유사하게 액션을 디스패치하여 상태를 업데이트한다.
const [state, dispatch] = useReducer(reducer, initialState);
기본적으로 useReducer는 리듀서 함수와 현재 상태 및 디스패치 함수를 반환한다.
reducer = 상태 변경 로직을 담고 잇는 함수
initialState = 초기값
state = 현재 상태 값
despatch = 상태를 업데이트하는 데 사용하는 함수, 액션을 리듀서로 전달한다.
import React, { useReducer } from 'react';
// 리듀서 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const Counter = () => {
const initialState = { count: 0 };
// useReducer를 사용해 상태와 디스패치 함수 정의
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
};
export default Counter;
reducer 함수는 상태를 어떻게 변경할지 정의하는 함수이다.
현재 상태(state)와 액션(action) 두 가지 인자를 받으며, 새로운 상태를 반환한다.
despatch 함수는 상태를 업데이트하는 액션을 reducer 함수로 전달하는 역할을 한다.
간단한 상태를 관리하는 경우엔 useState가 더 적합하지만,
복잡한 상태는 관리해야 하는 경우엔 useReducer를 사용하는 것이 좋다.
③ useEffect
React 컴포넌트에서 부수 효과(side Effects)를 수행하기 위한 Hook이다.
부수효과란 컴포넌트가 렌더링 된 후 발생하는 비동기 작업, 데이터 가져오기, DOM 조작과 같은 작업을 말한다.
클래스 컴포넌트에서 사용하는 생명주기 메서드의 대체 방식으로,
함수형 컴포넌트에서 생명주기와 관련된 작업을 처리할 수 있게 도와준다.
useEffect(() => {
// 실행할 부수 효과
return () => {
// 정리 작업 (cleanup) - 선택적
};
}, [dependencyArray]);
useEffect는 위와 같은 형태로 사용된다.
첫 번째 인자에는 실행할 함수를 넣어준다.
함수는 컴포넌트 렌더링 후 실행되며, 반환되는 함수가 있다면 컴포넌트가 언마운트 되거나 업데이트되기 전 정리 작업으로 실행된다.
두 번째 인자에는 의존병 배열이 들어간다.
해당 배열에 포함된 값이 변경될 때만 useEffect가 실행된다.
만일 의존성 배열이 없다면 컴포넌트가 매번 렌더링 될 때마다 실행되니 주의해야 한다.
import React, { useState, useEffect } from 'react';
const ExampleComponent = () => {
const [count, setCount] = useState(0);
// count 상태가 변경될 때마다 실행되는 부수 효과
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // count가 변경될 때만 effect 실행
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};
* 정리 작업
useEffect에서 반환된 함수는 컴포넌트가 언마운트되거나 업데이트되기 전에 실행된다.
이를 통해 이벤트 리스너 제거, 타이머 해제, 비동기 작업 중단, 구독 해제 등의 작업을 수행할 수 있다.
정리 작업을 적절히 사용하지 않으면 메모리 누수나 불필요한 리소스 소비가 발생할 수 있으며 성능 문제로 이어지기도 한다.
컴포넌트 언마운트는 화면에서 사라지기 전 실행되는 작업을 말한다.
예를 들어 useEffect에서 타이머 설정, 이벤트 리스너 추가와 같은 작업을 했다고 가정해 보자.
여기서 위 작업들을 제대로 정리하지 않으면 불필요한 작업임에도 계속 참조되어 메모리 누수가 발생할 수 있다.
또한, 의존성 배열의 값이 변경되는 경우도 마찬가지이다.
정리 작업으로 이전의 부수 효과를 정리함으로써 불필요한 중복 작업을 막고, 애플리케이션의 성능을 최적화할 수 있다.
import React, { useState, useEffect } from 'react';
const TimerComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 정리 작업: 컴포넌트 언마운트 시 타이머 해제
return () => {
clearInterval(timer); // 타이머를 해제하여 메모리 누수 방지
};
}, []); // 의존성 배열이 빈 배열이므로 처음 렌더링될 때만 실행
return <div>Timer: {count}</div>;
};
④ useContext
React ContextAPI를 함수형 컴포넌트에서 쉽게 사용할 수 있게 해주는 Hook이다.
데이터를 일일이 props로 전달할 필요 없이, 컴포넌트 트리 안의 어떤 하위 컴포넌트에서도 데이터 접근할 수 있게 해 준다.
주로 전역 상태나 전역 데이터를 관리하는 데 사용된다.
*Props Drilling
상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 때 여러 계층의 중간 컴포넌트를 거쳐야 하는 문제를 말한다.
중간 컴포넌트들은 데이터를 사용하지 않아도 불필요하게 props를 전달하게 된다.
useContext를 사용하면 위와 같은 과정을 줄일 수 있게 된다.
import React, { useContext } from 'react';
// 1. Context 생성
const MyContext = React.createContext();
const MyComponent = () => {
// 2. Context 값 사용
const value = useContext(MyContext);
return <div>{value}</div>;
};
// 3. Provider로 감싸기
const App = () => {
return (
<MyContext.Provider value="Hello from Context">
<MyComponent />
</MyContext.Provider>
);
};
export default App;
createContext()를 통해 Context 객체를 생성한다.
useContext로 Context의 값을 구독하고 사용이 가능하다.
Provider은 Context의 값을 하위 컴포넌트에 전달하며, value 속성을 통해 전달할 값을 설정한다.
useContext는 Provicer로 감싸진 범위 내에서만 Context 데이터를 사용할 수 있다.
단, Context 값이 변경되면 해당 Context를 사용하는 모든 컴포넌트가 렌더링 된다.
따라서 자주 변경되는 값보다는 테마, 인증 정보 등 비교적 안정적인 값에 적합하다.
성능 최적화가 필요한 경우라면 컨텍스트를 분리하거나 메모이제이션을 사용할 수 있다.
⑤ useMemo
React에서 성능 최적화를 위해 사용하는 Hook이다.
특정 값이 바뀌기 전까지의 계산을 기억하여, 불필요한 연산을 방지하고 리렌더링 성능 최적화에 도움을 준다.
배열 필터링, 정렬, 대규모 데이터 처리처럼 비용이 많이 드는 계산이나
값이 자주 변경되지 않음에도 컴포넌트가 자주 리렌더링 될 때 사용하면 유용하다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
첫 번째 인자는 값을 계산하는 함수가, 두 번째 인자는 의존성 배열이 들어간다.
의존성 배열에 있는 값이 변경될 때만 해당 함수가 다시 실행된다.
만일 의존성 배열에 있는 값이 변경되지 않으면, 계산된 값을 재사용한다.
const expensiveValue = useMemo(() => {
console.log('복잡한 계산 수행 중...');
return count * 2;
}, [count]); // count가 변경될 때만 계산
위 예시처럼 useMemo를 사용하면, 의존성 배열 안에 있는 값인 count가 변경될 때만 계산된다.
의존성 배열을 기반으로 언제 계산을 해야 할지 결정되므로, 잘 관리해야 한다.
* useMemo 남용
useMemo는 렌더링 최적화 도구이지만, 모든 경우에 남용하면 오히려 성능이 저하될 수 있다.
useMemo의 경우 값이 변하지 않았을 때 이전에 계산된 결과를 재사용하는 방식으로 성능을 최적화한다.
이 과정에서 값을 기억하는데 필요한 추가 메모리가 사용된다.
따라서 만일 계산 비용이 적거나 연산이 간단하다면 오히려 useMemo를 사용하는 것이 부담될 수 있다.
⑥ useCallback
메모이제이션된 콜백 함수를 반환하는 Hook이다.
함수가 매번 새로 생성되지 않도록 이전 함수를 기억하고, 의존성 배열에 있는 값들이 변경될 때만 새로운 함수를 생성한다.
이로 인해 컴포넌트가 불필요하게 리렌더링 되는 것을 방지하고 성능을 최적화하는 데 도움을 준다.
주로 하위 컴포넌트로 전달되는 함수가 불필요하게 리렌더링되는 것을 방지하기 위해 사용된다.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
첫 번째 인자에는 메모이제이션 할 함수가 들어가고, 두 번째 인자에는 의존성 배열이 들어간다.
의존성 배열에 있는 값들이 변경될 때만 함수가 새로 생성된다.
import React, { useState, useCallback } from 'react';
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 함수 메모이제이션
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
handleClick 함수는 useCallback을 사용하여 메모이제이션 되기 때문에 count가 변경되어도 새로 생성되지 않는다.
만일 useCallback을 사용하지 않았다면, ParentComponent가 리렌더링 될 때마다 handleClick 함수는 새로 생성되고
ChildComponent도 불필요하게 리렌더링 되었을 것이다.
이처럼 useCallback은 주로 자식 컴포넌트에 함수를 prop으로 전달할 때 유용하다.
useMemo는 값의 메모이제이션을 위해 사용된다면, useCallback은 함수 메모이제이션을 위해 사용한다.
또한, useCallback도 useMemo와 마찬가지로 불필요하게 사용하면 오히려 좋지 않으니 반드시 필요한 경우에 사용해야 한다.
⑦ useRef
참조를 관리하기 위한 hook으로, DOM 요소에 접근하거나 상태변화 없이 데이터를 저장할 때 사용된다.
useRef는 컴포넌트가 리렌더링 될 때마다 초기화되지 않고, 동일한 참조 값을 유지한다.
이를 통해 렌더링 상관없이 유지되는 값을 관리하거나, DOM 요소에 직접 접근할 때 유용하다.
const myRef = useRef(initialValue);
ref의 객체의 .current 속성을 변경해도 리렌더링이 트리거 되지 않는다.
import React, { useRef } from 'react';
const InputFocus = () => {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // input 요소에 포커스를 설정
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
};
export default InputFocus;
input 태그에 ref 속성으로 inputRef를 전달하면, DOM 요소에 직접 접근할 수 있다.
.current를 사용하여 ref에 저장된 값에 접근할 수 있다.
useRef로 참조한 값은 변경되더라도 컴포넌트가 리렌더링 되지 않기 때문에, UI에 즉각 반영되지 않는다.
따라서 DOM 접근이나 렌더링에 영향을 미치지 않는 값을 저장할 때 사용하는 것이 좋다.
상태를 관리하여 화면에 보여주려면 useState를 사용하는 것이 좋다.
* 직접적인 DOM 조작
React는 선언적 프로그래밍 패러다임을 따르며, UI의 상태와 렌더링을 상태(state)와 props에 따라 자동으로 처리한다.
즉, React는 상태가 변경될 때마다 자동으로 컴포넌트를 리렌더링 하고, DOM을 업데이트한다.
이 과정에서 개발자가 직접 DOM을 조작할 필요가 없도록 설계되어 있다.
그러나, useRef를 사용해 직접 DOM을 조작하는 경우, React의 선언적 흐름을 벗어나 명령형 방식으로 코딩하게 된다.
이는 React가 관리하는 DOM 업데이트와 충돌할 수 있으며, 의도하지 않은 결과를 초래할 수 있다.
따라서 DOM을 직접적으로 조작하기 위해서는 신중하게 접근하는 것이 좋다.
React의 상태와 충돌하지 않도록 코드를 설계하고,
useEffect 같은 라이프사이클 훅을 사용하여 적절한 시점에만 DOM을 조작하도록 해야 한다.
이상 React Hooks에 대해 공부해봤다.
왜 훅 사용 규칙을 지켜야 하는지, useReducer, useEffect의 정리 작업 등
생각없이 사용하고 있었던 것들의 이유를 알 수 있는 시간이었다.
솔직히 이유 쓸 줄도 알고 어느정도 익숙해졌는데 그냥 넘길까 생각도 했었는데,
이왕 공부하는 김에 제대로 해보자 라는 생각으로 포스팅을 작성했다.
다 작성하고 나니까 진짜 하기 잘한것 같다.
안하고 넘겼으면 똑같이 이유도 모르고 사용했을텐데,, 다음 과제들도 시작하기 전에 개념부터 제대로 정리하고 들어가야겠다!
'front > react' 카테고리의 다른 글
[React] VirtualDOM (2) | 2024.10.02 |
---|