본문 바로가기
프로그래밍/React

React Function and Class Components

by slowin 2024. 7. 29.

함수형 컴포넌트

선언하는 방법들

여러 방법중에 arrow function와named function declaration 두가지가 많이 사용되어지는 듯하다.

// arrow function
export const FunctionComponent1 = () => {
    return (
        FunctionComponent 1
    )
}

// named function declaration
export function FunctionComponent2() {
    return (
        FunctionComponent 2
    )
}
 

Props를 전달하는 방식

props를 매개변수로 받아와서 사용하는 방식

import React from 'react';

// 1. 기본적인 방식: props를 통해 값 받아오기
const FunctionComponent = (props: any) => {
    const { name } = props;
    return (
        <div>FunctionComponent {name}</div>
    );
}

// 2. 특정한 props 타입만 받고 싶을 때: 객체 리터럴을 이용해 타입 지정
export const FunctionComponent0 = (props: {name: string}) => {
    return (
        <div>FunctionComponent0 {props.name}</div>
    )
}

// 3. TypeScript 인터페이스를 사용하여 props 타입 지정
export interface FunctionComponentProps {
    name: string;
}

export const FunctionComponent1 = (props: FunctionComponentProps) => {
    return (
        <div>FunctionComponent1 {props.name}</div>
    )
}

export const FunctionComponent2 = (props: {name: string}) => {
    return (
        <div>FunctionComponent2 {props.name}</div>
    )
}

 

구조 분해 할당을 사용하는 방법

export interface FunctionComponentProps {
    name: string;
}

export const FunctionComponent = ({name}: { name: string }) => {
    return (
	    FunctionComponent1 {name}
    )
)}

    )
}

export const FunctionComponent = ({name}: FunctionComponentProps) => {
    return (
	    FunctionComponent1 {name}
    )
}

자식노드 전달

// 자식 노드만 받는 컴포넌트
export const FunctionComponent = ({ children }: { children: ReactNode }) => {
  return <div>{children}</div>;
};

// 사용 예시
const App = () => {
  return (
    <FunctionComponent>
      <p>im children</p>
    </FunctionComponent>
  );
};

// props와 자식 노드를 함께 받는 컴포넌트
export const FunctionComponentWithProps = ({
  children,
  name,
}: {
  children: ReactNode;
  name: string;
}) => {
  return (
    <div>
      <h1>{name}</h1>
      {children}
    </div>
  );
};

// 사용 예시
const AppWithProps = () => {
  return (
    <FunctionComponentWithProps name="Example">
      <p>im children</p>
    </FunctionComponentWithProps>
  );
};

React.FC(권장하지 않음)

기본 사용코드

import React from 'react';

// Props 타입 정의
type Props = {
    count: number;
};

// 명시적인 함수형 컴포넌트 정의
const FCComponent: React.FC<Props> = ({ count }) => {
    return (
        <div>
            Parent {count}
        </div>
    );
};

export default FCComponent;

권장하지 않는 이유

자식 노드가 필요하지 않는 경우에도 자식에 대한 암시적 정의를 제공함.

React 17버전 이하에서는 children 을 지정하지 않았더라도 에러가 발생하지 않는다.

17버전 이하로 했을때,

18버전으로 했을때,

하위 버전에서는 Child가 optional이므로 컴파일 에러가 발생하지 않는 문제가 있다.

제네릭 이슈

함수형 컴포넌트로 제네릭 사용하는 예시

interface Link {
    name: string;
    url: string;
}

export const GenericComponent = ({name, url}: T) => {
    return (
		{name} Link
    )
}

 

React.FC에서 제네릭을 일반화 정의 할 수 없다.

# 이렇게는 사용불가능하다.
const LinkFCoponent: React.FC = ({name, url}: T) => {
    return (
		{name} Link
    )
}

# 타입을 지정해줘야한다.
export const LinkFComponent: React.FC = ({name, url}) => {
    return (
		{name} Link
    )
}

