본문 바로가기
Translate

[번역] 리액트(React) 디자인 패턴

by viae 2023. 11. 14.

출처: React Design Patterns

 

 

소개

React 개발자는 테스트되고 신뢰할 수 있는 솔루션을 사용하여 문제를 해결하는 빠른 접근 방식을 제공하는 디자인 패턴을 사용하여 시간과 노력을 절약할 수 있습니다. 디자인 패턴을 사용하면 결합이 적은 응집력 있는 모듈을 만들 수 있으며, 이를 통해 React 개발자는 유지보수 가능하고 확장 가능하며 효율적인 애플리케이션을 만들 수 있습니다. 이 글에서는 React 디자인 패턴을 살펴보고 이 패턴이 React 애플리케이션 개발을 어떻게 개선할 수 있는지 살펴보겠습니다.

 

 

컨테이너 및 프레젠테이션 패턴

컨테이너 및 프레젠테이션 패턴은 리액트 코드에서 프레젠테이션 로직과 비즈니스 로직을 분리하여 모듈화되고 테스트 가능하며, 관심사 분리 원칙을 따르는 것을 목표로 하는 패턴입니다.

대부분 리액트 애플리케이션에서는 백엔드/스토어에서 데이터를 가져오거나 로직을 계산하고 그 계산 결과를 리액트 컴포넌트에서 표현해야 하는 경우가 발생합니다. 이런 경우 컨테이너와 프레젠테이션 패턴은 컴포넌트를 두 가지로 분류하는 데 사용할 수 있기 때문에 빛을 발합니다

  • 컨테이너 컴포넌트는 데이터 불러오기 또는 계산을 담당하는 컴포넌트 역할을 합니다.
  • 가져온 데이터나 계산된 값을 UI(사용자 인터페이스)에 렌더링하는 역할을 하는 프레젠테이션 컴포넌트.

컨테이너와 프레젠테이션 패턴의 예시는 아래와 같습니다

import React, { useEffect } from 'react';
import CharacterList from './CharacterList';

const StarWarsCharactersContainer:React.FC = () => {
    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [error, setError] = useState<boolean>(false);

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch("https://akabab.github.io/starwars-api/api/all.json");
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
    }, []);

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;
// the component is responsible for displaying the characters

import React from 'react';
import { Character } from './types';

interface CharacterListProps {
    loading: boolean;
    error: boolean;
    users: Character[];
}

