Skip to content

리액트 디자인 패턴 #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dlckdduq1107 opened this issue Aug 16, 2023 · 2 comments
Open

리액트 디자인 패턴 #6

dlckdduq1107 opened this issue Aug 16, 2023 · 2 comments

Comments

@dlckdduq1107
Copy link
Collaborator

No description provided.

@dlckdduq1107
Copy link
Collaborator Author

dlckdduq1107 commented Aug 16, 2023

Container/Presentational 패턴

  • 일단 Class 기반
  • 리액트에서 관심사를 분리하는 디자인 패턴(비즈니스 로직에서 뷰를 분리)

컴포넌트를 두개로 나눔

  • Presentational Component
    • 데이터가 어떻게 사용자에게 보여질지 다루는 부분
  • Container Component
    • 어떤 데이터가 보여질지 다루는 컴포넌트

image

예시

이미지 리스트 렌더링

//DogImages.jsx(Presentational)
export default function DogImages({ dogs }) {
  return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}

prop을 통해 데이터 받고 그대로 화면에 그리는 역할

UI변경을 위한 상태말고는 다른 상태를 가지지 않는다.

prop으로 받은 데이터는 Presentational Component에 의해 수정되지 않는다.

// Container

export default class DogImagesContainer extends React.Component {
  constructor() {
    super();
    this.state = {
      dogs: []
    };
  }

  componentDidMount() {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then(res => res.json())
      .then(({ message }) => this.setState({ dogs: message }));
  }

  render() {
    return <DogImages dogs={this.state.dogs} />;
  }
}

Presentational Component에 데이터를 전달하는게 목적

Presentational이외에는 아무것도 렌더링 하지 않는다.(style도 존재X)

API를 불러와 상태에 저장하고 Presentational에 prop으로 전달


여기까지 클래스 기반


해당 패턴은 Hook이 나오기전 주로 사용하던 방법으로 Hook으로 대체 가능하다.(예전엔(클래스) 이렇게 비즈니스 로직을 분리하곤 했다 정도)

Custom Hook을 사용하면

//Custom Hook
export default function useDogImages() {
  const [dogs, setDogs] = useState([])

  useEffect(() => {
    fetch('https://dog.ceo/api/breed/labrador/images/random/6')
      .then(res => res.json())
      .then(({ message }) => setDogs(message))
  }, [])

  return dogs
}

//렌더링 컴포넌트
export default function DogImages() {
  const dogs = useDogImages();

  return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}

쉽게 비즈니스 로직을 분리할 수 있다.

불필요한 Container Component작성을 줄일 수 있다.

장점

자연스럽게 관심사 분리

Presentational Component를 여러곳에서 재사용 가능

Presentational Component를 코드베이스에 대한 이해가 깊지 않은 개발자도 쉽게 수정 가능하고, 재사용된 전체를 한번에 수정가능

단점

Hook의 등장으로 해당 패턴 없이도 쉽게 구현가능해짐

Hook을 사용하더라도 사용가능하지만 오버엔지니어링이 될 가능성이 높다.

이 아티클을 예전에 썼었고 이제는 내 관점이 달라졌다. 특히 나는 더 이상 이런 방식으로 component를 나누는 것을 추천하지 않는다.

만약 자연스럽게 이 패턴의 필요성을 찾는다면 이 패턴은 유용할 것이다.

하지만 어떤 필요성 없이, 독단적으로 이 패턴이 강제 되고 있음을 여러번 보았다. 내가 이 패턴을 유용하게 보았던 주된 이유는 이 패턴이 다른 관점의 컴포넌트로부터 복잡하고 stateful한 로직을 분리하게 해주었기 때문이다.

임의의 분리없이 Hook은 똑같은 일을 할 수 있다. 이 글은 역사적인 이유로 보길 바라며 심각하게 받아들이지 마라. - Dan Abramov


주로 Hook을 쓰는 지금 자연스럽게 적용되는 디자인 패턴이기도 하고 현재는 거의 필요하지 않으므로 한가지만 더 간단하게 알아보자

HOC패턴(Higher Order Component)

앱 전반적으로 재사용 가능한 로직을 prop으로 컴포넌트에게 제공한다