default props

optional 타입이 아닌경우 defaultProps를 사용할 수 없다.

import React from 'react';

// Link 인터페이스 정의
interface Link {
    name: string;
    url: string;
}

// Link 인터페이스를 사용하여 props 타입 지정
const LinkFComponent: React.FC<Link> = ({ name, url }) => {
    return (
        <div>
            <a href={url}>{name} Link</a>
        </div>
    );
};

// 기본 props 설정
LinkFComponent.defaultProps = {
    name: "myDefaultLink",
    url: "http://localhost:3000"
};

export default LinkFComponent;

optional을 추가해줘야 사용 할 수 있다.

interface Link {
    name?: string;
    url?: string;
}

React.FC는 React 컴포넌트와 다소 다르게 작동되는 이슈가 있기때문에 사용을 지양하라고 권장한다.

클래스 컴포넌트

선언하는 방법

기본 코드

export class ClassComponent extends React.Component<any, any> {
    constructor(props: any) {
        super(props);
    }

    render() {
        return	Hello;
    }
}

# props 사용하기
export class ClassComponent extends React.Component<any, any> {
    render() {
        return Hello, {this.props.name}
    }
}

 

state 사용하기

export class ClassComponent extends React.Component<any, any> {
    constructor(props: any) {
        super(props);
        # 상태선언
        this.state = {
            firstName: "kapil",
            lastName: "sharma"
        };
    }
    render() {
        return
			Hello, {this.state.firstName} {this.state.lastName};
    }
}


# 물론 타입을 지정 할 수 있다.
export class ClassComponent extends React.Component<any, { firstName: string, lastName: string }> {
    constructor(props: any) {
        super(props);
        this.state = {
            firstName: "kapil",
            lastName: "sharma"
        };
    }
    render() {
        return Hello, {this.state.firstName} {this.state.lastName} ;
    }
}

 

타입 명시하기

type MyProps = any

type MyStatus = {
    firstName: string,
    lastName: string
}

export class ClassComponent extends React.Component<MyProps, MyStatus> {
    constructor(props: any) {
        super(props);
        this.state = {
            firstName: "kapil",
            lastName: "sharma"
        };
    }

    render() {
        return Hello, {this.state.firstName} {this.state.lastName};
    }
}

생명주기 함수들 (React 문서 참고)

interface을 보자

// react/index.ts

// 이것은 실제로 'Lifecycle<P,S>|Descated Lifecycle<P,S>'와 같은 것이어야 합니다,
// 리액트가 새로운 라이프사이클 중 하나라도 사용하지 않는 라이프사이클 방법을 호출할 것이므로
// 방법이 있습니다.
interface ComponentLifecycle<P, S, SS = any> extends NewLifecycle<P, S, SS>, DeprecatedLifecycle<P, S> {
/**
* 구성 요소가 장착된 후 즉시 호출됩니다. 여기에서 상태를 설정하면 재렌더링이 트리거됩니다.
*/
    componentDidMount?(): void;
/**
* 소품 및 상태 변경이 재렌더를 트리거할지 여부를 결정하기 위해 호출됩니다.
*
* component는 항상 true로 반환됩니다.
* 'PureComponent'는 소품과 상태를 얕은 비교를 구현하고, 있으면 참으로 돌아옵니다
* 소품 또는 상태가 변경되었습니다.
*
* false가 반환되면 'Component#render', 'ComponentWillUpdate'가 됩니다
* 'componentDidUpdate'가 호출되지 않습니다.
*/
    shouldComponentUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean;
/** 
* 구성 요소가 파괴되기 직전에 호출됩니다. 다음과 같이 이 방법으로 필요한 정리를 수행합니다 
* 취소된 네트워크 요청 또는 'componentDidMount'에 생성된 모든 DOM 요소를 정리합니다. 
*/
    componentWillUnmount?(): void; 
/** 
* 하위 구성 요소에서 생성된 예외를 캡처합니다. 처리되지 않은 예외는 다음을 야기합니다 
* 마운트 해제할 전체 구성 요소 트리. 
*/
    componentDidCatch?(error: Error, errorInfo: ErrorInfo): void; 
} 

