티스토리 뷰

Web/React

[React] 클래스형 컴포넌트

해구름 2021. 6. 23. 00:43
반응형

클래스형 컴포넌트는 함수형 컴포넌트와 Hook이 등장하기 이전에 사용하던 컴포넌트 형태로 ES6(ECMA Script 2015)부터 추가된 JavaScript Class를 바탕으로 컴포넌트를 작성합니다.

함수형 컴포넌트

  • 성능이나 메모리 사용에 있어 약간의 이점이 있음
  • Hook을 통해 state, lifeCycle을 구현함
  • 클래스형 컴포넌트에 비해 표현이 간결함

클래스형 컴포넌트

  • ES6(ECMA Script 2015)부터 추가된 Class를 통해 컴포넌트 선언
  • 함수형 컴포넌트와 Hook이 등장하기 전에 폭넓게 사용되었기 때문에, 레거시 코드에서 자주 발견할 수 있는 형태
  • 표현방법이 좀 더 명시적이고, 함수형 컴포넌트 보다 약간 기능이 더 많음

클래스형 컴포넌트 생명주기

Class 컴포넌트 생명주기

- 암기할 필요는 없지만 각 단계를 이해하고 있어야 하며 이 도표를 잘 보이는 곳에 붙여두고 활용하는 것이 좋음
- 자주 사용되는 생명주기 메서드는 볼드로 표시됨

마운트 단계 (Mounting)

컴포넌트 객체가 최초로 생성되어 DOM에 삽입될 때 한 번 실행됨

업데이트 단계 (Updating)

props, state가 변경될 때 마다 수행되며 변경사항을 화면에 표시함

소멸 단계 (Unmounting)

컴포넌트가 DOM상에서 제거될 때 한 번 호출 됨

오류 처리

위 생명주기에 나타나지 않았지만 오류가 발생하면 아래 메서드가 순차적으로 호출됨

  • static getDerivedStateFromError()
  • componentDidCatch()

생명주기 메서드

constructor()

  • 생성자로 props를 전달받아 state를 초기화해야 할 때 사용하는 메서드
    state를 초기화하지 않는다면 구현하지 않아도 됨
  • 반드시 첫 줄에 super(props);를 호출해야하며 그렇지 않은 경우 오류가 발생함
  • constructor()내부에서는 setState()메서드를 지원하지 않음 (무시됨, setState()는 컴포넌트가 마운트 된 이후에 사용가능)
class CounterComponent extends React.Component {
    constructor(props) {
      super(props);

      this.state = { counter: props.savedValue };
      this.handleClick = this.handleClick.bind(this);

      //constructor에서 setState()호출은 무시됩니다
      this.setState({ counter: 1 }); 
    }
}

constructor()를 Class Field 표현식으로 대체할 수 있음

class CounterComponent extends React.Component {
    state = { counter: this.props.savedValue };
    handleClick = () => {
    	console.log('handled');
    };
}

static getDerivedStateFromProps(props, state)

  • 렌더링 직전에 호출되면 props를 바탕으로 state를 수정해야할 때 사용
  • 최초 마운트 단계와 업데이트 단계에서 호출되며, props에 의존적인 state 값이 있을 때 유용함 (예를들어 props로부터 전달받은 값을 바탕으로 애니메이션을 출력할 때 사용할 수 있음)
  • null을 반환하면 아무 것도 갱신하지 않음
  • static 메서드 이기 때문에 this를 통해 컴포넌트에 접근할 수 없음
static getDerivedStateFromProps(props, state) {
   if (props.id !== state.id) {
      return {
          id: state.id, 
          valueChanged: true
      };
   }
   return null;
}

render()

  • 화면에 출력할 내용을 생성하여 반환
  • prpos와 state 값으로만 생성해야함
  • Side-Effect를 발생시켜서는 안됨

componentDidMount()

  • render()의 반환 값이 DOM에 반영된 후 호출됨
  • 컴포넌트가 DOM에 반영된 시점이므로 DOM으로부터 필요정보 조회가능
  • setState() 메서드 호출과 Side-Effect 코드 실행 가능
