React 개발자가 WebSquare5로 전환할 때 가장 어려워하는 부분이 생명주기 관리입니다. 두 기술은 서로 다른 철학과 접근 방식을 가지고 있지만, 각각의 생명주기를 정확히 이해하면 효과적인 전환이 가능합니다. 이번 글에서는 두 기술의 생명주기를 완전히 분석해보겠습니다.
React 15는 컴포넌트의 전체 생명주기를 세밀하게 제어할 수 있는 8개의 생명주기 메서드를 제공합니다.
class UserDashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
isAbilityOpen: 'F',
userData: null,
loading: true
};
console.log('1. constructor - 컴포넌트 인스턴스 생성');
}
// === 마운팅 단계 ===
componentWillMount() {
console.log('2. componentWillMount - DOM 삽입 직전');
// 서버 사이드 렌더링에서도 호출됨
this.prepareInitialData();
}
componentDidMount() {
console.log('3. componentDidMount - DOM 삽입 완료');
// DOM 조작, API 호출, 이벤트 리스너 등록
this.fetchUserData();
this.setupEventListeners();
this.initializeThirdPartyLibraries();
}
// === 업데이트 단계 ===
componentWillReceiveProps(nextProps) {
console.log('4. componentWillReceiveProps - 새 props 수신');
if (nextProps.userId !== this.props.userId) {
this.setState({ loading: true });
this.fetchUserData(nextProps.userId);
}
}
shouldComponentUpdate(nextProps, nextState) {
console.log('5. shouldComponentUpdate - 리렌더링 필요성 판단');
// 성능 최적화를 위한 조건부 렌더링
return nextState.isAbilityOpen !== this.state.isAbilityOpen ||
nextState.userData !== this.state.userData ||
nextProps.userId !== this.props.userId;
}
componentWillUpdate(nextProps, nextState) {
console.log('6. componentWillUpdate - 업데이트 직전');
// DOM 업데이트 전 준비 작업
this.saveScrollPosition();
}
componentDidUpdate(prevProps, prevState) {
console.log('7. componentDidUpdate - 업데이트 완료');
// DOM 업데이트 후 처리
if (prevState.isAbilityOpen !== this.state.isAbilityOpen) {
console.log(`역량평가 상태 변경: ${prevState.isAbilityOpen} → ${this.state.isAbilityOpen}`);
this.notifyStatusChange();
this.updateThirdPartyComponents();
}
if (prevProps.userId !== this.props.userId) {
this.restoreScrollPosition();
}
}
// === 언마운팅 단계 ===
componentWillUnmount() {
console.log('8. componentWillUnmount - 컴포넌트 제거 직전');
// 정리 작업
clearInterval(this.statusTimer);
clearTimeout(this.debounceTimer);
document.removeEventListener('scroll', this.handleScroll);
this.websocket?.close();
this.cleanup();
}
// === 비즈니스 로직 ===
fetchUserData = async (userId = this.props.userId) => {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
this.setState({ userData, loading: false });
} catch (error) {
console.error('사용자 데이터 로드 실패:', error);
this.setState({ loading: false });
}
}
handleAbilityToggle = () => {
this.setState(prevState => ({
isAbilityOpen: prevState.isAbilityOpen === 'F' ? 'T' : 'F'
}));
// setState 호출 → 자동으로 생명주기 메서드들이 순차적으로 실행됨
}
render() {
console.log('render - UI 렌더링');
if (this.state.loading) {
return <div>로딩 중...</div>;
}
const buttonText = this.state.isAbilityOpen === 'F' ? '역량평가 OPEN' : '역량평가 CLOSE';
return (
<div className="user-dashboard">
<h1>{this.state.userData?.name}님의 대시보드</h1>
<button onClick={this.handleAbilityToggle} className="ability-btn">
{buttonText}
</button>
<div className="user-info">
{this.state.userData?.department}
</div>
</div>
);
}
}
WebSquare5는 React와는 다른 생명주기 구조를 가지고 있습니다. 페이지 기반의 생명주기와 명시적 이벤트 바인딩이 특징입니다.
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:w2="http://www.inswave.com/websquare"
xmlns:xf="http://www.w3.org/2002/xforms">
<head>
<script type="text/javascript">
// ===== WebSquare5 생명주기 메서드들 =====
// 1. 화면 초기화 (가장 먼저 실행)
scwin.initScreen = function() {
console.log('1. initScreen - 화면 구조 초기화');
// 화면 레이아웃, 초기 설정
scwin.setupInitialConfig();
scwin.initializeDataMaps();
};
// 2. 페이지 로드 완료 (React의 componentDidMount와 유사)
scwin.onpageload = function() {
console.log('2. onpageload - 페이지 DOM 로드 완료');
// React의 componentDidMount에서 하던 작업들
scwin.setupEventListeners();
scwin.initializeUI();
scwin.loadInitialData();
// 초기 상태 설정
dcsearchMap1.set("isAbilityOpen", "F");
};
// 3. 초기 데이터 로드 완료 (WebSquare5만의 고유 생명주기)
scwin.onInitDataCompleted = function() {
console.log('3. onInitDataCompleted - 초기 데이터 로딩 완료');
// 모든 초기 데이터가 준비된 후 실행
scwin.finalizeInitialization();
scwin.updateInitialUI();
scwin.hideLoadingIndicator();
};
// 4. 페이지 언로드 (React의 componentWillUnmount와 유사)
scwin.onpageunload = function() {
console.log('4. onpageunload - 페이지 언로드 직전');
// React의 componentWillUnmount에서 하던 정리 작업들
if (scwin.statusTimer) {
clearInterval(scwin.statusTimer);
}
if (scwin.debounceTimer) {
clearTimeout(scwin.debounceTimer);
}
// 이벤트 리스너 해제
if (dcsearchMap1) {
dcsearchMap1.unbind("onvaluechange", scwin.handleDataChange);
}
if (dcUserData) {
dcUserData.unbind("onsuccess", scwin.onUserDataSuccess);
dcUserData.unbind("onerror", scwin.onUserDataError);
}
scwin.cleanup();
scwin.closeWebSocketConnection();
};
// ===== 이벤트 핸들러들 (React의 이벤트 핸들러 + 일부 생명주기 역할) =====
// 데이터 변경 감지 (React의 componentDidUpdate 역할 일부 담당)
scwin.handleDataChange = function(info) {
console.log('데이터 변경 감지:', info.colID, info.oldValue, '→', info.newValue);
// React의 componentDidUpdate처럼 이전 값과 비교
if (info.colID === "isAbilityOpen" && info.oldValue !== info.newValue) {
console.log('역량평가 상태 실제 변경됨');
scwin.updateAbilityButton();
scwin.notifyStatusChange();
scwin.updateThirdPartyComponents();
// 조건부 업데이트 (React의 shouldComponentUpdate 역할)
if (scwin.shouldUpdateRelatedComponents(info)) {
scwin.updateRelatedUI();
}
}
};
// 사용자 데이터 로드 성공 (비동기 데이터 처리)
scwin.onUserDataSuccess = function(requestId, data) {
console.log('사용자 데이터 로드 성공');
scwin.processUserData(data);
scwin.updateUserInfo();
scwin.hideLoadingIndicator();
};
// 사용자 데이터 로드 실패 (에러 처리)
scwin.onUserDataError = function(requestId, error) {
console.error('사용자 데이터 로드 실패:', error);
scwin.showErrorMessage('데이터를 불러오는 중 오류가 발생했습니다.');
scwin.hideLoadingIndicator();
};
// ===== 비즈니스 로직 메서드들 =====
scwin.setupInitialConfig = function() {
console.log('초기 설정 구성');
// 전역 설정, 상수 정의 등
};
scwin.initializeDataMaps = function() {
console.log('데이터맵 초기화');
// 데이터맵 구조 설정
};
scwin.setupEventListeners = function() {
console.log('이벤트 리스너 등록');
// WebSquare5에서는 명시적으로 이벤트를 바인딩해야 함
dcsearchMap1.bind("onvaluechange", scwin.handleDataChange);
dcUserData.bind("onsuccess", scwin.onUserDataSuccess);
dcUserData.bind("onerror", scwin.onUserDataError);
// 사용자 정의 이벤트들
$p.getComponentById("refreshBtn").bind("onclick", scwin.handleRefresh);
};
scwin.loadInitialData = function() {
console.log('초기 데이터 로딩 시작');
scwin.showLoadingIndicator();
// 비동기 데이터 로딩
var submission = $p.getSubmission("getUserDataSubmission");
submission.submit();
};
scwin.updateAbilityButton = function() {
var value = dcsearchMap1.get("isAbilityOpen");
var text = value === 'F' ? '역량평가 OPEN' : '역량평가 CLOSE';
var anchor = $p.getComponentById("abilityButton");
if (anchor) {
anchor.setLabel(text);
// 상태에 따른 스타일 변경
var buttonClass = value === 'F' ? 'btn-open' : 'btn-close';
anchor.setClass(buttonClass);
}
};
scwin.shouldUpdateRelatedComponents = function(info) {
// React의 shouldComponentUpdate와 같은 최적화 로직
if (info.colID === "isAbilityOpen") {
return true;
}
if (info.colID === "userData" && info.newValue?.department !== info.oldValue?.department) {
return true;
}
return false;
};
scwin.handleAbilityClick = function() {
console.log('역량평가 버튼 클릭');
var currentValue = dcsearchMap1.get("isAbilityOpen");
var newValue = currentValue === 'F' ? 'T' : 'F';
// 데이터 변경 (React의 setState와 유사하지만 수동 처리)
dcsearchMap1.set("isAbilityOpen", newValue);
// WebSquare5에서는 set() 호출 시 자동으로 onvaluechange 이벤트 발생
};
scwin.handleRefresh = function() {
console.log('새로고침 버튼 클릭');
scwin.loadInitialData();
};
// ===== 유틸리티 메서드들 =====
scwin.showLoadingIndicator = function() {
var loader = $p.getComponentById("loadingIndicator");
if (loader) loader.setVisible(true);
};
scwin.hideLoadingIndicator = function() {
var loader = $p.getComponentById("loadingIndicator");
if (loader) loader.setVisible(false);
};
scwin.notifyStatusChange = function() {
console.log('상태 변경 알림 전송');
// 웹소켓이나 이벤트 버스를 통한 알림
};
scwin.updateThirdPartyComponents = function() {
console.log('서드파티 컴포넌트 업데이트');
// 외부 라이브러리 연동
};
scwin.cleanup = function() {
console.log('리소스 정리');
// 메모리 누수 방지
};
scwin.closeWebSocketConnection = function() {
if (scwin.websocket) {
scwin.websocket.close();
}
};
</script>
</head>
<!-- 중요: 생명주기 이벤트들을 명시적으로 연결해야 함 -->
<body ev:onpageload="scwin.onpageload"
ev:onpageunload="scwin.onpageunload">
<w2:dataCollection baseNode="map">
<!-- 사용자 검색 조건 -->
<w2:dataMap id="dcsearchMap1">
<w2:keyInfo>
<w2:key id="isAbilityOpen" name="isAbilityOpen" dataType="text"/>
<w2:key id="userId" name="userId" dataType="text"/>
</w2:keyInfo>
</w2:dataMap>
<!-- 사용자 정보 데이터 -->
<w2:dataMap id="dcUserData">
<w2:keyInfo>
<w2:key id="name" name="name" dataType="text"/>
<w2:key id="department" name="department" dataType="text"/>
<w2:key id="position" name="position" dataType="text"/>
</w2:keyInfo>
</w2:dataMap>
</w2:dataCollection>
<!-- 서버 통신 -->
<w2:submission id="getUserDataSubmission"
ref="data:json,dcUserData"
target="/api/getUserData"
ev:onsuccess="scwin.onUserDataSuccess"
ev:onerror="scwin.onUserDataError">
</w2:submission>
<!-- UI 컴포넌트들 -->
<div class="user-dashboard">
<w2:textbox id="userNameText" ref="dcUserData.name" style="font-weight:bold;">
</w2:textbox>
<w2:textbox id="departmentText" ref="dcUserData.department">
</w2:textbox>
<w2:anchor class="btn_cm ability-btn"
id="abilityButton"
ev:onclick="scwin.handleAbilityClick">
<xf:label>역량평가 OPEN</xf:label>
</w2:anchor>
<w2:anchor class="btn_refresh"
id="refreshBtn">
<xf:label>새로고침</xf:label>
</w2:anchor>
<w2:group id="loadingIndicator" style="display:none;">
<xf:label>로딩 중...</xf:label>
</w2:group>
</div>
</body>
</html>
| 단계 | React 15 | WebSquare5 | 실행 시점 | 주요 용도 |
|---|---|---|---|---|
| 초기화 | constructor | initScreen | 컴포넌트/화면 생성 시 | 초기 설정, 상태 정의 |
| 마운트 직전 | componentWillMount | - | DOM 삽입 직전 | 준비 작업 (SSR 포함) |
| 마운트 완료 | componentDidMount | onpageload | DOM 삽입 완료 후 | API 호출, 이벤트 등록 |
| 데이터 로드 완료 | - | onInitDataCompleted | 초기 데이터 준비 완료 | UI 최종 초기화 |
| Props 변경 | componentWillReceiveProps | - | props 변경 시 | 외부 데이터 변경 대응 |
| 업데이트 판단 | shouldComponentUpdate | 수동 조건 확인 | 상태/props 변경 시 | 성능 최적화 |
| 업데이트 직전 | componentWillUpdate | - | 리렌더링 직전 | 업데이트 준비 |
| 업데이트 완료 | componentDidUpdate | onvaluechange 이벤트 | 리렌더링 완료 후 | 부수 효과 처리 |
| 언마운트 | componentWillUnmount | onpageunload | 제거 직전 | 정리 작업 |
React 15 - 자동 생명주기:
// setState 호출 한 번으로 모든 생명주기가 자동 실행
this.setState({ isAbilityOpen: 'T' });
// → shouldComponentUpdate 자동 호출
// → componentWillUpdate 자동 호출
// → render 자동 호출
// → componentDidUpdate 자동 호출
WebSquare5 - 이벤트 기반:
// 데이터 변경 시 이벤트만 발생, 개발자가 직접 처리
dcsearchMap1.set("isAbilityOpen", "T");
// → onvaluechange 이벤트 발생
// → 개발자가 직접 UI 업데이트 로직 실행
scwin.updateAbilityButton(); // 수동 호출
React 15 - 자동 바인딩:
class Component extends React.Component {
componentDidMount() {
// 자동으로 호출됨
}
}
WebSquare5 - 명시적 바인딩:
<!-- XML에서 명시적으로 연결해야 함 -->
<body ev:onpageload="scwin.onpageload">
// JavaScript에서도 명시적으로 바인딩
dcsearchMap1.bind("onvaluechange", scwin.handleDataChange);
React 15:
this.state로 컴포넌트 내부 상태 관리setState()로 상태 변경 시 자동 리렌더링WebSquare5:
set(), get() 메서드로 데이터 조작React 15:
shouldComponentUpdate(nextProps, nextState) {
// React가 자동으로 호출하여 렌더링 여부 결정
return nextState.data !== this.state.data;
}
WebSquare5:
scwin.handleDataChange = function(info) {
// 개발자가 직접 조건 확인하여 업데이트 제어
if (info.oldValue === info.newValue) {
return; // 불필요한 업데이트 방지
}
scwin.updateUI();
};
생명주기 메서드 매핑:
componentDidMount → onpageloadcomponentDidUpdate → onvaluechange 이벤트 핸들러componentWillUnmount → onpageunload이벤트 바인딩 필수:
// React (자동)
componentDidMount() { /* 자동 호출 */ }
// WebSquare5 (수동)
scwin.onpageload = function() { /* 정의 */ };
// + XML에서 ev:onpageload="scwin.onpageload" 연결 필수
수동 UI 업데이트:
// React (자동)
this.setState({ data: newData }); // 자동 리렌더링
// WebSquare5 (수동)
dataMap.set("data", newData); // 데이터만 변경
scwin.updateUI(); // 수동으로 UI 업데이트 호출
WebSquare5와 React 15는 각각 고유한 생명주기 패러다임을 가지고 있습니다:
React의 자동화된 시스템에 익숙한 개발자들에게는 WebSquare5의 수동 제어 방식이 번거로울 수 있지만, 이를 통해 더 세밀한 제어와 성능 튜닝이 가능합니다. 각 기술의 철학을 이해하고 적절한 패턴을 활용한다면, 두 기술 모두에서 효과적인 애플리케이션을 개발할 수 있습니다.