props drilling을 줄이고 사용하는 입장에서 '조합'해서 사용할 수 있도록 하는 패턴
예를 들어
아래와 같은 코드가 있고, 내부적으로 2~4단계의 트리 depth단계로 구성된 컴포넌트가 있다면
<Counter text="example" ex1={<div></div>} onchange{()=>{ console.log(change)}} onClick={() => console.log('test1')}>
사용하는 입장에서는 내부 구성을 정확하게 알 수 없을것이다.
그래서 사용자 입장에서 컴포넌트를 직접 조합해서 구현하고 props를 직접 내려주는 방식이다.
코드 예시)
export default function AppEx() {
return (
<div>
<Counter onClick={() => console.log('test1')}>
<Counter.Increment />
<Counter.Decrement />
<Counter.Label />
</Counter>
<Counter onClick={() => console.log('test2')}>
<Counter.Increment />
<Counter.Label color="blue" />
<Counter.Decrement />
</Counter>
<Counter onClick={() => console.log('test3')}>
<Counter.Increment />
<Counter.Label color="red" />
</Counter>
</div>
);
}
장점
매우 높은 유연성
선언적으로 구성 -> 가독성이 좋음
하나의 giant component를 쪼개 여러개로 분리함으로써 복잡성이 줄어듬
단점
매우 높은 유연성(실수 혹은 의도치 않는 코드 삽입 가능성 증가)
코드가 좀 복잡해짐
JSX 크기 증가
코드 예시)
import React, { useEffect, useState, Children } from 'react';
import styled from 'styled-components';
import { Decrement, Increment, Label, Reset } from './modules';
const StyledDiv = styled.div`
display: inline-flex;
flex-direction: row;
border: 0.5px solid;
gap: 0.5px;
margin: 30px;
`;
function Counter({ children, onClick }) {
const [counter, setCounter] = useState(0);
const resetCounter = () => setCounter(0);
return (
<StyledDiv onClick={onClick}>
{Children.map(children, (child) => {
return React.cloneElement(child, {
counter,
resetCounter, // 그냥 첨가 가능하다는 예시용
setCounter,
});
})}
</StyledDiv>
);
}
Counter.Reset = Reset;
Counter.Increment = Increment;
Counter.Decrement = Decrement;
Counter.Label = Label;
export default function AppEx() {
return (
<div>
<Counter onClick={() => console.log('test1')}>
<Counter.Increment />
<Counter.Decrement />
<Counter.Label />
</Counter>
<Counter onClick={() => console.log('test2')}>
<Counter.Increment />
<Counter.Label color="blue" />
<Counter.Decrement />
</Counter>
<Counter onClick={() => console.log('test3')}>
<Counter.Increment />
<Counter.Label color="red" />
</Counter>
</div>
);
}
구현하는 방식은 Context Api를 사용하는 방식과 Children와 cloneElement를 사용하는 방식 2가지를 발견했는데
ex) children & cloneElement 이용
function Counter({ children, onClick }) {
const [counter, setCounter] = useState(0);
const resetCounter = () => setCounter(0);
return (
<StyledDiv onClick={onClick}>
{Children.map(children, (child) => {
return React.cloneElement(child, {
counter,
resetCounter, // 그냥 첨가 가능하다는 예시용
setCounter,
});
})}
</StyledDiv>
);
}
ex) context api 이용
const CounterContext = createContext(undefined);
function CounterProvider({ children, value }: any) {
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}
function useCounterContext() {
const context = useContext(CounterContext);
if (context === undefined) {
throw new Error('useCounterContext must be used within a CounterProvider');
}
return context;
}
function Counter2({ children, onClick }) {
const [counter, setCounter] = useState(0);
const resetCounter = () => setCounter(0);
const handleIncrement = () => {
setCounter(counter + 1);
};
const handleDecrement = () => {
setCounter(counter - 1);
};
return (
<CounterProvider value={{ counter, handleIncrement, handleDecrement }}>
<StyledDiv onClick={onClick}>{children}</StyledDiv>
</CounterProvider>
);
}
children & cloneElement 방식의 경우 cloneElement라길래 복사하는 비용이 크지 않을까? 검색해봤는데
stackoverflow에 따르면
어차피 JSX가 React.createElement(Object)로 전환되는 과정에서 cloneElement를 쓰면 똑같이 Object로 전환되면서 props만 전달해서 바꿔주는 식으로 작동해서 성능 이슈는 큰 문제가 아니라고 한다.
https://stackoverflow.com/questions/54922160/react-cloneelement-in-list-performance
cloneElement 방식의 경우 depth가 깊어지면 다루기 좀 힘들어질꺼 같지만 간편하고
Context API 방식은 boilerplate?를 까는게 번거롭긴 하지만 depth에 상관없이 사용 가능할꺼 같다.
headlessui 란 오픈소스는 context 방식을 사용한것으로 보인다.
그외 비슷한 패턴들
쓸만한게 있을까 빠르게 훑고 넘어가자
Control Props Pattern
import React, { useState } from "react";
import { Counter } from "./Counter";
function Usage() {
const [count, setCount] = useState(0);
const handleChangeCounter = (newCount) => {
setCount(newCount);
};
return (
<Counter value={count} onChange={handleChangeCounter}>
<Counter.Decrement icon={"minus"} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon={"plus"} />
</Counter>
);
}
export { Usage };
Example
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/control-props
컴포넌트를 제어 컴포넌트로 사용
하나의 'single source of truth' 단일 상태 사용 -> base component에 custom logic과 상태 부여
장점
직접적 제어권 부여
단점
(compound 패턴보다) 구현의 복잡성 -> state, function, component 제어가 필요
Custom Hook Pattern
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
function Usage() {
const { count, handleIncrement, handleDecrement } = useCounter(0);
const MAX_COUNT = 10;
const handleClickIncrement = () => {
//Put your custom logic
if (count < MAX_COUNT) {
handleIncrement();
}
};
return (
<>
<Counter value={count}>
<Counter.Decrement
icon={"minus"}
onClick={handleDecrement}
disabled={count === 0}
/>
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment
icon={"plus"}
onClick={handleClickIncrement}
disabled={count === MAX_COUNT}
/>
</Counter>
<button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}
export { Usage }
Example
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/custom-hooks
단점
로직과 렌더링UI의 분리 -> (남이 보기에) 가독성 하락
Props Getters Pattern
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
const MAX_COUNT = 10;
function Usage() {
const {
count,
getCounterProps,
getIncrementProps,
getDecrementProps
} = useCounter({
initial: 0,
max: MAX_COUNT
});
const handleBtn1Clicked = () => {
console.log("btn 1 clicked");
};
return (
<>
<Counter {...getCounterProps()}>
<Counter.Decrement icon={"minus"} {...getDecrementProps()} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} {...getIncrementProps()} />
</Counter>
<button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
Custom increment btn 1
</button>
<button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
Custom increment btn 2
</button>
</>
);
}
export { Usage };
장점
getter를 통한 쉬운 사용 및 통합
복잡성을 getter에 감춤으로써 복잡성을 숨김
단점
Lack of visibility -> getter가 어떻게 구현되어 있는지 모르는 사람들에게는 가독성이 하락한다
//props getter for 'Counter'
const getCounterProps = ({ ...otherProps } = {}) => ({
value: count,
"aria-valuemax": max,
"aria-valuemin": 0,
"aria-valuenow": count,
...otherProps
});
getter는 이런식으로 구성되어있는데 spread 연산자로 뿌려주는 식이다.
State reducer pattern
custom hook 패턴에 reducer를 조합한 형태
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
const MAX_COUNT = 10;
function Usage() {
const reducer = (state, action) => {
switch (action.type) {
case "decrement":
return {
count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1)
};
default:
return useCounter.reducer(state, action);
}
};
const { count, handleDecrement, handleIncrement } = useCounter(
{ initial: 0, max: 10 },
reducer
);
return (
<>
<Counter value={count}>
<Counter.Decrement icon={"minus"} onClick={handleDecrement} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} onClick={handleIncrement} />
</Counter>
<button onClick={handleIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}
export { Usage };
Example
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/state-reducer
가장 파워풀하고 가장 Lack of visibility한 패턴이지 싶다.
느낀점?
협업시 가독성 문제 때문에 느낌상 1 ~ 3번 패턴이 젤 무난하지 않을까 싶다.
그 외로는 공통으로 사용할 수 있는 로직을 가지고(추상화) UI만 갈아끼우는 식으로 사용 가능할꺼 같다
reference
https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6
'소프트웨어공학 > 디자인패턴' 카테고리의 다른 글
메모 (0) | 2022.03.25 |
---|---|
프론트엔드에서의 레포지토리 패턴? (0) | 2022.03.18 |
[행위] 책임 연쇄 패턴 (0) | 2022.03.13 |
[행위] 전략 패턴 & 커맨드 패턴 (0) | 2022.03.07 |
[행동] 중재자 패턴 (1) | 2022.03.01 |