class BoxSize extends React.Component {
    state = { boxWidth: 0 };
    domRef = React.createRef();

    //Mount 된 후 DOM에 접근하여 정보를 얻을 수 있으며, setState()호출가능
    componentDidMount() {
        const boxSize = this.domRef.current.getBoundingClientRect();
        this.setState({ boxWidth: boxSize.width });
    }

    render() {
        const { boxWidth } = this.state;
        return (
            <div ref={this.domRef} style={{ width:'100%', height:'100px' }}>
                Box Width : {boxWidth}px
            </div>
        );
    }
}

bool shouldComponentUpdate(nextProps, nextState)

  • 렌더링 성능 최적화를 위한 메서드
    (렌더링을 막는 목적으로 사용하면 심각한 오류가 발생할 수 있음. froceUpdate()메서드는 shouldComponentUpdate()를 건너뛰고 렌더링을 강제시킬 수 있음)
  • false를 반환하면 이후의 render(), componentDidMount(), componentDidUpdate()가 호출되지 않음
    (함수를 생략하면 항상 true를 반환함)
class UserComponent extends React.Component {
    ...

    shouldComponentUpdate(nextProps, nextState) {
        //렌더링 과정을 진행 해야할지, 중단해야할지 결정
        return this.state.userId !== nextState.userId;
    }
}

getSnapshotBeforeUpdate(prevProps, prevState)

  • render()의 결과가 DOM에 반영되기 직전에 호출 됨
  • 이전 DOM의 정보를 조회할 수 있는 메서드
  • 이 메서드가 반환하는 값은 componentDidUpdate(prevProps, prevState, snapshot)의 세번째 인자에 전달됨

componentDidUpdate(prevProps, prevState, snapshot)

  • render()의 결과가 DOM에 반영된 이후에 호출됨
  • Side-Effect 코드, setState()를 호출 할 수 있음
  • 이전 prop, state를 전달받으므로 변경사항을 직접 비교할 수 있음
class UserProfile extends React.Component {
    componentDidMount() {
        this.fetchAndSetUserData(this.props.user.id);
    }
    componentDidUpdate(prevProps, prevState, snapshot) {
        if(prevProps.user.id !== this.props.user.id) {
            this.fetchAndSetUserData(this.props.user.id);
        }
    }
    fetchUserData(userId) {
        requestUserData(userId)
            .then(user => this.setState({ user }));
    }
}

static getDerivedStateFromError(error)

  • 자식 컴포넌트에서 오류가 발생했을 때 호출 (자신의 오류는 잡아낼 수 없음)
  • state에 병합될 값을 반환해야함

componentDidCatch(error, info)

  • 자식 컴포넌트에서 오류가 발생했을 때 호출 (자신의 오류는 잡아낼 수 없음)
  • 매개변수 error는 오류객체, info는 오류를 발생시킨 componentStatck 정보를 포함하는 객체
  • Commit 단계에서 호출되기 때문에 setState()와 Side-Effect 실행 가능
    (setState()를 호출하여 UI가 다시 렌더링 되도록 할 수 있지만 추후 릴리즈에서 막힐 예정이므로, 대신 getDerivedStateFromError()를 사용해야함)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    //state에 병합될 값을 반환
    //Render 단계에서 호출되는 메서드이므로 Side-Effect를 발생시켜선 안됨
    //static 메서드이므로 this를 통해 컴포넌트에 접근 불가능함
    return {hasError: true};
  }
  componentDidCatch(error, info) {
    //Commit 단계에서 호출되므로 Side-Effect가 허용됨
    //에러를 서버로 전송하기 좋은 위치
    logErrorToMyService(error, info);

    //setState()호출은 추후 릴리즈에서 막힐 예저이므로 getDerivedStateFromError()를 대신 사용할 것
    //this.setState({ hasError: true }); 
  }

  render() {
    if (this.state.hasError) {
      return <h1>오류가 발생했습니다.</h1>;
    }
    return this.props.children;
  }
}

componentWillUnmount()

  • 컴포넌트 소멸 시 호출되는 유일한 메서드
  • 이벤트 해제, 리소스 해제 등 소멸자로서의 역할

기타 클래스 컴포넌트 사용법

setState(updater, [callback]) 또는 setState(stateChange, [callback])

  • state의 값 변경을 React에 요청하는 메서드
    setState()가 호출되면 React는 추후 setState() 요청들을 한꺼번에 처리함 (=비동기 일괄실행)
    따라서 setState() 호출 후 state를 즉시 참조하면 변경되기 전의 값이 참조됨
  • updater 매개변수는 함수로서 prop, state를 바탕으로 state 값 변경을 요청할 수 있음
    this.setState((state, props) => {
      return {counter: state.counter + props.step};
    });
    
  • stateChange 매개변수는 새로운 값을 포함한 state 객체로서 기존 state와 병합되게 됨
  • callback 함수는 React가 추후 setState() 요청을 처리하게 되면 호출하는 함수
    this.setState({ count: 1 }, 
    	//callback은 state가 변경되고 나서 호출됨
    	() => {
      		console.log('state changed!');
        });
    

forceUpdate()

  • 컴포넌트의 render()를 강제로 실행하도록 React에 요청하는 메서드. React는 추후 shouldComponentUpdate()를 건너뛰고 render()를 실행함
  • render()내에서는 forceUpdate()를 사용해서는 안됨 (=무한루프)
    onClick = () => {
        //forceUpdate()는 render()를 강제로 호출시킴
        this.forceUpdate();
    };
    

이벤트 바인딩

  • 이벤트 핸들러 함수를 작성할 때는 함수의 this에 컴포넌트를 연결하는 bind() 작업을 해야함
    class Counter extends Component {
      constructor(props) {
        super(props);
        this.state = { count:0 };
    
        //increaseBound 함수의 this가 컴포넌트를 가리키도록 bind() 진행
        this.increaseBound = this.increaseBound.bind(this);
      }
      increaseBound() {
        /*GOOD
          bind()를 진행했으므로 함수의 this는 컴포넌트를 가리킴*/
        this.setState({ count: this.state + 1});
      }
      increaseNotBound() {
        /*BAD
          bind()를 진행하지 않으면 this는 호출자를 가리킴
          버튼의 onClick에서 호출하므로 this는 버튼을 가리킴
          따라서 아래 코드는 오류가 발생함 */
        this.setState({ count: this.state + 1});
      }
    
        
    
      render() {
        return (
          <div>
            Count: {this.state.count} <br />
            <button onClick={this.increaseBound}>Increase (Bind 적용)</button>
            <button onClick={this.increaseNotBound}>Increase (Bind 미적용)</button>
          </div>
        );
      }
    }
    
  • Class Field 문법을 사용하면 bind()를 생략할 수 있음
    class Counter extends Component {
      constructor(props) {
        super(props);
        this.state = { count:0 };
      }
    
      //Cleass Field를 통해 이벤트 핸들러를 선언하면 this와 연결하는 bind()생략가능
      increase => () {
        this.setState({ count: this.state.count + 1 });
      }
    
      render() {
        return (
          <div>
            Count: {this.state.count}<br />
            <button onClick={this.increase}>Increase</button>
          </div>
        );
      }
    }
    

Context를 통한 컴포넌트간 데이터 공유

리액트에서는 컴포넌트 트리간 데이터를 공유하기 위해서 Context를 사용합니다. 클래스형 컴포넌트에서는 구독할 Context를 지정하기 위해 contextType Static 멤버 변수를 사용합니다.

//1. 컴포넌트간 공유할 Context 선언
const UserContext = React.createContext('AnonymousUser');

class UserProfileComponent extends React.Component {
    componentDidMount() {
        //3. 컴포넌트에 할당된 Context 객체 조회
        const user = this.context;
    }
}

//2. 컴포넌트에서 사용할 Context 객체 할당
UserProfileComponent.contextType = UserContext;