const CharacterList: React.FC<CharacterListProps> = ({ loading, error, characters }) => {

    if (loading && !error) return <div>Loading...</div>;
    if (!loading && error) return <div>error occured.unable to load characters</div>;
    if (!characters) return null;

    return (
        <ul>
            {characters.map((user) => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
};

export default CharacterList;

 

 

Hook을 사용한 컴포넌트 구성

Hook은 React 16.8에서 처음 등장한 새로운 기능입니다. 그 이후로 리액트 애플리케이션을 개발하는 데 중요한 역할을 해왔습니다. Hook은 함수형 컴포넌트에 상태 및 생명주기 메소드에 대한 접근 권한을 부여하는 기본 함수입니다(이전에는 class 컴포넌트만 사용할 수 있었음). 반면에 Hook은 컴포넌트 요구사항을 충족하도록 특별히 설계될 수 있으며 추가적인 사용 사례를 가질 수 있습니다.

이제 모든 상태 저장 로직(반응형 상태 변수가 필요한 로직 유형)을 분리하고 커스텀 훅을 사용하여 컴포넌트에서 작성하거나 사용할 수 있습니다. 결과적으로 후크가 컴포넌트에 느슨하게 연결되어 있으므로 개별적으로 테스트할 수 있으므로 코드가 더 모듈화되고 테스트가 용이해집니다.

Hook을 사용한 컴포넌트 구성의 예는 아래와 같습니다.

// creating a custom hook that fetches star wars characters

export const useFetchStarWarsCharacters = () => {

    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(false);
    const controller = new AbortController()

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch(
                "https://akabab.github.io/starwars-api/api/all.json", 
                {
                    method: "GET", 
                    credentials: "include",
                    mode: "cors",
                    headers: {
                        'Content-Type': 'application/json',
                        'Access-Control-Allow-Origin': '*'
                    },
                    signal: controller.signal
                }
            );
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
        return () => {
            controller.abort();
        }
    }, []);

    return [
        characters,
        isLoading,
        error
    ];
};

 

커스텀 훅을 생성한 후 StarWarsCharactersContainer 컴포넌트로 임포트하여 사용하겠습니다

// importing the custom hook to a component and fetch the characters 

import React from 'react';
import { Character } from './types';
import { useFetchStarWarsCharacters } from './useFetchStarWarsCharacters';

const StarWarsCharactersContainer:React.FC = () => {

    const [ characters, isLoading, error ] = useFetchStarWarsCharacters();

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;

 

 

Reducers를 사용한 상태 관리

대부분의 경우 컴포넌트에서 많은 상태를 처리하면 그룹화되지 않은 많은 상태와 관련된 문제가 발생하여 처리하기가 부담스럽고 어려울 수 있습니다. 이러한 상황에서는 reducer 패턴이 유용한 옵션이 될 수 있습니다. reducer를 사용하는 상태를 실행 시 그룹화된 상태를 변경할 수 있는 특정 액션으로 분류할 수 있습니다.

이 패턴을 사용하는 개발자는 컴포넌트 및 Hook의 상태 관리를 제어하여 이벤트가 전송될 때 상태 변경을 관리할 수 있습니다.

reducer 패턴을 사용하는 예는 아래와 같습니다.

 

위 코드에서 컴포넌트는 두 가지 액션을 전송합니다.

  • '로그인' 액션 유형은 로그인, 사용자, 토큰의 세 가지 상태 값에 영향을 미치는 상태 변경을 트리거합니다.
  • '로그아웃' 액션은 단순히 상태를 초기값으로 재설정합니다.

 

 

프로바이더를 사용한 데이터 관리

프로바이더 패턴은 애플리케이션의 컴포넌트 트리를 통해 데이터를 전달하기 위해 컨텍스트 API를 활용하므로 데이터 관리에 매우 유용합니다. 이 패턴은 리액트 개발에서 흔히 발생하는 문제인 prop drilling에 대한 효과적인 해결책입니다.

프로바이더 패턴을 구현하기 위해 먼저 프로바이더 컴포넌트를 생성합니다. 프로바이더는 컨텍스트 객체가 우리에게 제공하는 상위 컴포넌트입니다. React에서 제공하는 createContext 메서드를 활용해 Context 객체를 생성할 수 있습니다.

export const ThemeContext = React.createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

 

프로바이더를 생성한 후, 생성된 프로바이더 컴포넌트로 컨텍스트 API의 데이터에 종속된 컴포넌트를 묶습니다.

컨텍스트 API에서 데이터를 가져오기 위해 컨텍스트를 매개변수(이 경우 ThemeContext)로 받아들이는 useContext 훅을 호출합니다.

import { useContext } from 'react';
import { ThemeProvider, ThemeContext } from "../context";


const HeaderSection = () => {
  <ThemeProvider>
    <TopNav />
  </ThemeProvider>;
};


const TopNav = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
      ...
    </div>
  );
};

 

 

HOC(고차 컴포넌트)를 사용한 컴포넌트 향상

상위 컴포넌트는 컴포넌트를 인자로 받아 추가 데이터나 기능이 주입된 강화된 컴포넌트를 반환합니다. React에서 상위 컴포넌트를 사용할 수 있는 이유는 상속보다 컴포지션을 선호하는 React의 특성 때문입니다.

상위 컴포넌트(HOC) 패턴은 컴포넌트의 기능을 증가시키거나 수정하는 메커니즘을 제공하여 컴포넌트 재사용 및 코드 공유를 용이하게 합니다.

HOC 패턴의 예는 아래와 같습니다.

import React from 'react'

