많은 스타트업에서 그러하듯 초기 프로토타입 플랫폼은 인하우스가 아닌 외주 업체를 통해 제작하는 경우가 많다.
현재 필자가 개발자로 근무하는 회사의 플랫폼도 외주 업체에 의해 제작된 애플리케이션이다.
다행히 꼼꼼하고 실력 있는 개발자께서 만들어 주셔서 유지 보수에 문제가 있는 것은 아니지만,
몇몇 부분에서 코드를 읽고 이해하기 난해한 부분들이 있다.
특히, 디자인 패턴에 있어서는 일관성 문제로 코드를 이해하는데 시간을 많이 잡아먹는 문제가 발생하고 있다.
파일명 혹은 클래스명을 토대로 이해하기로는 container - presenter 패턴을 이용한 것으로 생각되나,
container와 presenter의 구분이 모호해지는 회색 영역이 코드의 대부분을 차지하며 props drilling 지옥이 필자를 괴롭히고 있다.
따라서, 이번 베타 버전 배포가 끝나면 두 달간에 걸쳐 전체적인 리팩터링을 통해 디자인 패턴을 다시 정확하게 잡아보려고 한다.
관련해 좋은 글이 있어 번역해 옮겨 본다.
React component design patterns for 2022
Introduction
디자인 패턴이란 일반적인 소프트웨어 개발 문제 해결을 위한 일종의 탬플릿이다. 리엑트에서 디자인 패턴이란, 경험 많은 리엑트 개발자에 의해 증명된 문제 해결 방법을 뜻한다.
리엑트 API가 진화하며, 새로운 패턴들이 나타나고 개발자들은 이전의 패턴을 대체하고 있다. 이번 글에서는 2022년의 유용한 리엑트 디자인 패턴에 대해 알아보려고 한다.
2022 React components design patterns
이번 섹션에선, 2022년에 가장 흔하게 사용되는 리엑트 컴포넌트 디자인 패턴에 대해 알아볼 것이다. 아래 글에는 cross-cutting concerns, 전역 데이터 공유(props drilling 없이), 다른 컴포넌트에 복잡한 stateful 로직 저장 등 관심사의 분리에 효율적인 리엑트 디자인 패턴 리스트가 소개되어 있다.
아래가 그 패턴 리스트이다:
The higher-order component pattern
Higher-order 컴포넌트, 혹은 HOC 패턴이라고도 부르는데, 이는 컴포넌트 로직을 전체 애플리케이션에서 재활용하는데 좋은 리엑트 디자인 패턴이다. HOC 패턴은 전체 어플리케이션에 컴포넌트 로직을 공유할 수 있는 기능이 있어 cross-cutting concerns에 유용하다. 해당 기능으로는 authorization, logging, 그리고 data retrieval 등이 있다.
HOC는 React API의 핵심은 아니지만, 자바스크립트 함수인 리엑트 함수형 컴포넌트의 본연적 특성에 따라 부상하게 되었다.
high-order 컴포넌트는 자바스크립트의 higher-order 함수와 비슷하다. 사이드 이펙트가 없는 순수 함수이다. 그리고 자바스크립트에서의 higher-order 함수와 마찬가지로, HOC는 데코레이터 함수와 같이 동작한다.
리엑트에서, higher-order 컴포넌트는 아래와 같이 구성된다.
import React, {Component} from 'react';
const higherOrderComponent = (DecoratedComponent) => {
class HOC extends Component {
render() {
return <DecoratedComponent />;
}
}
return HOC;
};
The provider pattern
provider 패턴은 리엑트 컴포넌트 트리에서 다수의 컴포넌트에 전역 데이터를 공유하는데 특화된 패턴이다.
provider 패턴은 Provider 컴포넌트를 가지는데, 이는 전역 데이터를 Consumer 컴포넌트 혹은 커스텀 훅을 이용해 하위 컴포넌트에게 공유한다.
provider 패턴은 리엑트에선 새로운 개념이 아니다. React-Redux와 MobX와 같은 라이브러리에서도 provider 패턴을 이용한다.
아래 코드는 React-Redux에서 provider 패턴을 셋업 하는 코드이다.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
리엑트에서 provider 패턴은 React context API로도 구현가능하다.
리엑트 기본적으로 부모 컴포넌트에서 자식 컴포넌트로 아래로의 한방향 데이터 플로우를 지원한다. 결과적으로 컴포넌트 트리의 아래 깊숙히 위치하고 있는 자식 컴포넌트에 데이터를 전달하기 위해선 각 층의 컴포넌트 트리에 props를 전다랳 줘야 하는데, 이를 prop drilling이라고 부른다.
React context API는 이 문제를 해결하기 위해 provider 패턴을 이용한다. 따라서 리엑트 컴포넌트에서 prop drilling 없이 데이터 공유가 가능하도록 해준다.
Context API를 이용하기 위해서, context 객체를 React.createContext를 이용해 생성해야 한다. context 객체는 Provider 컴포넌트를 가지고 있는데, 전역 데이터 값을 가지게 된다. context 객체는 또한 Consumer 컴포넌트를 가지고 있는데 context 변경을 위해 Provider 컴포넌트를 섭스크라이브 하고 있다. Consumer 컴포넌트는 가장 최근의 context 값을 자식에게 props로 전달하게 된다.
아래는 React context API의 예이다:
import { createContext } from "react";
const LanguageContext = createContext({});
function GreetUser() {
return (
<LanguageContext.Consumer>
{({ lang }) => (
<p>Hello, Kindly select your language. Default is {lang}</p>
)}
</LanguageContext.Consumer>
);
}
export default function App() {
return (
<LanguageContext.Provider value={{ lang: "EN-US" }}>
<h1>Welcome</h1>
<GreetUser />
</LanguageContext.Provider>
);
}
React context API는 현재 유저 인증, 테마, 혹은 언어 선택과 같이 전역 데이터가 컴포넌트 트리 전역에 공유되어야 하는 기능에 사용된다.
참고로 리엑트는 더 직접적인 API도 제공한다. useContext 훅을 이용하면 Consumer 컴포너트를 이용하는 대신에 현재의 context 값을 섭스크라이브한다.
The compound components pattern
Compound 컴포넌트 패턴은 다수의 컴포넌트가 스테이트와 핸들 로직을 다수의 컴포넌트에 간단하고 효율적으로 제공하는 리엑트 컨테이너 패턴의 발전된 패턴이다.
Compound 컴포넌트 패턴은 부모 컴포넌트와 자식 컴포넌트 사이에 표현적이면서 유연한 API를 제공한다. 또한 compound 컴포넌트는 부모 컴포넌트가 스테이트를 자식과 명백하게 상호작용하고 공유함으로써 선언적 UI 작성에 적합하다.
두 가지 좋은 예로는 select와 options HTML 엘리먼트이다. select와 options HTML 엘리먼트는 필드에서 드롭다운으로 함께 작동한다.
아래 코드를 보자:
<select>
<option value="javaScript">JavaScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
</select>
위 코드에서, select 엘리먼트는 상태를 options 엘리먼트와 관리하고 공유한다. 결과적으로, 명백한 상태 선언이 없음에도 불구하고, select 엘리먼트는 유저가 선택하는 옵션이 무엇인지 알게 된다.
Compound 컴포넌트 패턴은 복잡한 리엑트 컴포넌트를 만들 때 유용하다. 예를 들어 switch, tab switcher, accordion, dropdowns, tag list, etc 등이 있다. context API 혹은 React.cloneElement API를 이용해 만들 수 있다.
이번 섹션에서는 아코디언 메뉴를 만들어 보면서 compound 컴포넌트 패턴에 대해 더 자세히 알아보자. context API를 이용해 compound 컴포넌트 패턴을 만들어 볼 것이다. 아래 과정을 따라 해 보자:
1. 새로운 리엑트 앱 설치:
yarn create react-app Accordion
cd Accordion
yarn start
2. 의존 라이브러리 설치:
yarn add styled-components
3. 더미 데이터 추가:
src 디렉터리에, data 폴더를 생성하고 아래 코드를 추가하자
const faqData = [
{
id: 1,
header: "What is LogRocket?",
body:
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
},
{
id: 2,
header: "LogRocket pricing?",
body:
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
},
{
id: 3,
header: "Where can I Find the Doc?",
body:
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
},
{
id: 4,
header: "How do I cancel my subscription?",
body:
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
},
{
id: 5,
header: "What are LogRocket features?",
body:
"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
}
];
export default faqData;
4. 컴포넌트 생성과 스타일 추가: src 디렉토리에 components 폴더와 Accordian.js 파일, Accordian.styles.js 파일을 생성하자. styled-components를 이용해 스타일을 만들 것이다. 아래 코드를 Accordian.styles.js 파일에 넣자.
import styled from "styled-components";
export const Container = styled.div
display: flex;
background: #6867ac;
border-bottom: 8px solid #ffbcd1;
font-family: "Inter", sans-serif;
; export const Wrapper = styled.div
margin-bottom: 40px;
; export const Inner = styled.div
display: flex;
padding: 70px 45px;
flex-direction: column;
max-width: 815px;
margin: auto;
; export const Title = styled.h1
font-size: 33px;
line-height: 1.1;
margin-top: 0;
margin-bottom: 8px;
color: white;
text-align: center;
; export const Item = styled.div
color: white;
margin: auto;
margin-bottom: 10px;
max-width: 728px;
width: 100%;
&:first-of-type {
margin-top: 3em;
}
&:last-of-type {
margin-bottom: 0;
}
; export const Header = styled.div
display: flex;
flex-direction: space-between;
cursor: pointer;
border: 1px solid #ce7bb0;
border-radius: 8px;
box-shadow: #ce7bb0;
margin-bottom: 1px;
font-size: 22px;
font-weight: normal;
background: #ce7bb0;
padding: 0.8em 1.2em 0.8em 1.2em;
user-select: none;
align-items: center;
; export const Body = styled.div
font-size: 18px;
font-weight: normal;
line-height: normal;
background: #ce7bb0;
margin: 0.5rem;
border-radius: 8px;
box-shadow: #ce7bb0;
white-space: pre-wrap;
user-select: none;
overflow: hidden;
&.open {
max-height: 0;
overflow: hidden;
}
span {
display: block;
padding: 0.8em 2.2em 0.8em 1.2em;
}
;
5. 아래 코드를 Accordian.js 파일에 추가하자:
import React, { useState, useContext, createContext } from "react";
import { Container, Inner, Item, Body, Wrapper, Title, Header
} from "./Accordion.styles";
const ToggleContext = createContext();
export default function Accordion({ children, ...restProps }) {
return (
<Container {...restProps}>
<Inner>{children}</Inner>
</Container>
);
}
Accordion.Title = function AccordionTitle({ children, ...restProps }) {
return <Title {...restProps}>{children}</Title>;
};
Accordion.Wrapper = function AccordionWrapper({ children, ...restProps }) {
return <Wrapper {...restProps}>{children}</Wrapper>;
};
Accordion.Item = function AccordionItem({ children, ...restProps }) {
const [toggleShow, setToggleShow] = useState(true);
const toggleIsShown = (isShown) => setToggleShow(!isShown);
return (
<ToggleContext.Provider value={{ toggleShow, toggleIsShown }}>
<Item {...restProps}>{children}</Item>
</ToggleContext.Provider>
);
};
Accordion.ItemHeader = function AccordionHeader({ children, ...restProps }) {
const { toggleShow, toggleIsShown } = useContext(ToggleContext);
return (
<Header onClick={() => toggleIsShown(toggleShow)} {...restProps}>
{children}
</Header>
);
};
Accordion.Body = function AccordionBody({ children, ...restProps }) {
const { toggleShow } = useContext(ToggleContext);
return (
<Body className={toggleShow ? "open" : ""} {...restProps}>
<span>{children}</span>
</Body>
);
};
위 코드에서, ToggleContext 콘텍스트 객체는 toglgeShow 상태를 가지고 Accordian childeren 전체에 ToggleContext.Prvider를 통해 전달해 주고 있다.
또한, JSX dot notation을 이용해 Accordion 컴포넌트에 새로운 컴포넌트를 생성하고 추가했다.
6. 마지막으로 아래와 같이 App.js를 업데이트 하자:
import React from "react";
import Accordion from "./components/Accordion";
import faqData from "./data";
export default function App() {
return (
<Accordion>
<Accordion.Title>LogRocket FAQ</Accordion.Title>
<Accordion.Wrapper>
{faqData.map((item) => (
<Accordion.Item key={item.id}
<Accordion.ItemHeader>{item.header}</Accordion.ItemHeader>
<Accordion.Body>{item.body}</Accordion.Body>
</Accordion.Item>
))}
</Accordion.Wrapper>
</Accordion>
);
}
The presentational and container component patterns
이 용어는 Dan Abramov에 의해 만들어졌다. 하지만 Dan Abramov는 더 이상 이 패턴을 추천하지 않는다.
위 두 presentational 그리고 container 패턴은 복잡한 스테이트풀한 로직을 다른 컴포넌트로부터 분리해 관심사의 분리를 하는데 용이하다.
하지만, 리엑트 훅이 인위적인 구별 없이도 관심사의 분리를 가능하게 해 주면서 presentational and container 컴포넌트 패턴 대신에 훅 패턴이 선호되기 시작했다. 하지만 사용 경우에 따라, presentational and container 패턴이 여전히 유용할 수도 있다.
이 패턴은 관심사를 분리하고 코드를 이해하고 사유하기 쉽도록 설계되었다.
presentational 컴포넌트는 stateless한 함수 컴포넌트로 데이터를 렌더링 하는 뷰에만 초점을 둔다. 그리고 다른 애플리케이션 부분과 의존성이 없다.
뷰와 관련된 상태를 가져야 하는 경우에는 리엑트 클래스 컴포넌트를 이용해 구현한다.
presentational component에서 리스트를 렌더링 하는 예제를 살펴보자:
const usersList = ({users}) => {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.username}
</li>
))}
</ul>
);
};
container 컴포넌트는 내부 상태와 라이프 사이클을 관리하는 클래스 컴포넌트이다. presentational 컴포넌트를 포함하고 data 요청 로직이 들어간다.
컨테이너 컴포넌트의 예제로는 아래와 같다:
class Users extends React.Component {
state = {
users: []
};
componentDidMount() {
this.fetchUsers();
}
render() {
return (); // ... jsx code with presentation component
}
}
The Hooks pattern
리엑트 훅 API는 리엑트 16.8 버전에 소개되었고 리엑트 컴포넌트를 구축하는 방식을 완전히 바꾸어 놓았다.
리엑트 훅 API는 리엑트 함수형 컴포넌트가 props, state, context, refs, 그리고 라이프사이클 등 일반적인 리엑트 기능에 쉽고 간단하게 접근할 수 있도록 했다.
그 결과 함수형 컴포넌트는 단순한 함수형 컴포넌트에서 벗어나 state, 라이프사이클 훅, 사이드 이펙트를 사용할 수 있게 되었다. 기존에는 클래스형 컴포넌트에만 지원되던 기능들이었다.
presentational 그리고 container 컴포넌트 패턴은 관심사를 분리하도록 했지만, 종종 container는 엄청나게 비대해졌다. 왜냐하면 여러 라이프 사이클마다 거대한 로직을 나누어야 했기 때문이다. 그리고 비대한 컴포넌트는 읽고 유지하기가 어려웠다.
또한 컨테이너가 클래스이다 보니, 작성하기도 쉽지 않았다. 그리고 컨테이너 작업을 하다 보면, 오토 바인딩이나 this와 같이 클래스 관련 문제들을 맞닥뜨리기도 한다.
함수형 컴포넌트에 내부 스테이트 관리, 컴포넌트 라이프사이클 접근, 그리고 다른 클래스 컴포넌트 기능을 부여함으로써 훅 패턴은 위에서 언급된 클래스 관련 문제들을 해결했다. 순수 자바스크립트 함수로, 리엑트 함수형 컴포넌트는 this 키워드와 씨름하지 않고 쉽게 작성할 수 있다.
아래 코드를 한번 살펴보자:
import React, { Component } from "react";
class Profile extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
user: {}
};
}
componentDidMount() {
this.subscribeToOnlineStatus(this.props.id);
this.updateProfile(this.props.id);
}
componentDidUpdate(prevProps) {
// compariation hell.
if (prevProps.id !== this.props.id) {
this.updateProfile(this.props.id);
}
}
componentWillUnmount() {
this.unSubscribeToOnlineStatus(this.props.id);
}
subscribeToOnlineStatus() {
// subscribe logic
}
unSubscribeToOnlineStatus() {
// unscubscribe logic
}
fetchUser(id) {
// fetch users logic here
}
async updateProfile(id) {
this.setState({ loading: true });
// fetch users data
await this.fetchUser(id);
this.setState({ loading: false });
}
render() {
// ... some jsx
}
}
export default Profile;
위의 컨테이너를 기반으로 세 가지 문제점을 확인할 수 있다.
- 스테이트 설정 이전에 생성자 작업과 super() 호출. 이 문제는 자바스크립트의 class field 소개 이후 문제가 해결되었지만 훅이 여전히 더 간단한 API를 가진다.
- this 작업
- 라이프사이클에 따른 반복된 로직
훅은 이러한 문제들을 깔끔하고 짧은 API로 해결한다. Profile 컴포넌트를 아래와 같이 리펙터링 해볼 수 있다.
import React, { useState, useEffect } from "react";
function Profile({ id }) {
const [loading, setLoading] = useState(false);
const [user, setUser] = useState({});
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
updateProfile(id);
subscribeToOnlineStatus(id);
return () => {
unSubscribeToOnlineStatus(id);
};
}, [id]);
const subscribeToOnlineStatus = () => {
// subscribe logic
};
const unSubscribeToOnlineStatus = () => {
// unsubscribe logic
};
const fetchUser = (id) => {
// fetch user logic here
};
const updateProfile = async (id) => {
setLoading(true);
// fetch user data
await fetchUser(id);
setLoading(false);
};
return; // ... jsx logic
}
export default Profile;
Conclusion
이번 글에서, 2022년에 유용한 패턴에 대해 알아보았다. 디자인 패턴은 해당 패턴을 만들어낸 숙련되고 경험 많은 개발자들의 지혜를 이용할 수 있다는 점에서 강력하다.
결과적으로 미리 설정된 해결책을 이용하며 개발 시간을 단축시키고 소프트웨어 품질을 향상할 수 있게 된다.
출처:https://blog.logrocket.com/react-component-design-patterns-2022/
React component design patterns for 2022 - LogRocket Blog
Explore the most recent React design patterns to solve common issues, including cross-cutting concerns and global data sharing.
blog.logrocket.com
'Front-End > React' 카테고리의 다른 글
[React] 리엑트 컴포넌트가 두 번 렌더링 되는 이유? (0) | 2022.07.29 |
---|---|
[React] 리엑트 디자인 패턴: Return Component From Hooks (0) | 2022.06.30 |
[React] Controlled vs Uncontrolled component (0) | 2022.06.21 |
[React] 웹 개발에서 리엑트를 사용하는 이유 (0) | 2022.06.03 |
[React] 외부 클릭 시 메뉴창 닫기 (0) | 2022.04.08 |