UserProfileComponent는 부모 트리를 따라 올라가며 가장 가까이 있는 Provider로부터 context 값을 읽게 됩니다. 하나 이상의 컨텍스트를 사용하려면 여러 컨텍스트 구독하기 예제를 확인해주세요.

컴포넌트간 공통로직 관리1: Higher Order Component(HOC, 고차 컴포넌트) 패턴

리액트에서는 독립적이고 재사용가능한 UI 단위를 구성하기 위해 컴포넌트 API를 제공하고 있지만, 콤포넌트간 공통 로직을 관리하기 위한 API는 제공하고 있지 않습니다. 따라서 컴포넌트간의 공통 로직을 관리하기 위해 여러 개발패턴들이 사용되고 있으며, 그 중 자주 사용되는 패턴 중 하나가 고차컴포넌트(Higher Order Component(HOC)) 패턴입니다.

함수형 프로그래밍에서는 고차함수(Higher Order Function)라는 개념이 있습니다. 함수를 매개변수로 받아, 연산을 추가한 후, 함수를 반환하는 함수를 고차 함수라고 합니다. 동일하게 고차 컴포넌트는 컴포넌트를 매개변수로 받아, 기능을 더한 후, 컴포넌트를 반환하는 것을 말합니다.

const EnhancedComponent = higherOrderComponent(InputComponent);

고차 컴포넌트 패턴에서 매개변수로 전달받은 컴포넌트에 기능을 더하기 위한 방법으로는 컴포넌트 합성(Composition)과 상속(Inherit)을 사용합니다.

//* HOC를 이용한 컴포넌트 공통로직 추가: Type1 합성방식
withMountLogger(InputComponent, componentName) { //컴포넌트를 매개변수로 받음
    return class OutputComponent extends Components { //컴포넌트 반환
        componentDidMount() {
            console.log('Mounted: ' + componentName); //기능을 더함
        }
        render () {
            return <InputComponent {...this.props} />;
        }
    }
}

//* HOC를 이용한 컴포넌트 공통로직 추가: Type2 상속방식
withMountLogger(InputComponent, componentName) { //컴포넌트를 매개변수로 받음
    return class OutputComponent extends InputComponent { //컴포넌트 반환
        componentDidMount() {
            super.componentDidMount();
            console.log('Mounted: ' + componentName); //기능을 더함
        }
    }
}

합성과 상속 두가지 방법을 사용할 수 있지만 결합도와 복잡성을 낮추고 재사용성을 높이기 위해 합성 방법을 권장하고 있습니다.

컴포넌트간 공통로직 관리2: Render Props 패턴

Render Props(렌더 속성값) 패턴을 통해 컴포넌트의 공통로직을 관리할 수 있습니다. Render Props 패턴은 공통 기능을 가진 컴포넌트에게 Render()를 수행하는 함수를 Props로 전달하는 패턴을 말합니다. 공통 기능을 가진 컴포넌트는 Props로 전달받은 Render() 함수를 실행하여 UI를 생성합니다.


//공통 기능을 가진 컴포넌트 (Render Props 패턴)
class EventLogger extends Component {
    //공통기능 작성
    componentDidMount()) {
        logEvent('Mount', this.props.name);
    }

    //전달받은 render() Props 값을 출력
    render() {
        return this.props.children();
    }
}

//Render Pros 공통로직 컴포넌트 사용예시1
class Company extends Component {
    ...
    render() {
        //공통로직 컴포넌트에게 render() 함수를 Props로 전달함
        return (
            <EventLogger name='Company' render={() => <div>... Company ...</div>} />
        );
    }
}

//Render Pros 공통로직 컴포넌트 사용예시2
class User extends Components {
    ...
    render() {
        return (
            //공통로직 컴포넌트에게 render() 함수를 Props로 전달함
            <EventLogger name='User'>
                {() => <div> ... User Component ...</div>}
            </EventLogger>
        );
    }
}

render() 함수를 props로 전달하는 것이 Render Props 패턴입니다. 예를들어 아래 코드는 Render Props 패턴이 아닙니다. Render()함수가 아니라 React Element를 전달하고 있기 때문입니다.


