React Clean Code 1
Principal
리액트 코드의 응집도, 결합도, 단일 책임 원칙
- 응집도 : 유사한 기능을 하는 컴포넌트들은 뭉치면 좋다.
- 결합도 : 서로 다른 역할을 가진 컴포넌트들의 응집도는 최소화 하며, 결합도를 높인다. (합성 패턴 등 사용)
- 단일 책임 원칙 : 하나의 함수, 이벤트 핸들러에서 다양한 로직을 담지 않도록 한다.
🧠 Composition vs Compound
리액트가 추구하는 방향은 합성 패턴이다. = 컴포넌트의 확장은 상속이 아닌 합성(Composition) 사용 해라.
Controlled vs Uncontrolled component
- Controlled component : 부모 컴포넌트가 자식의 상태, 이벤트를 관리할 수 있다.
- Uncontrolled component : 부모 컴포넌트는 자식의 상태 관리 불가, 별도의 API를 통해 접근해야 한다.
Composition vs Compund
- Composition 컴포넌트 합성 : 기존의 컴포넌트는 유지하면서, 앞, 뒤, 혹은 감싸는 방식으로 컴포넌트를 확장하는 방식.
- 물리에서 합성물의 형태를 생각. -> 물리적 재결합
- 예) HoC, Render Props 패턴 등
- Compund 컴포넌트의 혼합 : 컴포넌트의 특성이 바뀌는 조작으로 컴포넌트들을 연결한다.
- 물리에서 결합물을 생각. -> 화학적 재결합
- 예) Context에 영향을 받는 컴포넌트들, Radix의 Namespace Patterned Component
- Composition 컴포넌트 합성 : 기존의 컴포넌트는 유지하면서, 앞, 뒤, 혹은 감싸는 방식으로 컴포넌트를 확장하는 방식.
좋은 컴포넌트 설계
- 컴포넌트의 props만으로도 어떤 데이터가 필요하고 어떤 데이터를 변경하는지 예측 가능 해야함.
- Composition 패턴을 사용 (재사용성 높이기) 하면서 Controlled Component로 (에측 가능한) 코드를 작성한다.
특정한 목적을 가진 컴포넌트들들은 합성 대신 Compund를 사용해도 좋다.
- 대신 해당 컴포넌트를 사용하는 컨테이너 컴포넌트는 Composition 합성을 사용한다.
🧠 역할 구분하기 - 도메인 로직이 있는것과 아닌 것
도메인 로직이 없는 경우 : 재사용 가능한 컴포넌트로 뺄 수 있다.
- 즉, 컴포넌트는 Composition 지원을 위해 Controlled component(or hybrid component)형태로 가야함.
도메인 로직이 있는 경우 1.비즈니스 로직이 간단한 경우라면 컴포넌트의 Composition으로 끝내야 한다.
2.하지만 props drilling, props hell 등 굉장히 복잡해지는 경우가 있다.- 한 화면에서 복잡한 데이터를 다루는 데스크탑 앱 코드 등..
- props drilling, props hell 을 해결하기 위해 컴포넌트간의 강결합(Compund)로 처리한다.
- 주의, props 처리가 간단해지는 만큼 추후 재사용성은 기대하면 안된다.
- 팁, Context를 사용하면 좋지만 리렌더링 최소화를 위해 context-selector를 사용하면 좋다.
- 컨벤션, 강결합의 의미로 Namespace pattern을 의도적으로 사용하는것은 좋다.
- 리팩토링, 반복되는 로직들을 분리하여 use-funnel과 라이브러리 처럼 추상화 한다.
⚠️ Props Drilling을 줄이기 위한 방법
- 1.Component Lifing-Up
- 리렌더링을 줄여주는 최적화 효과도 있음.
⚠️ Props Hell을 줄이기 위한 방법
- 1.Computed field, dto object 로 묶어서 관리하기
Optimization
Optimized Context API
Before
- 문제점 : React Context API를 사용하면 불필요한 리렌더링이 많이 발생한다.
- 본인이 구독하는 컨텍스트 필드가 아니더라도 다른 필드가 변경되면 리렌더링 발생함.
// After
type State = {
count: number;
};
type Action = {
type: "INCREMENT" | "DECREMENT";
};
function reducer(state: State, action: Action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
throw new Error("Provide a valid action.");
}
}
type StateContext = { count: number };
type DispatchContext = Dispatch<Action>;
export const StateContext = createContext<StateContext | null>(null);
export const DispatchContext = createContext<DispatchContext | null>(null);
type CartProviderProps = {
children: ReactNode;
};
// ✅ Dispatch Context, State Context 두개를 나눠서 사용하면 불필요한 리렌더링을 방지
export const CartProvider = ({ children }: CartProviderProps) => {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
export function useStateContext() {
const value = useContext(StateContext);
if (value === null) {
throw new Error("Must be wrapped inside Context.Provider");
}
return value;
}
export function useDispatchContext() {
const value = useContext(DispatchContext);
if (value === null) {
throw new Error("Must be wrapped inside Context.Provider");
}
return value;
}
확인하기 : 불필요한 리렌더링 제거되었는지 체크.
Anti-Pattern
안티패턴들 모음
Chain of useEffect
문제점
- useEffect 가 또 다른 useEffect를 부르는 체이닝 패턴은 불필요한 렌더링 횟수 증가와, 무한 렌더링 가능성 높임.
우선 useEffect 언제 사용하는지 명확히 해야 함.
- 리액트에서는 렌더링 코드, 이벤트 핸들러가 주를 이룬다.
- 외부시스템과 연동 (Web API, AJAX, Storage 연결 등)이 필요한 경우가 있다.
- 이벤트 핸들러로 외부 시스템 연동이 가능하지만, 초기화 등의 상황에서는 이벤트 없이 연동이 필요한 경우 있음.
- ->이때 useEffect를 사용한다.
- 즉 : 주요 렌더링 코드와 상관없이, 이벤트 없이 외부 시스템 연동 하는 경우에 useEffect를 사용한다.
- 그 외 코드에서는 useEffect를 줄일 수 있는지 강력히 검토하자.
useEffect를 줄이는 패턴 (다른 말로 안티패턴임)
- 장바구니 추가되었어요.! 토스트 : useState의 state를 감지해서 toast를 보여주는 경우
- -> 이벤트 핸들러로 처리 가능
- 모달창 열리면 로그 API 호출 : useState의 isOpen state를 감지해서 AJAX 쏘는 경우
- -> 이벤트 핸들러로 처리 가능
- computed field를 useEffect에서 계산하는 경우
- -> useMemo 혹은 그냥 필드로 처리 가능
const [products, setProducts] = useState([]);
const [productCount, setProductCount] = useState(0);
// ❌ Anti-Pattern
useEffect(() => {
setProductCount(products.length);
}, [products]);
return (
<div>
<h1>Product Count: {productCount}</h1>
</div>
)