안녕하세요 OB 김건휘입니다🌊. 이번 시간에는 useEffect 훅을 딥다이브 해보는 시간을 가져보도록 하겠습니다.
📌Effect란?
즉, effect는 컴포넌트가 렌더링된 이후 특정 작업을 수행하기 위한 도구를 의미한다. 이를 구현하기 위해 useEffect 훅을 사용하며, 이는 리액트 함수형 컴포넌트에서 부작용(side effect)을 처리하는 방식이다.
📌부작용(Side Effect)이란?
부작용은 컴포넌트의 렌더링 외에 발생하는 모든 작업을 의미한다.
- API 호출 (데이터 가져오기/저장하기)
- 구독 설정/해제 (실시간 통신, 이벤트 리스너 등)
- DOM 수정 (DOM 직접 접근)
- 타이머 설정 (setInterval이나 setTimeout 사용)
🧐왜 useEffect의 호출을 두 번 수행할까?
//App.jsx
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>채팅에 오신걸 환영합니다!</h1>;
}
//Chat.js
export function createConnection() {
// 실제 구현은 정말로 채팅 서버에 연결하는 것이 되어야 합니다.
return {
connect() {
console.log('✅ 연결 중...');
},
disconnect() {
console.log('❌ 연결이 끊겼습니다.');
}
};
}
이 Effect는 마운트될 때만 실행되므로 콘솔에 ”✅ 연결 중…”이 한 번 출력될 것으로 예상할 수 있다. 그러나 콘솔을 확인해 보면 ”✅ 연결 중…”이 두 번 출력된다.
✔️이유
React의 Strict Mode 때문이다. Strict Mode는 React 개발 중 일반적인 오류를 감지하고 피할 수 있도록 돕기 위해 특정 컴포넌트 메서드를 의도적으로 두 번 호출한다. 왜냐하면, 사이드 이펙트를 감지하고 정리(cleanup) 작업이 올바르게 작동하는지 확인하기 위해서이다.
Strict Mode가 무엇인지 궁금하다면?
위의 코드에서 ChatRoom 컴포넌트는 다음 과정을 거친다:
- 첫 번째 마운트:
- useEffect가 실행되고, createConnection으로 연결을 생성한 뒤 connection.connect()가 호출.
- "✅ 연결 중..."이 콘솔에 출력.
- Strict Mode로 인해 컴포넌트가 언마운트되고 다시 마운트:
- React는 마운트 → 언마운트 → 다시 마운트 과정을 실행.
- 이 과정에서 useEffect가 다시 실행되며, 또다시 createConnection과 connection.connect()가 호출.
- 결과적으로 "✅ 연결 중..."이 두 번 출력.
✔️올바른 clean up 함수를 추가한 코드
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
// Cleanup: 연결 해제
return () => {
connection.disconnect();
};
}, []);
return <h1>채팅에 오신걸 환영합니다!</h1>;
}
콘솔 로그:
- "✅ 연결 중..."
- "❌ 연결 해제됨"
- "✅ 연결 중..."
연결 중 => 컴포넌트 언마운트 => 연결 해제 => 컴포넌트 마운트 => 연결 중가 올바른 useEffect의 동작이다.
✔️결론
의도적으로 useEffect를 두 번 실행하여 정리(cleanup) 코드가 제대로 작동하는지 확인한다. 배포 환경에서는 한번만 출력된다.
=> 개발자가 동작이 제대로 수행되는지 확인하도록 개발환경에서만 2번 실행한다.
📌useEffect를 남용하면 안되는 이유
무분별하게 Effect를 추가하면 성능 문제, 코드 복잡성 증가, 유지보수 어려움 등의 문제가 발생할 수 있다. 따라서 useEffect를 도입하기 전에 정말 필요한지를 고민하고, 의존성을 명확히 정의하며, 책임 분리를 통해 사용해야 한다.
📌Effect 안에서 fetch 호출을 작성하는 것의 단점
흔히, 우리들이 가장 많이 사용하는 방법이다. 즉, useEffect로 서버 데이터를 패칭해오고, 이를 useState로 관리하게 된다.
하지만 다른 여러가지 클라이언트 상태를 관리할 때도 useState를 사용하게 되며, 결국에 useState 를 통해서 서버 상태와 클라이언트 상태를 “모두” 관리하게 되는것이다. => 이렇게 되면 서버 상태를 관리할 때에도 컴포넌트의 생명주기를 고려해야하고( 필수적으로 사용하는 서버 데이터 캐싱 같은 전략을 구현하기 어렵다), 클라이언트 상태와 분리해서 관리할 수 없으니 코드가 길어지고 지저분해진다.(다들 공감할 내용일 것이다)
📌그래서 우리는 React Query를 공부해야하고, 사용해야 한다.
리액트 공식문서에서도 대안으로 React Query를 언급하고 있고, React Query를 사용하게 되면 useEffect를 사용하여 데이터 패칭을 하였을 때의 단점(부작용)들을 모두 겪지 않을 수 있다.
🧐어떻게 useEffect를 사용하지 않고 데이터 패칭을 할 수 있는거야?
해당 스터디는 React Query 스터디가 아니기 때문에 간단하게만 언급해보면, React-Query의 useQuery를 사용하면 다음과 같은 경우, 기본 설정으로 인해 자동으로 데이터를 다시 가져오기 때문이다. (이 설정은 변경가능!)
- 동일한 쿼리를 사용하는 새로운 컴포넌트가 마운트 될 때
- 브라우저 창이 포커스 될 때
- 네트워크가 다시 연결 될 때
- 선택적으로 리패칭하는 간격을 설정 했을 때
react query는 API 상태와 관련된 다양한 데이터를 제공하여 복잡한 구현과 설계 없이도 개발자가 효율적으로 화면을 구성할 수 있게끔 도와준다.
📌custom Hook이란?
리액트의 기본 제공 훅(Hook)을 사용자가 자신의 요구에 맞게 만든 사용자 정의 훅을 의미한다. 즉, 반복되는 로직을 리액트 내장 훅 들을 사용하여 구현한 '내가 만든(커스텀한) 훅'이다.
📌custom Hook을 사용하는 이유
- 코드 재사용성 향상: Custom Hook을 사용하면 상태 관리나 사이드 이펙트 관리 같은 로직을 여러 컴포넌트에서 쉽게 재사용할 수 있다. 이는 코드 중복을 줄이고 프로젝트의 유지보수성을 높여준다.
- 컴포넌트 간소화: 복잡한 로직을 컴포넌트 밖으로 분리함으로써, 컴포넌트 자체는 더 간결하고 명확해진다. => UI를 처리하는 컴포넌트와 기능적인 로직을 분리하여서 의존성을 제거할 수 있다는게 key point!
- 사용자 정의 로직의 모듈화: useState, useEffect, useContext 같은 리액트의 내장 훅을 조합하여 만들 수 있다. Custom Hook을 통해 특정 동작이나 로직을 모듈화하여, 애플리케이션 전반에 걸쳐 일관된 방식으로 기능을 제공할 수 있다.
📌custom Hook 네이밍 규칙
커스텀 훅의 이름은 "use"로 시작해야 한다. ex)useFetchData, useCounter
📌custom Hook 사용시 주의사항
- 훅(hook)은 함수의 최상위 레벨에서만 호출해야 한다: 훅은 반복문, 조건문, 중첩된 함수 내부에서 호출하면 안된다. 이 규칙을 지키면 훅의 호출 순서가 보장되어 훅의 내부 상태가 올바르게 유지된다.
- 훅(hook)은 리액트 함수 컴포넌트 내에서만 호출되어야 한다: 훅은 리액트의 함수 컴포넌트 또는 다른 훅 내부에서만 호출될 수 있다. 일반 JavaScript 함수에서 훅을 호출하면 안된다.
📌책임 분리 원칙을 적용한 컴포넌트 만들기
개인적으로 custom Hook을 사용하여서 로직을 분리하는 것을 굉장히 좋아한다.(코드 가독성 증가, 코드 복잡성 감소) 아직 경험해보지 못했다면 꼭! 경험해보길.
특히, 컴포넌트를 설계할 때 "책임 분리 원칙"을 적용한 컴포넌트를 설계하여야 한다는 말을 들어본적이 있을 것이다. 간단하게 설명하면, 각 컴포넌트가 하나의 역할에 집중하도록 설계하는 것이다. 이 때, custom Hook을 활용한다면 로직과 UI를 분리하여, 컴포넌트는 UI를 띄워주는 역할만하고, 상태관리와 비지니스 로직 처리는 custom Hook이 담당하게 할 수 있다.
//useSearch.js
import { useState, useEffect } from 'react';
function useSearch(query, apiEndpoint) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!query) return;
setIsLoading(true);
fetch(`${apiEndpoint}?q=${query}`)
.then(res => res.json())
.then(data => {
setResults(data);
setIsLoading(false);
});
}, [query, apiEndpoint]);
return { results, isLoading };
}
//SearchBar.jsx
function SearchBar({ apiEndpoint }) {
const [query, setQuery] = useState('');
const { results, isLoading } = useSearch(query, apiEndpoint);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isLoading && <p>Loading...</p>}
<ul>
{results.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
책임 분리 적용
- Custom Hook (useSearch): 검색 로직을 담당.
- UI 컴포넌트 (SearchBar): 사용자 입력과 결과 표시 UI만 담당.
🧐궁금한 점
저는 개인적으로 custom Hook을 분리할 수 있는 것은 최대한 분리하는 것을 좋아합니다. 위에서 언급했듯이, 책임 분리 적용 뿐만 아니라 custom Hook을 사용하면 훨씬 컴포넌트 코드가 깔끔하게 느껴지더라구요. 다들 custom Hook을 만들 때, 기준이 있는지 궁금합니다.
'나야, 리액트 스터디' 카테고리의 다른 글
[Week7] Effect로 동기화하기 (2) | 2024.12.08 |
---|---|
[week7] Effect& custom Hook (4) | 2024.12.08 |
[Week 6] Ref (1) | 2024.12.02 |
[week6]탈출구 - Ref로 값 참조하기, Ref로 DOM조작하기 (2) | 2024.12.01 |
[week 6] useRef 뿌시기 (3) | 2024.12.01 |