WebSquare5 vs React 15 생명주기 비교

뿌이·2025년 9월 26일

websquare5

목록 보기
3/3

React 개발자가 WebSquare5로 전환할 때 가장 어려워하는 부분이 생명주기 관리입니다. 두 기술은 서로 다른 철학과 접근 방식을 가지고 있지만, 각각의 생명주기를 정확히 이해하면 효과적인 전환이 가능합니다. 이번 글에서는 두 기술의 생명주기를 완전히 분석해보겠습니다.

React 15의 생명주기

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의 생명주기

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 15WebSquare5실행 시점주요 용도
초기화constructorinitScreen컴포넌트/화면 생성 시초기 설정, 상태 정의
마운트 직전componentWillMount-DOM 삽입 직전준비 작업 (SSR 포함)
마운트 완료componentDidMountonpageloadDOM 삽입 완료 후API 호출, 이벤트 등록
데이터 로드 완료-onInitDataCompleted초기 데이터 준비 완료UI 최종 초기화
Props 변경componentWillReceiveProps-props 변경 시외부 데이터 변경 대응
업데이트 판단shouldComponentUpdate수동 조건 확인상태/props 변경 시성능 최적화
업데이트 직전componentWillUpdate-리렌더링 직전업데이트 준비
업데이트 완료componentDidUpdateonvaluechange 이벤트리렌더링 완료 후부수 효과 처리
언마운트componentWillUnmountonpageunload제거 직전정리 작업

핵심 차이점

1. 자동 vs 수동 실행

React 15 - 자동 생명주기:

// setState 호출 한 번으로 모든 생명주기가 자동 실행
this.setState({ isAbilityOpen: 'T' });
// → shouldComponentUpdate 자동 호출
// → componentWillUpdate 자동 호출
// → render 자동 호출
// → componentDidUpdate 자동 호출

WebSquare5 - 이벤트 기반:

// 데이터 변경 시 이벤트만 발생, 개발자가 직접 처리
dcsearchMap1.set("isAbilityOpen", "T");
// → onvaluechange 이벤트 발생
// → 개발자가 직접 UI 업데이트 로직 실행
scwin.updateAbilityButton(); // 수동 호출

2. 이벤트 바인딩

React 15 - 자동 바인딩:

class Component extends React.Component {
  componentDidMount() {
    // 자동으로 호출됨
  }
}

WebSquare5 - 명시적 바인딩:

<!-- XML에서 명시적으로 연결해야 함 -->
<body ev:onpageload="scwin.onpageload">
// JavaScript에서도 명시적으로 바인딩
dcsearchMap1.bind("onvaluechange", scwin.handleDataChange);

3. 상태 관리 방식

React 15:

  • this.state로 컴포넌트 내부 상태 관리
  • setState()로 상태 변경 시 자동 리렌더링
  • Immutable 업데이트 권장

WebSquare5:

  • DataMap, DataList로 상태 관리
  • set(), get() 메서드로 데이터 조작
  • 변경 시 이벤트 기반으로 수동 UI 업데이트

4. 성능 최적화

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();
};

실무 활용 패턴

React에서 WebSquare5로 전환 시 주의사항

  1. 생명주기 메서드 매핑:

    • componentDidMountonpageload
    • componentDidUpdateonvaluechange 이벤트 핸들러
    • componentWillUnmountonpageunload
  2. 이벤트 바인딩 필수:

    // React (자동)
    componentDidMount() { /* 자동 호출 */ }
    
    // WebSquare5 (수동)
    scwin.onpageload = function() { /* 정의 */ };
    // + XML에서 ev:onpageload="scwin.onpageload" 연결 필수
  3. 수동 UI 업데이트:

    // React (자동)
    this.setState({ data: newData }); // 자동 리렌더링
    
    // WebSquare5 (수동)
    dataMap.set("data", newData); // 데이터만 변경
    scwin.updateUI(); // 수동으로 UI 업데이트 호출

결론

WebSquare5와 React 15는 각각 고유한 생명주기 패러다임을 가지고 있습니다:

  • React 15: 자동화된 생명주기 시스템, 선언적 UI 업데이트
  • WebSquare5: 이벤트 기반 생명주기, 명시적 제어와 수동 업데이트

React의 자동화된 시스템에 익숙한 개발자들에게는 WebSquare5의 수동 제어 방식이 번거로울 수 있지만, 이를 통해 더 세밀한 제어와 성능 튜닝이 가능합니다. 각 기술의 철학을 이해하고 적절한 패턴을 활용한다면, 두 기술 모두에서 효과적인 애플리케이션을 개발할 수 있습니다.

profile
기록이 쌓이면 지식이 된다.

0개의 댓글