// 안타깝게도 구성 요소 구성 요소가 이를 구현해야 한다고 선언할 방법이 없습니다
interface StaticLifecycle<P, S> { 
    getDerivedStateFromProps?: GetDerivedStateFromProps<P, S> | undefined; 
    getDerivedStateFromError?: GetDerivedStateFromError<P, S> | undefined; 
} 

interface NewLifecycle<P, S, SS> { 
/** 
* React는 'render' 결과를 문서에 적용하기 전에 실행합니다 
* componentDidUpdate에 제공할 개체를 반환합니다. 저장에 유용합니다 
* '스크롤'이 그것에 변화를 일으키기 전의 스크롤 위치와 같은 것들. 
* 
* 참고: getSnapshotBeforeUpdate가 있으면 권장되지 않는 기능이 없습니다 
* 실행 중인 라이프사이클 이벤트. 
* getSnapshotBeforeUpdate 메서드를 사용하는 경우에는 React의 다른 라이프사이클 이벤트들이 실행되지 않으므로 주의해야 합니다. 
*/
    getSnapshotBeforeUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>): SS | null; 
/** 
* 업데이트가 발생한 후 즉시 호출됩니다. 초기 렌더에 대해 호출되지 않습니다. 
* 
* 스냅샷은 getSnapshotBeforeUpdate가 있고 null이 아닌 것을 반환하는 경우에만 존재합니다. 
*/
    componentDidUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot?: SS): void; 
}

생명 주기 하나씩 파악해보자

componentDidMount

/**
 * 구성 요소가 장착된 후 즉시 호출됩니다. 여기에서 상태를 설정하면 재렌더링이 트리거됩니다.
 */
componentDidMount() {
    console.log("componentDidMount! ", this.unixTime());
}

render() {
    return (
        <div>
            <h1>Hello</h1>
            <p>{this.unixTime()}</p>
        </div>
    );
}

componentDidMount 는 최초 한번 마운트시 호출 된다.

언제 사용할까?

  • 네트워크 요청 과 같은 비동기 작업
  • 라이브러리 초기화
  • 이벤트 리스너

# 컴포넌트 방식으로는 어떻게 사용할까?

userEffect를 사용하자.

useEffect 의 두번째 인자 DependencyList 가 빈배열이면 최초 한번만 호출된다.

useEffect(() => {
    console.log("ComponentStateCounter componentDidMount! ", unixTime());
}, []);

 

shouldComponentUpdate

컴포넌트를 업데이트 할지 말지 결정하는 함수

/**
* 소품 및 상태 변경이 재렌더를 트리거할지 여부를 결정하기 위해 호출됩니다.
*/
shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean {
    console.log("shouldComponentUpdate! ", unixTime());
    return Math.random() >= 0.5;
}

랜덤으로 true or false 를 발생시켜 보았다.

countUp() {
    this.setState({
        count: this.state.count + 1
    })
}
...
render() {
    return 
        ...
          this.countUp() }>증가+1
}

this.countUp() 의 통해 상태를 변경되더라도, shouldComponentUpdate 가 false 를 리턴한다면 render() 가 수행되지 않는다.

함수형 컴포넌트에서는 useMemo, memo 가 비슷한 역할을 한다고 볼 수 있다.

 

componentWillUnmount

구성 요소가 파괴되기 직전에 호출된다.

class Button extends React.Component<any, any> {
    handleClick = () => {
        this.props.onClick();
    };

    /**
     * 구성 요소가 파괴되기 직전에 호출됩니다. 다음과 같이 이 방법으로 필요한 정리를 수행합니다
     * 취소된 네트워크 요청 또는 'componentDidMount'에 생성된 모든 DOM 요소를 정리합니다.
     */
    componentWillUnmount() {
        console.log("componentWillUnmount! ", unixTime());
    }

    render() {
        return (
            
                {this.props.children}
            
        );
    }
}
{this.state.isVisible && (
    나를 사라지게
)}

예시코드와 같이 Button 컴포넌트가 사라질때, componentWillUnmount 호출된다.

함수형 컴포넌트에서 사용하고 싶을때

useEffect의 리턴 메소드를 정의한다

useEffect(() => {
    const timer = setInterval(() => {
        setCount((prevCount) => prevCount + 1);
    }, 1000);

    // 컴포넌트가 언마운트될 때 실행되는 부분
    return () => {
        clearInterval(timer); // 타이머 정리
    };
}, []); // 빈 배열을 전달하면 마운트와 언마운트 시에만 실행

DependencyList를 추가하면?

export const ComponentStateCounter = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("NO DI useEFFECT", unixTime());
        return () => {
            console.log("NO DI useEFFECT RETURN", unixTime());
        }
    }, []);

    useEffect(() => {
        console.log("Count useEffect", unixTime());
        return () => {
            console.log("Count useEffect RETURN Start", unixTime());
            for (let i = 0; i < 999999999; i++) {}
            console.log("Count useEffect RETURN End", unixTime());
        }
    }, [count]);

    return (
        <div>
            Count: {count}
            <button onClick={() => setCount((prevCount) => prevCount + 1)}>+1</button>
        </div>
    );
};

 

+1 버튼을 클릭하고 사라지는 버튼을 클릭했을때 호출 순서

 

componentDidCatch

하위 구성 요소에서 생성된 예외를 캡처합니다.

export class ErrorBoundary extends React.Component<any, any> {
    constructor(props: {}) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error:any, errorInfo: any) {
        // 컴포넌트 내에서 발생한 예외를 처리합니다.
        console.error("에러가 발생했습니다:", error);
        console.error("에러 정보:", errorInfo);

        // 예외를 처리한 후 상태를 업데이트하여 에러 메시지를 렌더링합니다.
        this.setState({ hasError: true });
    }

    render() {
        if (this.state.hasError) {
            // 에러가 발생한 경우 대체 컨텐츠를 렌더링합니다.
            return ; // 에러가 발생했습니다. 대체 컨텐츠를 표시합니다.
        }

        // 에러가 없는 경우 자식 컴포넌트를 렌더링합니다.
        return this.props.children;
    }
}

export class ExampleComponent extends React.Component {
    render() {
        // 에러를 발생시키는 예제 코드
        if (Math.random() < 0.5) {
            throw new Error("자식 컴포넌트에서 전달하는 에러 메시지!!!!! 랜덤 예외 발생!");
        }

        return ;
    }
}

에러가 발생했을때 감지 됨.

정리

  • React 에서 컴포넌트 정의는 함수형과 클래스형으로 할 수 있다.
  • React.FC는 과거 버전 or 라이브러리 호환성 문제 와 다소 다르게 작동되는 부분이 있기 때문에 사용을 지양함.
  • 특별한 라이프 사이클을 사용해야 할때는 클래스 컴포넌트를 활용하자.

참고

https://ko.legacy.reactjs.org/docs/state-and-lifecycle.html#adding-lifecycle-methods-to-a-class

https://medium.com/@martin_hotell/10-typescript-pro-tips-patterns-with-or-without-react-5799488d6680

https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30695

https://github.com/typescript-cheatsheets/react/issues/87

프로젝트 dependencies

"dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.46",
    "@types/react": "^18.2.21",
    "@types/react-dom": "^18.2.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
}

'프로그래밍 > React' 카테고리의 다른 글

Golang Slice(슬라이스) 동작원리와 사용법  (0) 2024.07.31
React Props,State  (0) 2024.07.29
React Virtual DOM  (0) 2024.07.29
React Composition  (0) 2024.07.29