[React] 라이프사이클
이번 주차에 리액트 라이프사이클에 대해 공부하였습니다. 다시 한번 정리하며, 이전 프로젝트에서 이를 활용했던 사례를 공유하고자 합니다.
라이프 사이클
Life Cycle; 컴포넌트가 생성(Mounting), 업데이트(Updating), 소멸(Unmounting)의 과정을 거쳐 어떻게 동작하고 관리되는지
클래스형 컴포넌트
생성 단계
1. constructor
- 클래스 컴포넌트를 초기화하는 함수
constructor(props) {
super(props);
this.state = { count: 0 };
}
2. getDerivedStateFromProps
- props로부터 state를 동기화하고 싶을 때 사용
static getDerivedStateFromProps(props, state) {
if (props.someValue !== state.someValue) {
return { someValue: props.someValue };
}
return null;
}
❓ 왜 getDerivedStateFromProps 메서드는 static 인가 ❓
1. 클래스 인스턴스 없이 호출 가능
해당 함수는 리액트가 컴포넌트를 렌더링하기 전에 호출하며, 컴포넌트 인스턴스에 의존하지 않고 동작한다. 즉, this를 사용할 필요가 없기 때문에 static으로 선언된다.
2. 부작용 방지
해당 함수는 props와 state를 비교하며 새로운 상태를 반환하는 역할만 수행한다. DOM 조작이나 네트워크 요청 등 부작용을 허용하지 않기 때문에 static으로 선언한다. (static 메서드는 컴포넌트의 인스턴스(this)에 접근할 수 없기 때문에 DOM 조작이나 네트워크 요청 수행이 불가하다.)
3. render
- 컴포넌트를 화면에 렌더링하기 위한 메서드
render() {
return <div>{this.state.count}</div>;
}
4. componentDidMount
- 컴포넌트가 화면에 렌더링된 직후 호출
- 데이터 요청, DOM 조작 등에 사용
componentDidMount() {
console.log("did mount");
}
업데이트 단계
1. getDerivedStateFromProps
2. shouldComponentUpdate
- 컴포넌트를 리렌더링할지 결정
- true를 반환하면 리렌더링, false는 리렌더링 방지
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count;
}
3. render
4. getSnapshotBeforeUpdate
- DOM이 업데이트되기 직전에 호출
- 이전 상태를 저장하고, 반환값을 componentDidUpdate에서 받음
getSnapshotBeforeUpdate(prevProps, prevState) {
return { scrollPosition: window.scrollY };
}
5. componentDidUpdate
- DOM이 업데이트된 후 호출
- 네트워크 요청, DOM 조작에 사용
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot) {
console.log("이전 스크롤 위치:", snapshot.scrollPosition);
}
}
소멸 단계
1. componentWillUnmount
- 컴포넌트가 DOM에서 제거되기 직전에 호출
componentWillUnmount() {
console.log("unmount.");
}
함수형 컴포넌트
생성 단계
useEffect(() => {
console.log("useEffect -> mount");
}, []); // 두 번째 인자에 빈 배열
업데이트 단계
useEffect(() => {
console.log("count 값 변경:", count);
}, [count]); // 마운트와 count가 변경될 때마다 실행
소멸 단계
useEffect(() => {
const timer = setInterval(() => {
console.log("타이머 실행");
}, 1000);
return () => {
clearInterval(timer); // 타이머 정리
console.log("컴포넌트가 언마운트되었습니다.");
};
}, []);
적용 사례
웹 푸시 알림을 구현하기 위해 권한을 요청하고, FCM(Firebase Cloud Messaging) 토큰을 발급받는 작업을 진행한 경험이 있습니다. Firebase 프로젝트를 초기화하고 서비스 워커를 등록한 후, 사용자의 알림 권한을 요청하여 고유한 FCM 토큰을 발급받는 기능을 구현하였고, 이를 재사용 가능하도록 모듈화하여 작성하였습니다.
초기 설계 당시, React의 useEffect를 활용하여 컴포넌트가 처음 마운트될 때 requestFirebaseToken 함수를 실행하도록 구성하였습니다. 이를 통해 FCM 토큰은 애플리케이션 실행 시 한 번만 발급되어, 이후 불필요한 네트워크 요청을 방지할 수 있었습니다. (FCM 토큰이 클라이언트를 식별하는 고유한 값으로, 발급 후 주기적으로 갱신되기 때문에 매번 요청할 필요가 없다고 판단)
(실제 프로젝트 최종 설계에서는 API 호출 직전에 토큰을 가져오도록 하였으나, 초기 설계 당시 useEffect를 사용하여 마운트 될 때 해당 작업을 수행하도록 하였습니다. 초기 설계 내용이 생명 주기와 연관된 내용이었기에 복기를 위하여 이를 기반으로 작성하였습니다. 실제 FCM 토큰을 한번만 가져오는지 확인하기 위해 아래와 같이 UI를 변경하여 테스트 진행하였습니다.)
import { initializeApp } from "firebase/app";
import { getMessaging, getToken } from "firebase/messaging";
// Firebase Config 값
const firebaseConfig = {
apiKey: "~",
authDomain: "~",
projectId: "~",
storageBucket: "~",
messagingSenderId: "~",
appId: "~",
measurementId: "~"
};
// FirebaseApp 초기화
const firebaseApp = initializeApp(firebaseConfig);
// Messaging service
export const messaging = getMessaging(firebaseApp);
// 권한 요청 및 FCM 토큰 가져오기
export const requestFirebaseToken = async () => {
try {
const permission = await Notification.requestPermission();
if (permission === "granted") {
const registration = await navigator.serviceWorker.register("./firebase-messaging-sw.js");
console.log("Service Worker 등록 완료:", registration);
const token = await getToken(messaging, {
vapidKey: process.env.REACT_APP_VAPID_KEY,
serviceWorkerRegistration: registration,
});
console.log(`푸시 토큰 발급 완료: ${token}`);
return token; // 반환된 푸시 토큰
} else {
console.warn("푸시 권한이 거부되었습니다.");
}
} catch (error) {
console.error("푸시 토큰 요청 중 에러 발생:", error.message);
throw new Error("푸시 토큰 요청 실패");
}
};
import React, { useEffect, useState } from "react";
import { requestFirebaseToken } from "./firebaseConfig";
import "./App.css";
const App = () => {
const [fcmToken, setFcmToken] = useState(""); // FCM 토큰 상태
const [counter, setCounter] = useState(0); // 상태 변경 확인을 위한 카운터
useEffect(() => {
const fetchFirebaseToken = async () => {
try {
const token = await requestFirebaseToken();
console.log("FCM Token (마운트 시 실행):", token);
setFcmToken(token); // FCM 토큰을 상태에 저장
} catch (error) {
console.error("FCM Token 요청 중 오류:", error);
}
};
fetchFirebaseToken();
}, []);
const maskToken = (token) => {
if (token && token.length > 10) {
const firstPart = token.slice(0, 6);
const lastPart = token.slice(-4);
return `${firstPart}*****${lastPart}`;
}
return token;
};
return (
<div>
<header>
<h1>FCM Notification Test</h1>
</header>
<main>
<p>
이 앱은 Firebase Cloud Messaging(FCM)을 테스트하기 위한 React 애플리케이션입니다.
</p>
<p>
아래 버튼을 눌러 상태를 업데이트해도 FCM 토큰 요청은 마운트 시 한 번만 실행됩니다.
</p>
<button onClick={() => setCounter(counter + 1)}>
상태 변경 (카운터: {counter})
</button>
<div className="token-container">
<h3>FCM Token</h3>
{fcmToken ? (
<p>{maskToken(fcmToken)}</p>
) : (
<p className="loading">토큰을 요청 중입니다. 잠시만 기다려 주세요...</p>
)}
</div>
</main>
<footer>© 2025 Firebase Notification Test App</footer>
</div>
);
};
export default App;