Development/Code

[React/리액트] React 개발자라면 반드시 알아야 할 디자인 패턴 9가지

Danny Seo 2024. 7. 31. 15:48

목차

    리액트 디자인 패턴 9가지

     

    React를 활용한 프론트엔드 개발에서 디자인 패턴의 적용은 필수적인 관행으로 자리 잡았습니다. 이러한 패턴은 React의 특성에 맞춰 발전해왔으며, 개발자가 견고한 컴포넌트와 애플리케이션을 설계할 때 반복적으로 직면하는 문제에 우아한 해결책을 제공합니다.

     

    디자인 패턴의 근본적인 목적은 상태 관리, 로직 및 요소 구성을 단순화하여 컴포넌트 개발의 문제를 해결하는 것입니다. 사전 정의된 구조와 검증된 방법론을 제공함으로써 React의 디자인 패턴은 코드베이스의 일관성, 모듈성 및 확장성을 촉진합니다.

     

    React에 적용되는 디자인 패턴의 대표적인 예로는 커스텀 훅, 고차 컴포넌트(HOC), Prop 기반 렌더링 기술이 있습니다. 이러한 요소는 개발자가 애플리케이션의 구조와 데이터 흐름을 최적화하여 코드 재사용성과 개념적 명확성을 높이는 강력한 도구입니다.

     

    이 패턴들은 기술적 문제를 해결하는 것뿐만 아니라 코드 효율성, 가독성 및 유지보수성을 우선시합니다. 표준화된 관행과 잘 정의된 개념을 채택함으로써 개발 팀은 보다 효과적으로 협업하고 장기적으로 견고하고 적응력 있는 React 애플리케이션을 구축할 수 있습니다.

     


    왜 React에서 패턴을 사용할까요?

    React에서 디자인 패턴을 사용하는 것은 개발자가 개발 시간과 비용을 줄이고, 기술 부채의 축적을 완화하며, 코드 유지보수를 쉽게 하고, 지속적이고 안정적인 개발을 촉진하기 때문입니다. 이러한 패턴은 개발 프로세스를 간소화하고 전체 소프트웨어 품질을 향상시키는 일관된 구조를 제공합니다.

     

    Custom Hook 패턴

    React에서 커스텀 훅 패턴은 컴포넌트의 로직을 재사용 가능한 함수로 캡슐화하는 기술입니다. 커스텀 훅은 useState, useEffect, useContext 등 React에서 제공하는 훅을 사용하는 JavaScript 함수로, 컴포넌트 간에 로직을 효과적으로 캡슐화하고 재사용할 수 있습니다.

    사용 시기

    • 코드 중복 없이 React 컴포넌트 간 로직을 공유해야 할 때
    • 컴포넌트의 복잡한 로직을 추상화하여 더 읽기 쉽고 유지 관리하기 쉽게 만들고 싶을 때
    • 컴포넌트의 로직을 모듈화하여 단위 테스트를 쉽게 하고 싶을 때

    사용하지 말아야 할 때

    • 로직이 단일 컴포넌트에만 특화되어 다른 곳에서 재사용되지 않을 때
    • 로직이 간단하여 커스텀 훅을 생성할 필요가 없을 때

    장점

    • 공통 로직을 별도의 함수로 캡슐화하여 코드 재사용을 촉진합니다.
    • 로직을 컴포넌트에서 분리하여 코드 구성 및 가독성을 높입니다.
    • 커스텀 훅에 캡슐화된 로직에 대해 더 구체적이고 집중된 단위 테스트를 수행할 수 있어 테스트 용이성이 향상됩니다.

    단점

    • 남용하여 많은 커스텀 훅을 생성하면 추가적인 복잡성을 초래할 수 있습니다.
    • 올바른 구현을 위해 React와 훅 개념에 대한 깊은 이해가 필요합니다.

    예제

    다음은 TypeScript와 React를 사용하여 일반적인 HTTP 요청을 수행하는 커스텀 훅의 예제입니다. 이 훅은 요청을 수행하고 로드 상태, 데이터 및 오류를 처리하는 로직을 다룹니다.

    import { useState, useEffect } from 'react';
    import axios, { AxiosResponse, AxiosError } from 'axios';
    
    type ApiResponse<T> = {
      data: T | null;
      loading: boolean;
      error: AxiosError | null;
    };
    
    function useFetch<T>(url: string): ApiResponse<T> {
      const [data, setData] = useState<T | null>(null);
      const [loading, setLoading] = useState<boolean>(true);
      const [error, setError] = useState<AxiosError | null>(null);
    
      useEffect(() => {
        const fetchData = async () => {
          try {
            const response: AxiosResponse<T> = await axios.get(url);
            setData(response.data);
          } catch (error) {
            setError(error);
          } finally {
            setLoading(false);
          }
        };
    
        fetchData();
      }, [url]);
    
      return { data, loading, error };
    }
    
    // Using the Custom Hook on a component
    function ExampleComponent() {
      const { data, loading, error } = useFetch<{ /* Expected data type */ }>('https://example.com/api/data');
    
      if (loading) {
        return <div>Loading...</div>;
      }
    
      if (error) {
        return <div>Error: {error.message}</div>;
      }
    
      if (!data) {
        return <div>No data.</div>;
      }
    
      return (
        <div>
          {/* Rendering of the obtained data */}
        </div>
      );
    }
    
    export default ExampleComponent;

    이 예제에서 커스텀 훅 useFetch는 URL을 인수로 받아 Axios를 사용하여 GET 요청을 수행합니다. 이 훅은 로드 상태, 데이터 및 오류를 관리하고 이 정보를 포함하는 객체를 반환합니다.

     

    ExampleComponent는 커스텀 훅 useFetch를 사용하여 API에서 데이터를 가져와 사용자 인터페이스에 렌더링합니다. 요청 상태에 따라 로드 인디케이터, 오류 메시지 또는 가져온 데이터가 표시됩니다.

     

    HOC 패턴

    고차 컴포넌트(HOC) 패턴은 컴포넌트 간 로직을 재사용하는 React의 구성 기술입니다. HOC는 컴포넌트를 받아 추가 또는 확장된 기능을 갖춘 새로운 컴포넌트를 반환하는 함수입니다.

    사용 시기

    • 여러 컴포넌트 간에 로직을 공유해야 할 때
    • 여러 컴포넌트에 공통 동작이나 기능을 추가해야 할 때
    • 프레젠테이션 로직과 비즈니스 로직을 분리하고 싶을 때

    사용하지 말아야 할 때

    • 로직이 단일 컴포넌트에만 특화되어 있을 때
    • 로직이 너무 복잡하여 HOC를 이해하기 어렵게 만들 때

    장점

    • 컴포넌트 간 로직을 캡슐화하고 공유하여 코드 재사용을 촉진합니다.
    • 프레젠테이션 로직과 비즈니스 로직을 명확하게 분리할 수 있습니다.
    • 함수형 디자인 패턴을 적용하여 코드 구성 및 모듈화를 촉진합니다.

    단점

    • 데이터 흐름을 추적하기 어려운 추가 추상화 레이어를 도입할 수 있습니다.
    • HOC를 과도하게 구성하면 디버깅하기 어려운 복잡한 컴포넌트가 생성될 수 있습니다.
    • 때로는 컴포넌트 계층 구조를 숨겨 애플리케이션 구조를 이해하기 어렵게 만들 수 있습니다.

    예제

    폼에서 데이터를 제출하기 위한 상태와 메서드를 처리하는 HOC를 생성한다고 가정해 봅시다. 이 HOC는 폼 값을 처리하고 데이터를 유효성 검사하며 서버로 요청을 전송합니다.

    import React, { ComponentType, useState } from 'react';
    
    interface FormValues {
      [key: string]: string;
    }
    
    interface WithFormProps {
      onSubmit: (values: FormValues) => void;
    }
    
    // HOC that handles form state and logic
    function withForm<T extends WithFormProps>(WrappedComponent: ComponentType<T>) {
      const WithForm: React.FC<T> = (props) => {
        const [formValues, setFormValues] = useState<FormValues>({});
    
        const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
          const { name, value } = event.target;
          setFormValues((prevValues) => ({
            ...prevValues,
            [name]: value,
          }));
        };
    
        const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
          event.preventDefault();
          props.onSubmit(formValues);
        };
    
        return (
          <WrappedComponent
            {...props}
            formValues={formValues}
            onInputChange={handleInputChange}
            onSubmit={handleSubmit}
          />
        );
      };
    
      return WithForm;
    }
    
    // Component that uses the HOC to manage a form.
    interface MyFormProps extends WithFormProps {
      formValues: FormValues;
      onInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
    }
    
    const MyForm: React.FC<MyFormProps> = ({ formValues, onInputChange, onSubmit }) => {
      return (
        <form onSubmit={onSubmit}>
          <input type="text" name="name" value={formValues.name || ''} onChange={onInputChange} />
          <input type="text" name="email" value={formValues.email || ''} onChange={onInputChange} />
          <button
    
     type="submit">Enviar</button>
        </form>
      );
    };
    
    // Using the HOC to wrap the MyForm component
    const FormWithLogic = withForm(MyForm);
    
    // Main component that renders the form
    const App: React.FC = () => {
      const handleSubmit = (values: FormValues) => {
        console.log('Form values:', values);
        // Logic to send the form data to the server
      };
    
      return (
        <div>
          <h1>HOC Form</h1>
          <FormWithLogic onSubmit={handleSubmit} />
        </div>
      );
    };
    
    export default App;

    이 예제에서 withForm HOC는 폼을 처리하는 로직을 캡슐화합니다. 이 HOC는 폼 값의 상태를 관리하고, 폼 값을 업데이트하는 함수(handleInputChange)를 제공하며, 폼 제출을 처리하는 함수(handleSubmit)를 제공합니다. 그런 다음 HOC는 MyForm 컴포넌트를 래핑하는 데 사용되며, 이는 메인 애플리케이션(App)에서 렌더링될 폼입니다.

     

    Extensible Styles 패턴

    Extensible Styles 패턴은 유연하고 쉽게 사용자 정의 가능한 스타일로 React 컴포넌트를 생성할 수 있는 기술입니다. 이 패턴은 스타일을 직접 컴포넌트에 적용하는 대신, 사용자의 필요에 따라 수정 및 확장할 수 있는 동적 CSS 속성이나 클래스를 사용합니다.

    사용 시기

    • 애플리케이션 내에서 다양한 스타일이나 테마에 적응할 수 있는 컴포넌트를 생성해야 할 때
    • 최종 사용자가 컴포넌트의 외관을 쉽게 사용자 정의할 수 있도록 할 때
    • 컴포넌트의 외관에 유연성을 제공하면서 사용자 인터페이스의 시각적 일관성을 유지하고 싶을 때

    사용하지 말아야 할 때

    • 스타일 사용자 정의가 필요하지 않거나 스타일이 크게 변하지 않을 때
    • 컴포넌트의 스타일과 외관을 엄격하게 제어해야 하는 애플리케이션에서

    장점

    • 소스 코드를 수정하지 않고 컴포넌트의 스타일을 사용자 정의하고 확장할 수 있습니다.
    • 애플리케이션의 시각적 일관성을 유지하면서 스타일에 유연성을 제공합니다.
    • 스타일링 로직을 컴포넌트 코드에서 분리하여 유지보수를 간소화합니다.

    단점

    • 확장 가능한 스타일을 적절히 관리하지 않으면 복잡성이 증가할 수 있습니다.
    • 스타일을 일관되고 예측 가능하게 확장할 수 있도록 신중한 설계가 필요합니다.

    예제

    다음은 props를 통해 색상과 크기를 변경할 수 있는 확장 가능한 스타일의 버튼 컴포넌트를 생성하는 예제입니다.

    import React from 'react';
    import './Button.css';
    
    interface ButtonProps {
      color?: string;
      size?: 'small' | 'medium' | 'large';
      onClick: () => void;
    }
    
    const Button: React.FC<ButtonProps> = ({ color = 'blue', size = 'medium', onClick, children }) => {
      const buttonClasses = `Button ${color} ${size}`;
    
      return (
        <button className={buttonClasses} onClick={onClick}>
          {children}
        </button>
      );
    };
    
    export default Button;
    .Button {
      border: none;
      cursor: pointer;
      padding: 8px 16px;
      border-radius: 4px;
      font-size: 14px;
      font-weight: bold;
    }
    
    .small {
      padding: 4px 8px;
    }
    
    .medium {
      padding: 8px 16px;
    }
    
    .large {
      padding: 12px 24px;
    }
    
    .blue {
      background-color: blue;
      color: white;
    }
    
    .red {
      background-color: red;
      color: white;
    }
    
    .green {
      background-color: green;
      color: white;
    }

    이 예제에서 Button 컴포넌트는 색상과 크기와 같은 속성을 받아 외관을 사용자 정의할 수 있습니다. CSS 스타일은 확장 가능한 방식으로 정의되어, props를 통해 버튼의 색상과 크기를 쉽게 수정할 수 있습니다. 이를 통해 개발자는 애플리케이션 내에서 다양한 스타일에 컴포넌트를 적응시킬 수 있는 유연성을 제공합니다.

     

    Compound Components 패턴

    복합 컴포넌트 패턴은 React에서 서로 밀접하게 협력하여 작동하는 컴포넌트를 생성하는 디자인 기술입니다. 이 패턴에서는 부모 컴포넌트가 여러 자식 컴포넌트를 캡슐화하여, 그들 간의 원활한 통신과 조정된 상호 작용을 가능하게 합니다.

    사용 시기

    • 서로 의존하고 함께 그룹화될 때 더 잘 작동하는 컴포넌트를 생성해야 할 때
    • 다양한 사용 사례에 적응할 수 있는 고도로 사용자 정의 가능하고 유연한 컴포넌트를 생성할 때
    • React 컴포넌트 트리 계층 구조에서 명확하고 조직적인 컴포넌트 구조를 유지하고 싶을 때

    사용하지 말아야 할 때

    • 컴포넌트 간의 관계가 밀접하지 않거나 명확한 의존성이 없을 때
    • 복합 컴포넌트 패턴의 추가된 복잡성이 그 이점을 정당화하지 못할 때

    장점

    • 관련 로직을 컴포넌트 집합에 캡슐화하고 재사용할 수 있습니다.
    • 복합 컴포넌트와 상호 작용하는 명확하고 일관된 API를 제공합니다.
    • 여러 컴포넌트를 하나로 결합하여 더 큰 유연성과 사용자 정의를 가능하게 합니다.

    단점

    • 컴포넌트 간의 상호 작용 방식을 이해하는 데 추가적인 복잡성을 도입할 수 있습니다.
    • 복합 컴포넌트를 유연하고 사용하기 쉽게 설계하려면 신중한 설계가 필요합니다.

    예제

    다음은 선택된 인덱스에 따라 탭(Tab)을 표시하고 숨기는 Tabs 컴포넌트를 생성하는 예제입니다.

    import React, { useState, ReactNode } from 'react';
    
    interface TabProps {
      label: string;
      children: ReactNode;
    }
    
    const Tab: React.FC<TabProps> = ({ children }) => {
      return <>{children}</>;
    };
    
    interface TabsProps {
      children: ReactNode;
    }
    
    const Tabs: React.FC<TabsProps> = ({ children }) => {
      const [activeTab, setActiveTab] = useState(0);
    
      return (
        <div>
          <div className="tab-header">
            {React.Children.map(children, (child, index) => {
              if (React.isValidElement(child)) {
                return (
                  <div
                    className={`tab-item ${index === activeTab ? 'active' : ''}`}
                    onClick={() => setActiveTab(index)}
                  >
                    {child.props.label}
                  </div>
                );
              }
            })}
          </div>
          <div className="tab-content">
            {React.Children.map(children, (child, index) => {
              if (index === activeTab) {
                return <>{child}</>;
              }
            })}
          </div>
        </div>
      );
    };
    
    const Example: React.FC = () => {
      return (
        <Tabs>
          <Tab label="Tab 1">
            <div>탭 1의 내용</div>
          </Tab>
          <Tab label="Tab 2">
            <div>탭 2의 내용</div>
          </Tab>
          <Tab label="Tab 3">
            <div>탭 3의 내용</div>
          </Tab>
        </Tabs>
      );
    };
    
    export default Example;

     

    Render Props 패턴

    렌더 Props 패턴은 React에서 코드를 공유하기 위해 특정 함수를 나타내는 특수한 Prop을 사용하는 기술입니다. 이 패턴은 컴포넌트의 특정 부분을 렌더링하는 책임을 Prop으로 제공된 함수에 위임하여, 컴포넌트 구성에서 더 큰 재사용성과 유연성을 제공합니다.

    사용 시기

    • 여러 컴포넌트 간에 렌더링 로직을 공유해야 할 때
    • 다양한 사용 사례에 적응할 수 있는 고도로 사용자 정의 가능한 컴포넌트를 생성할 때
    • 컴포넌트에서 프레젠테이션 로직과 비즈니스 로직을 분리하고 싶을 때

    사용하지 말아야 할 때

    • 렌더링 로직이 단일 컴포넌트에만 특화되어 재사용되지 않을 때
    • 렌더 Props 패턴이 불필요한 복잡성을 초래하여 코드를 이해하기 어렵게 만들 때

    장점

    • 다른 패턴보다 유연하게 렌더링 로직을 컴포넌트 간에 재사용할 수 있습니다.
    • 외부 함수에 렌더링 로직을 위임하여 컴포넌트를 더 유연하게 구성하고 사용자 정의할 수 있습니다.
    • 컴포넌트에서 프레젠테이션 로직과 비즈니스 로직을 분리하여 관심사의 분리를 촉진합니다.

    단점

    • 데이터 흐

    름을 이해하기 어렵게 만드는 추가적인 추상화 레이어를 도입할 수 있습니다.

    • 적절한 구현을 위해 React의 Props와 함수에 대한 깊은 이해가 필요합니다.

    예제

    렌더 Props 패턴은 React에서 에러 처리와 같은 다양한 방식으로 적용될 수 있습니다.

    import React, { Component, ErrorInfo, ReactNode } from 'react';
    
    interface ErrorBoundaryProps {
      renderError: (error: Error, errorInfo: ErrorInfo) => ReactNode;
    }
    
    interface ErrorBoundaryState {
      hasError: boolean;
      error: Error | null;
      errorInfo: ErrorInfo | null;
    }
    
    class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
      constructor(props: ErrorBoundaryProps) {
        super(props);
        this.state = {
          hasError: false,
          error: null,
          errorInfo: null,
        };
      }
    
      componentDidCatch(error: Error, errorInfo: ErrorInfo) {
        this.setState({
          hasError: true,
          error: error,
          errorInfo: errorInfo,
        });
      }
    
      render() {
        const { renderError, children } = this.props;
        const { hasError, error, errorInfo } = this.state;
    
        if (hasError) {
          return renderError(error!, errorInfo!);
        }
    
        return children;
      }
    }
    
    const App: React.FC = () => {
      const renderError = (error: Error, errorInfo: ErrorInfo) => {
        return (
          <div>
            <h2>Something went wrong.</h2>
            <details style={{ whiteSpace: 'pre-wrap' }}>
              {error && error.toString()}
              <br />
              {errorInfo.componentStack}
            </details>
          </div>
        );
      };
    
      return (
        <ErrorBoundary renderError={renderError}>
          <div>
            <h1>Welcome to My App</h1>
            <p>This is a sample application.</p>
            <button onClick={() => { throw new Error('An unexpected error occurred.'); }}>
              Trigger Error
            </button>
          </div>
        </ErrorBoundary>
      );
    };
    
    export default App;

    이 예제에서 우리는 자식 컴포넌트에서 발생한 모든 오류를 잡고 componentDidCatch 메서드를 사용하여 처리하는 ErrorBoundary 컴포넌트를 생성합니다. 오류가 발생하면 ErrorBoundary 컴포넌트는 render 함수(renderError)에서 제공하는 컴포넌트를 렌더링하여 오류에 대한 사용자 정의 UI를 표시합니다.

     

    App 컴포넌트는 ErrorBoundary를 사용하여 내용을 래핑하고 오류가 발생할 경우 정보를 표시할 렌더 함수를 제공합니다. 이 예제는 Render Props 패턴이 React 애플리케이션 내에서 오류를 처리하기 위해 사용자 정의 동작을 제공하는 방법을 보여줍니다.

     

    Control Props 패턴

    제어 Props 패턴은 React에서 컴포넌트가 부모 컴포넌트에서 제공한 Props를 통해 내부 상태를 제어할 수 있도록 하는 기술입니다. 컴포넌트가 자체 상태를 내부적으로 처리하는 대신, 상태의 제어를 Props를 통해 부모 컴포넌트에 위임하여 부모가 자식 컴포넌트의 상태를 필요에 따라 조작하고 제어할 수 있습니다.

    사용 시기

    • 컴포넌트의 상태를 외부에서 제어해야 하고, 상위 컴포넌트에서 상태를 관리해야 할 때
    • 다양한 컨텍스트에서 사용될 수 있는 유연한 컴포넌트를 생성해야 할 때
    • React 계층 구조에서 컴포넌트 간 양방향 통신이 필요한 상황

    사용하지 말아야 할 때

    • 컴포넌트의 상태가 순전히 내부적이며 외부에서 제어할 필요가 없을 때
    • 제어 Props 패턴이 Prop 과부하와 불필요한 복잡성을 초래할 때

    장점

    • 상위 컴포넌트에서 컴포넌트의 상태를 더 잘 제어할 수 있습니다.
    • React 계층 구조에서 컴포넌트 간 명확하고 양방향의 통신을 가능하게 합니다.
    • 컴포넌트를 다양한 애플리케이션 컨텍스트에서 재사용할 수 있도록 합니다.

    단점

    • 컴포넌트 간의 과도한 종속성을 도입하여 데이터 흐름 이해를 어렵게 만들 수 있습니다.
    • Props와 컴포넌트 간의 통신을 신중하게 관리하여 예상치 못한 동작을 방지해야 합니다.

    예제

    외부에서 토글할 수 있는 Toggle 컴포넌트를 생성한다고 가정해 봅시다.

    import React, { useState } from 'react';
    
    interface ToggleProps {
      value: boolean;
      onChange: (value: boolean) => void;
    }
    
    const Toggle: React.FC<ToggleProps> = ({ value, onChange }) => {
      const handleClick = () => {
        onChange(!value);
      };
    
      return (
        <button onClick={handleClick}>
          {value ? 'On' : 'Off'}
        </button>
      );
    };
    
    // Usage of the Toggle component controlled by props
    const Example: React.FC = () => {
      const [isToggled, setIsToggled] = useState(false);
    
      const handleToggleChange = (value: boolean) => {
        setIsToggled(value);
      };
    
      return (
        <div>
          <h1>Control Props Example</h1>
          <Toggle value={isToggled} onChange={handleToggleChange} />
          <p>The current state is: {isToggled ? 'On' : 'Off'}</p>
        </div>
      );
    };
    
    export default Example;

    이 예제는 Toggle 컴포넌트를 통해 제어 Props 패턴을 보여줍니다. 이 컴포넌트는 상위 컴포넌트에서 제공한 props를 통해 토글 상태를 제어합니다. 사용자가 토글 버튼을 클릭하면 상위 컴포넌트에서 제공한 onChange 함수가 호출되어 토글 상태를 업데이트합니다.

     

    Example 컴포넌트는 Toggle 컴포넌트를 사용하여 상태를 유지하고, 상태가 변경될 때 handleToggleChange 함수를 사용하여 상태를 업데이트합니다. 이 예제는 제어 Props 패턴이 부모 컴포넌트에서 제공한 props를 통해 컴포넌트의 내부 상태를 제어할 수 있는 방법을 보여줍니다.

     

    Props Getters 패턴

    Props Getters 패턴은 React에서 자식 컴포넌트가 부모 컴포넌트의 특정 props를 가져오고 수정할 수 있도록 하는 기술입니다. 이 패턴에서 부모 컴포넌트는 "props getters"라는 특수한 함수를 자식 컴포넌트에 전달하여, 자식 컴포넌트가 부모 컴포넌트의 특정 props에 접근하고 필요한 경우 이를 수정할 수 있습니다.

    사용 시기

    • 자식 컴포넌트가 부모 컴포넌트의 특정 props에 접근하거나 이를 수정해야 할 때
    • 높은 결합도가 있는 컴포넌트 간의 명확하고 예측 가능한 통신이 필요한 경우
    • 자식 컴포넌트 내에서 부모의 특정 props를 수정할 유연성이 필요할 때

    사용하지 말아야 할 때

    • 컴포넌트 간의 통신이 부모의 특정 props에 접근하거나 수정하는 것을 포함하지 않는 경우
    • Props Getters 패턴이 불필요한 복잡성을 초래하여 애플리케이션의 데이터 흐름을 이해하기 어렵게 만들 때

    장점

    • 자식 컴포넌트가 부모 컴포넌트의 특정 props에 접근하고 수정할 수 있는 명확하고 통제된 메커니즘을 제공합니다.
    • 컴포넌트 간의 명확하고 예측 가능한 통신을 가능하게 하여 코드 유지 보수와 디버깅을 용이하게 합니다.
    • 부모의 props에 기반하여 자식 컴포넌트의 동작을 적응시킬 수 있는 유연성을 제공합니다.

    단점

    • 컴포넌트 간의 명시적인 종속성을 도입하여 복잡성과 결합도를 증가시킬 수 있습니다.
    • Props Getters가 일관되게 사용되도록 신중하게 설계해야 하며, 예상치 못한 부작용을 일으키지 않도록 주의해야 합니다.

    예제

    여기에서는 열을 정렬할 수 있는 테이블을 구현하기 위해 Props Getters 패턴을 적용한 예제를 보여드립니다.

    import React, { useState } from 'react';
    
    interface Column {
      id: string;
      label: string;
      sortable: boolean;
    }
    
    interface TableProps {
      columns: Column[];
      data: any[];
    }
    
    interface TableHeaderProps {
      column: Column;
      onSort: (columnId: string) => void;
    }
    
    const TableHeader: React.FC<TableHeaderProps> = ({ column, onSort }) => {
      const handleSort = () => {
        if (column.sortable) {
          onSort(column.id);
        }
      };
    
      return (
        <th onClick={handleSort} style={{ cursor: column.sortable ? 'pointer' : 'default' }}>
          {column.label}
        </th>
      );
    };
    
    const Table: React.FC<TableProps>
    
     = ({ columns, data }) => {
      const [sortColumn, setSortColumn] = useState('');
    
      const handleSort = (columnId: string) => {
        setSortColumn(columnId);
        // Sorting logic would go here according to the selected column
      };
    
      return (
        <table>
          <thead>
            <tr>
              {columns.map(column => (
                <TableHeader key={column.id} column={column} onSort={handleSort} />
              ))}
            </tr>
          </thead>
          <tbody>
            {data.map((row, index) => (
              <tr key={index}>
                {columns.map(column => (
                  <td key={column.id}>{row[column.id]}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      );
    };
    
    // Example usage
    const Example: React.FC = () => {
      const columns: Column[] = [
        { id: 'name', label: 'Name', sortable: true },
        { id: 'age', label: 'Age', sortable: true },
        { id: 'country', label: 'Country', sortable: false },
      ];
    
      const data = [
        { name: 'John', age: 30, country: 'USA' },
        { name: 'Alice', age: 25, country: 'Canada' },
        { name: 'Bob', age: 35, country: 'UK' },
      ];
    
      return <Table columns={columns} data={data} />;
    };
    
    export default Example;

    이 예제는 열을 정렬할 수 있는 테이블을 보여줍니다. Table 컴포넌트는 columns와 data를 props로 받으며, TableHeader 컴포넌트는 열 헤더를 클릭하여 정렬 프로세스를 시작합니다. 정렬 상태는 Table 컴포넌트에서 유지됩니다. 이 예제는 Props Getters 패턴을 사용하여 자식 컴포넌트인 TableHeader가 부모 컴포넌트인 Table의 특정 함수를 사용하여 테이블의 동작을 제어하는 방법을 보여줍니다.

     

    State Initializer 패턴

    상태 초기화 패턴은 React에서 함수형 컴포넌트의 초기 상태를 정의하고 구성하는 기술입니다. 컴포넌트 내에서 직접 상태를 초기화하는 대신, "상태 초기화 함수"라는 특수한 함수를 사용하여 초기 상태를 정의합니다. 이 함수는 React 상태 훅(useState)의 인수로 전달되어 컴포넌트의 초기 상태를 보다 유연하고 동적으로 설정할 수 있습니다.

    사용 시기

    • 컴포넌트의 초기 상태가 계산된 값이나 더 복잡한 로직에 의존할 때
    • 초기 상태가 props나 다른 상태에 기반할 때
    • 상태 초기화 로직을 컴포넌트의 나머지 부분과 분리하여 컴포넌트를 더 깔끔하고 모듈화하고 싶을 때

    사용하지 말아야 할 때

    • 컴포넌트의 초기 상태가 정적이며 추가 로직이 필요 없는 경우
    • 상태 초기화 패턴이 불필요한 복잡성을 초래하여 컴포넌트를 이해하기 어렵게 만들 때

    장점

    • 컴포넌트의 초기 상태를 정의하는 명확하고 조직적인 방법을 제공합니다.
    • 초기 상태를 더 동적으로 설정할 수 있어 다양한 상황에 쉽게 적응할 수 있습니다.
    • 상태 초기화 로직을 컴포넌트의 나머지 부분과 분리하여 모듈성 및 코드 재사용을 촉진합니다.

    단점

    • 컴포넌트의 데이터 흐름을 따라가기 어렵게 만드는 추가적인 추상화 레이어를 도입할 수 있습니다.
    • React 훅과 함수형 컴포넌트의 상태 관리에 대한 깊은 이해가 필요합니다.

    예제

    여기에서는 커스텀 훅을 사용하여 폼의 상태를 초기화하는 예제를 보여드립니다.

    import React, { useState } from 'react';
    
    // Interface definition for the form state
    interface FormState {
      username: string;
      password: string;
    }
    
    // Custom hook to handle form state
    const useFormState = (): [FormState, (e: React.ChangeEvent<HTMLInputElement>) => void] => {
      // Initial state of the form
      const initialFormState: FormState = {
        username: '',
        password: '',
      };
    
      // State hook for the form
      const [formState, setFormState] = useState<FormState>(initialFormState);
    
      // Function to handle changes in form fields
      const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setFormState(prevState => ({
          ...prevState,
          [name]: value,
        }));
      };
    
      return [formState, handleInputChange];
    };
    
    // Example component using the form hook
    const FormExample: React.FC = () => {
      // Using the custom hook to get the form state and function to handle changes
      const [formState, handleInputChange] = useFormState();
    
      // Function to handle form submission
      const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        console.log('Form submitted:', formState);
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <div>
            <label htmlFor="username">Username:</label>
            <input
              type="text"
              id="username"
              name="username"
              value={formState.username}
              onChange={handleInputChange}
            />
          </div>
          <div>
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              name="password"
              value={formState.password}
              onChange={handleInputChange}
            />
          </div>
          <button type="submit">Submit</button>
        </form>
      );
    };
    
    export default FormExample;

    이 예제에서 커스텀 훅 useFormState는 폼 상태를 관리합니다. 이 훅은 폼 상태와 폼 필드의 변경 사항을 처리하는 함수를 포함하는 배열을 반환합니다. FormExample 컴포넌트는 이 훅을 사용하여 폼 상태와 변경 사항을 처리하는 함수를 얻어 폼 로직을 크게 단순화하고 유지 관리 및 이해를 쉽게 만듭니다.

     

    State Reducer 패턴

    상태 리듀서 패턴은 React에서 Redux와 유사한 "리듀서"에 상태 제어를 위임하여 컴포넌트 상태를 관리하는 기술입니다. 이 리듀서는 액션과 현재 상태를 받아 새로운 상태를 반환하는 함수입니다. 이 접근 방식은 한 곳에서 상태 업데이트 로직을 중앙 집중화하여 애플리케이션의 유지 보수성과 확장성을 향상시킵니다.

    사용 시기

    • 복잡한 상태 로직이 있는 애플리케이션에서 보다 고급 상태 관리가 필요할 때
    • 상태 업데이트의 예측 가능성과 추적 가능성이 필요한 애플리케이션에서
    • 상태 업데이트 로직을 한 곳에서 중앙 집중화하고 싶을 때

    사용하지 말아야 할 때

    • 상태 관리가 복잡하지 않고 더 직접적인 접근 방식으로 충분한 작은 또는 간단한 애플리케이션에서
    • 상태 리듀서 패턴이 과도한 복잡성을 초래하여 애플리케이션의 데이터 흐름을 이해하기 어렵게 만들 때

    장점

    • 상태 업데이트 로직을 중앙 집중화하여 애플리케이션 유지 보수성을 향상시킵니다.
    • 상태 변경을 추적하고 디버깅을 용이하게 합니다.
    • 특히 많은 상태 로직이 있는 애플리케이션에서 더 확장 가능하고 구조화된 설계를 촉진합니다.

    단점

    • 리듀서 로직과 관련 인프라를 구현할 때 초기 오버헤드가 발생할 수 있습니다.
    • Redux 개념과 상태 관리 패턴에 대한 깊은 이해가 필요합니다.

    예제

    import React, { useReducer } from 'react';
    
    // Definition of the action type for the reducer
    type Action =
      | { type: 'ADD_TODO'; payload: string }
      | { type: 'TOGGLE_TODO'; payload: number }
      | { type: 'REMOVE_TODO'; payload: number };
    
    // Interface definition for the todo state
    interface Todo {
      id: number;
      text: string;
      completed: boolean;
    }
    
    // Interface definition for the global state
    interface State {
      todos: Todo[];
    }
    
    // Reducing function to handle actions and update state
    const reducer = (state: State, action: Action): State => {
      switch (action.type) {
        case 'ADD_TODO':
          return {
            ...state,
            todos: [
              ...state.todos,
              {
                id: state.todos.length + 1,
                text: action.payload,
                completed: false,
              },
            ],
          };
        case 'TOGGLE_TODO':
          return {
            ...state,
            todos: state.todos.map(todo =>
              todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
            ),
          };
        case 'REMOVE_TODO':
          return {
            ...state,
            todos: state.todos.filter(todo => todo.id !== action.payload),
          };
        default:
          return state;
      }
    };
    
    // Example
    
     component using the State Reducer Pattern
    const TodoList: React.FC = () => {
      // Use the useReducer hook to manage state with the defined reducer
      const [state, dispatch] = useReducer(reducer, { todos: [] });
    
      // Functions to handle user interactions
      const addTodo = (text: string) => {
        dispatch({ type: 'ADD_TODO', payload: text });
      };
    
      const toggleTodo = (id: number) => {
        dispatch({ type: 'TOGGLE_TODO', payload: id });
      };
    
      const removeTodo = (id: number) => {
        dispatch({ type: 'REMOVE_TODO', payload: id });
      };
    
      return (
        <div>
          <h2>Todo List</h2>
          <ul>
            {state.todos.map(todo => (
              <li key={todo.id}>
                <span
                  style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
                  onClick={() => toggleTodo(todo.id)}
                >
                  {todo.text}
                </span>
                <button onClick={() => removeTodo(todo.id)}>Remove</button>
              </li>
            ))}
          </ul>
          <input
            type="text"
            placeholder="Add todo..."
            onKeyDown={(e) => {
              if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') {
                addTodo(e.currentTarget.value.trim());
                e.currentTarget.value = '';
              }
            }}
          />
        </div>
      );
    };
    
    export default TodoList;

    이 예제는 할 일 목록을 추가하고, 완료로 표시하고, 제거하는 기능을 보여줍니다. 상태 리듀서 패턴을 사용하여 할 일 목록의 상태를 관리합니다. 리듀서는 상태 업데이트 로직을 한 곳에서 처리하여 애플리케이션의 유지 보수성과 확장성을 높입니다. TodoList 컴포넌트는 useReducer 훅을 사용하여 글로벌 상태를 관리하고 관련 기능을 렌더링합니다. 이 접근 방식은 할 일 목록의 상태와 사용자 상호작용 로직을 쉽게 관리할 수 있게 합니다.

    디자인 패턴 적용의 중요성

    React와 TypeScript를 활용한 개발에서 디자인 패턴을 적용하는 것은 소프트웨어의 품질, 유지 보수성 및 확장성에 직접적으로 영향을 미치는 일련의 중요한 이점을 가져다줍니다. 상태 리듀서, 렌더 Props, 제어 Props 등의 패턴을 분석하면 이러한 패턴이 현대 애플리케이션 개발에서 가지는 가치를 명확히 알 수 있습니다.

     

    우선, 이러한 패턴은 코드를 보다 효율적이고 응집력 있게 조직할 수 있는 구조와 지침을 제공합니다. 사용자 인터페이스 개발에서 공통적으로 발생하는 문제에 대한 입증된 해결책을 제공하여, 개발자가 새로운 것을 발명하는 대신 견고한 개발 관행을 준수하도록 도와줍니다.

    또한, 디자인 패턴을 적용하면 코드 재사용성과 모듈성이 촉진되어 소프트웨어 확장 및 유지 보수가 용이해집니다. 컴포넌트를 분리하고 특정 기능적 관심사를 분리할 수 있는 능력은 보다 민첩한 개발을 가능하게 하고 오류 발생 가능성을 줄입니다.

     

    디자인 패턴을 사용하면 코드의 명확성과 가독성도 크게 향상됩니다. 정립된 관례와 인식된 패턴을 따르면, 프로젝트에 참여할 다른 개발자가 코드를 이해하기 더 쉬워집니다. 이는 개발 팀 간의 협업을 촉진하고 새로운 팀원이 적응하는 학습 곡선을 줄이는 데도 도움이 됩니다.

    또한, 이러한 패턴은 애플리케이션의 일관성과 예측 가능성을 높여 복잡한 상황에서도 효과적으로 문제를 해결할 수 있도록 합니다. 이를 통해 애플리케이션이 성장하는 비즈니스 요구와 요구사항을 충족할 수 있도록 확장할 수 있습니다.

     

    결론적으로, React의 디자인 패턴은 현대적이고 견고한 사용자 인터페이스를 구축하려는 개발자에게 중요한 도구와 접근 방식을 제공합니다. 이러한 패턴을 적절히 이해하고 적용함으로써, 팀은 프론트엔드 프로젝트의 품질과 효율성을 극대화하고 애플리케이션의 지속적인 성장과 발전을 위한 견고한 기반을 마련할 수 있습니다.

     

    읽어주셔서 감사합니다! 😊
    개발 관련 궁금증이나 고민이 있으신가요?
    아래 링크를 통해 저에게 바로 문의해 주세요! 쉽고 빠르게 도움 드리겠습니다.

    '개발자서동우' 프로필 보기

     

    디자인패턴 / 리팩토링 / 아키텍처를 공부하실 수 있는 사이트를 소개합니다 :)

    사이트 이동하기

    devloo.io