//공통 레이아웃을 정의 (Render Props 패턴이 아님)
class Layout extends Component {
    render() {
        <div id="layout">
            ... Layout 정의 ...

            <!-- Render()함수를 호출하는 것이 아닌 React Element를 그대로 출력-->
            {this.props.children} 
        </div>
    }
}

//공통 레이아웃 사용
class Login extends Component {
    render() {
        return (
            <Layout>
                //Render Props 패턴과 달리 Render() 함수를 전달하지 않고 React Element를 전달
                <div id='login'>
                    ... 로그인 페이지 정의 ...
                </div>
            </Layout>
        );
    }
}

Render Props 패턴은 Render() 함수를 Props로 전달하기 때문에 매개변수를 주고 받을 수 있다는 특징이 있습니다. 따라서 공통로직을 구현하는 컴포넌트와 데이터를 주고 받을 수 있습니다.

//Render Props 공통로직 컴포넌트(서버로부터 데이터 로드)
class DataLoadder extends Component {
    state = { response : null };
    componentDidMount() {
        loadFromServer(this.props.url)
            .then(r => this.setState({ response : r });
    }

    render() {
        if(this.state.response == null) {
            return <div>Loading...</div>;
        }
        else {
            return children({ this.state.response });
        }
    }
}


//공통로직 콤포넌트를 사용
class User extends Component {
    render() {
        <DataLoadder url="https://example.com/user?id=1">
            { ({ response }) => (
                <div id='User'>
                    {`Name : ${response.Name}`}
                    {`Email : ${response.Email}`}
                </div>
            )}
        </DataLoadder>
            
    }
}

Higher Order Component vs Render Props 패턴

  • Higher Order Component 패턴
    한번 정의해두면 사용하기 간편합니다. 반면에 합성이나 상속이 진행되기 때문에 Props 이름 충돌이나, 다중 중첩이 진행 될 때 복잡성이 높아지는 문제가 있습니다.
  • User Porps 패턴
    상속이나 합성 등으로 발생하는 이름 충돌 문제가 없으며, 구현도 간편한 편입니다. 반대로 사용할 때 약간 더 번잡해진다는 점이 단점으로 꼽힙니다.

클래스형 컴포넌트의 한계

클래스형 컴포넌트가 명시적이고 기능이 더 많음에도 불구하고 함수형 컴포넌트로 대체된 데에는 몇가지 이유가 있습니다.

  1. 의례적으로 작성해야하는 코드량이 많음
    클래스형 컴포넌트는 React Component를 상속해야하며 라이프사이클 메서드를 정의해야하며 준수해야하는 규칙이 상대적으로 더 많습니다. 아래는 함수형 컴포넌트와 클래스형 컴포넌트의 차이를 보여줍니다.
    /* 클래스형 컴포넌트 vs 함수형 컴포넌트 */
    
    //클래스형 컴포넌트
    class User extends Component {
        constructor(props) {
            suepr(props;
            this.state = {
                fullName: `${props.firstName} ${props.lastname}`,
            };
        }
        render() {
            return (
                <div>User Name : {this.state.fullName}</div>
            );
        };
    }
    
    //함수형 컴포넌트
    function User({firstName, lastName }) {
        const [fullName, setFullName] = useState(`${props.firstName} ${props.lastname}`);
        return (
                 <div>User Name : {fullName}</div>           
            );
    }
    
  2. 성능상 불리함
    컴파일러가 코드 압축과 최적화를 진행하기에 약간 더 어려운 어려운 구조이며 , 메모리 사용량도 좀 더 높습니다.
  3. 공통로직 관리가 어려움
    클래스형 컴포넌트에서는 공통로직을 관리하기 위해 Higher Order Component 패턴이나 Render Props 패턴을 주로 사용하며, 이러한 패턴들은 대체로 복잡성이 높습니다. 함수형 컴포넌트는 그 자체가 함수이기 때문에 공통로직 관리가 간편합니다.

 

댓글