여러 컴포넌트에서 같은 로직을 사용해야하는 경우(스타일을 적용하거나, 권한을 요청하거나 등등)

고차 컴포넌트란?

다른 컴포넌트를 받는 컴포넌트

예제

여러 컴포넌트에 동일한 스타일을 적용하고 싶다.

HOC가 style 객체를 직접 만들어 컴포넌트에 전달

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

이전 Container/Presentational 패턴의 예제에서 이미지를 가져오는 동안은 로딩표시를 하고 싶다면?

export default function DogImages() {
  const dogs = useDogImages();

  return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}

로딩을 표시하는 고차 컴포넌트(HOC)

export default function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);

    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);

    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

fetch가 완료되기 전에는 로딩중임을 보여주는 컴포넌트

function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

고차 컴포넌트 조합

호버링 추가하고 싶을때

export default function withHover(Element) {
  return props => {
    const [hovering, setHover] = useState(false);

    return (
      <Element
        {...props}
        hovering={hovering}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
      />
    );
  };
}

두번 감싸기

export default withHover(
  withLoader(DogImages, "https://dog.ceo/api/breed/labrador/images/random/6")
);

Hook

고차 컴포넌트 대신 호버링 Hook으로 구현(커스텀 훅)

export default function useHover() {
  const [hovering, setHover] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = () => setHover(true);
  const handleMouseOut = () => setHover(false);

  useEffect(() => {
    const node = ref.current;
    if (node) {
      node.addEventListener("mouseover", handleMouseOver);
      node.addEventListener("mouseout", handleMouseOut);

      return () => {
        node.removeEventListener("mouseover", handleMouseOver);
        node.removeEventListener("mouseout", handleMouseOut);
      };
    }
  }, [ref.current]);

  return [ref, hovering];
}

간단하게 커스텀 훅 호출해 호버링 구현

function DogImages(props) {
  const [hoverRef, hovering] = useHover();

  return (
    <div ref={hoverRef} {...props}>
      {hovering && <div id="hover">Hovering!</div>}
      <div id="list">
        {props.data.message.map((dog, index) => (
          <img src={dog} alt="Dog" key={index} />
        ))}
      </div>
    </div>
  );
}

but, 일반적으로 Hook은 HOC를 완전히 대체하지 못한다.

훅은 컴포넌트의 내부에서 특정한 동작을 추가할 수 있게 해 주지만. HOC에 비해 버그를 발생시킬 확률을 증가시킨다. 라고 하는데 정확히는 모르겠다

HOC사용 사례

전반적으로 동일하고 커스텀 불가능한 동작이 여러 컴포넌트에 필요할때

컴포넌트가 어떤 로직 추가 없이 단독으로 실행되어야 할 경우

React의 fallback…?

Hook사용사례

공통 기능이 각 컴포넌트에서 쓰이기 전에 커스터마이징 되어야 하는 경우

공통 기능이 앱 전반적으로 쓰이는 것이 아닌 하나나 혹은 몇개의 컴포넌트에서 요구되는 경우

해당 기능이 기능을 쓰는 컴포넌트에게 여러 프로퍼티를 전달해야 하는 경우

단점

여러 고차 컴포넌트를 조합할 경우 모든 prop이 최종적으로 병합되므로 어떤 고차 컴포넌트가 어떤 prop와 관련있는지 파악이 어렵다.

고차컴포넌트를 사용할때 감싸는 순서가 중요해지는 경우 버그를 만들어내기 쉽다.

두가지 패턴을 살펴본 결과 Hook이란게 매우 많은 역할을 하고 있다


함수형 컴포넌트에서 상태를 사용할 수 있도록 하는 역할만 하는줄 알았지만 디자인 패턴 관점에서도 훌륭한 이점을 가지고 있다는걸 깨달았다.(HOC를 완전히 대체할수는 없지만)

@doong-jo
Copy link
Collaborator

doong-jo commented Aug 16, 2023

compound pattern

하나의 큰 컴포넌트가 가지는 로직와 디자인이 결합도가 높아 변경에 유연하지 않은 문제를 compound pattern으로 여러 컴포넌트를 복합적으로 구성하는 방식으로 해결하는 패턴

예제 codesandbox

compound-pattern

https://www.radix-ui.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants