React Function and Class Components

React Function and Class Components

·

8 min read

함수형 컴포넌트

선언하는 방법들

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

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

// named function declaration
export function FunctionComponent2() {
    return (
        <div>
            <h1>FunctionComponent 2</h1>
        </div>
    )
}

Props를 전달하는 방식

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

# 올바른 방식
const FunctionComponent = (props: any) => {
    const { name } = props;
    return (
        <div>
            <h1>FunctionComponent1 {name}</h1>
        </div>
    );
}

# 커스텀 포맷만 받고 싶을때
export const FunctionComponent0 = (props: {name: string}) => {
    return (
        <div>
            <h1>FunctionComponent0 {props.name}</h1>
        </div>
    )
}

# 타입 지정
export interface FunctionComponentProps {
    name: string;
}

export const FunctionComponent = (props: FunctionComponentProps) => {
    return (
        <div>
            <h1>FunctionComponent {props.name}</h1>
        </div>
    )
}
# 함수 컴포넌트의 기본 구조와 맞지 않아서 이렇게 사용 할 수 없다.
export const FunctionComponent = (name: string) => {
    return (
        <div>
            <h1>FunctionComponent1 {name}</h1>
        </div>
    )
}

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

export interface FunctionComponentProps {
    name: string;
}

export const FunctionComponent = ({name}: { name: string }) => {
    return (
        <div>
            <h1>FunctionComponent1 {name}</h1>
        </div>
    )
}

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

자식노드 전달

네이밍규칙이 지켜져야한다.

export const FunctionComponent = ({ children }: {children : ReactNode}) => {
    return (
        <div>
            {children} <=={<h1>i'm children</h1>}
        </div>
    )
}

# 호출
<FunctionComponent>
    <h1>i'm children</h1>
</FunctionComponent>


# props 도 추가해보기
export const FunctionComponent = (props: { children: ReactNode, name: string }) => {
    return (
        <div>
            {props.name}
            {props.children}
        </div>
    )
}

# 호출
<FunctionComponent name="Jone">
    <h1>i'm children</h1>
</FunctionComponent>

React.FC(권장하지 않음)

기본 사용코드

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 = <T extends Link>({name, url}: T) => {
    return (
        <div>
            <a href={url}>{name} Link</a>
        </div>
    )
}

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

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

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

default props

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

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

export const LinkFComponent: React.FC<Link> = ({name, url}) => {
    return (
        <div>
            <a href={url}>{name} Link</a>
        </div>
    )
}

LinkFComponent.defaultProps = {
    name: "myDefaultLink",
    url: "http://localhost:3000"
}

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 <div>
            <h1>Hello</h1>
        </div>;
    }
}

props 사용하기

export class ClassComponent extends React.Component<any, any> {
    render() {
        return <div>
            <p>Hello, {this.props.name}</p>
        </div>;
    }
}

<ClassComponent name="Jone"></ClassComponent>

state 사용하기

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

물론 타입을 지정 할 수 있다.

export class ClassComponent extends React.Component<any, { firstName: string, lastName: string }> {
    constructor(props: any) {
        super(props);
        this.state = {
            firstName: "kapil",
            lastName: "sharma"
        };
    }
    render() {
        return <div>
            <p>Hello, {this.state.firstName} {this.state.lastName}</p>
        </div>;
    }
}

타입 명시하기

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 <div>
            <p>Hello, {this.state.firstName} {this.state.lastName}</p>
        </div>;
    }
}

생명주기 함수들 (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<any>, nextState: Readonly<any>, 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 
        ...
         <button onClick={ () => this.countUp() }>증가+1</button>
}

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

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

componentWillUnmount

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

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

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

    render() {
        return (
            <button onClick={this.handleClick}>
                {this.props.children}
            </button>
        );
    }
}
{this.state.isVisible && (
    <Button onClick={this.unvisible}>나를 사라지게</Button>
)}

예시코드와 같이 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>
            <p>Count: {count}</p>
            <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 <div>에러가 발생했습니다. 대체 컨텐츠를 표시합니다.</div>;
        }

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

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

        return <div>예외가 발생하지 않았습니다.</div>;
    }
}

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

정리

  • 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"
}