const higherOrderComponent = Component => {
  return class HOC extends React.Component {
    state = {
      name: 'John Doe'
    }

    render() {
      return <Component name={this.state.name {...this.props} />
    }
 }


const AvatarComponent = (props) => {
  return (
    <div class="flex items-center justify-between">
      <div class="rounded-full bg-red p-4">
          {props.name}
      </div>
      <div>
          <p>I am a {props.description}.</p>
      </div>
    </div>
  )
}


const SampleHOC = higherOrderComponent(AvatarComponent);


const App = () => {
  return (
    <div>
      <SampleHOC description="Frontend Engineer" />
    </div>
  )
}

export default App;

 

위의 코드에서, higherOrderComponent에서 프로퍼티를 제공하면 내부적으로 이를 활용하게 됩니다.

 

 

Compound Components

컴파운드 컴포넌트 패턴은 자식 컴포넌트로 구성된 부모 컴포넌트를 관리하기 위한 React 디자인 패턴입니다.

이 패턴의 원리는 부모 컴포넌트를 더 작은 컴포넌트로 분해한 다음 props, context 또는 기타 리액트 데이터 관리 기술을 사용하여 이러한 작은 컴포넌트 간의 상호 작용을 관리하는 것입니다.

이 패턴은 작은 컴포넌트로 구성된 재사용 가능한 다용도 컴포넌트를 만들어야 할 때 유용합니다. 이를 통해 개발자는 명확하고 간단한 코드 구조를 유지하면서 쉽게 커스터마이징하고 확장할 수 있는 정교한 UI 컴포넌트를 만들 수 있습니다.

 

Compound Components 패턴의 사용 사례의 예는 다음과 같습니다.

import React, { createContext, useState } from 'react';

const ToggleContext = createContext();

function Toggle({ children }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn(!on);

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

Toggle.On = function ToggleOn({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? children : null;
}

Toggle.Off = function ToggleOff({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? null : children;
}

Toggle.Button = function ToggleButton(props) {
  const { on, toggle } = useContext(ToggleContext);
  return <button onClick={toggle} {...props} />;
}

function App() {
  return (
    <Toggle>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button>Toggle</Toggle.Button>
    </Toggle>
  );
}

 

 

Prop combination

여러 개의 props로 하나의 객체를 만들어 컴포넌트에 하나의 prop로 전달하는 방식입니다.


이 패턴을 사용하면 코드를 깔끔하게 정리하고 props를 더 간단하게 관리할 수 있으므로 컴포넌트에 많은 관련 props를 전달하려는 경우에 특히 유용합니다.

import React from 'react';

function P(props) {
  const { color, size, children, ...rest } = props;
  return (
    <p style={{ color, fontSize: size }} {...rest}>
      { children }
    </p>
  );
}

function App() {
  const paragraphProps = {
    color: "red",
    size: "20px",
    lineHeight: "22px"
  };
  return <P {...paragraphProps}>This is a P</P>;
}

 

 

Controlled inputs

Controlled Input 패턴은 입력 필드를 처리하는 데 사용할 수 있습니다. 이 패턴은 입력 필드의 값이 변경되면 이벤트 핸들러를 사용해 컴포넌트 상태를 업데이트하고 입력 필드의 현재 값을 컴포넌트 상태에 저장하는 것을 포함합니다.

React는 컴포넌트의 상태와 동작을 제어하기 때문에, 이 패턴은 컴포넌트의 상태를 사용하지 않고 DOM(문서 객체 모델)을 통해 직접 제어하는 비제어 입력 패턴보다 코드를 더 예측 가능하고 가독성 있게 만듭니다.

제어 입력 패턴의 사용 사례의 예는 다음과 같습니다.

import React, { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <input type="text" value={inputValue} onChange={handleChange} />
  );
}

 

 

ForwardRef로 커스텀 컴포넌트 관리하기

ForwardRef라고 하는 상위 컴포넌트는 다른 컴포넌트를 입력으로 받아 원래 컴포넌트의 참조를 전달하는 새 컴포넌트를 출력합니다. 이렇게 하면 하위 컴포넌트의 참조를 사용하여 기본 DOM 노드 또는 컴포넌트 인스턴스를 검색할 수 있으며, 부모 컴포넌트에서 액세스할 수 있게 됩니다.

애플리케이션 내에서 타사 라이브러리 또는 다른 사용자 정의 컴포넌트와 상호 작용하는 사용자 정의 컴포넌트를 만들 때 워크플로에 ForwardRef 패턴을 포함하면 매우 유용합니다. 라이브러리의 DOM 노드 또는 다른 컴포넌트의 DOM 인스턴스에 대한 액세스 권한을 부여함으로써 해당 컴포넌트에 대한 제어권을 사용자에게 이전하는 데 도움이 됩니다.

ForwardRef 패턴의 사용 사례의 예는 아래와 같습니다.

import React from "react";

const CustomInput = React.forwardRef((props, ref) => (
  <input type="text" {...props} ref={ref} />
));

const ParentComponent = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <CustomInput ref={inputRef} />;
};

 

위의 코드에서는 forwardRefs를 사용하여 <ParentComponent/> 컴포넌트에서 다른 컴포넌트 <CustomInput/>으로 포커스를 트리거했습니다.

 

 

결론

이 글에서는 Higher-Order Components, Container-Presentational Component Patterns, Compound Components, Controlled Components 등을 포함한 React 디자인 패턴에 대해 설명했습니다. 이러한 디자인 패턴과 모범 사례를 React 프로젝트에 통합하면 코드 품질을 향상하고 팀 협업을 촉진하며 앱의 확장성, 유연성, 유지보수성을 높일 수

있습니다.