관심사의 분리 원칙(Seperation Of Concern)

관심사 분리 원칙이란?

코드를 유지보수함에 있어서 파악할 수 있는 단위에 한가지의 업무성격만을 유지하도록 하는 방식

코드가 단위별로 하나의 관심사에만 충실하게 만드는 것.

  • 코드 파악을 위해 읽어야 하는 코드의 단위가 적음
  • 수정 시 신경 써야 할 코드 수가 적어짐
  • 낮은 결합도와 높은 응집도로 수정 시 변경점을 줄일 수 있고 연관 있는 코드가 모여 있음

가장 대표적인 예시로 VIEWBUSINESS LOGIC 간의 분리를 들 수 있음.

  • VIEW 부분: BUSINESS LOGIC으로부터 데이터를 받아서 해당 데이터를 표시하는 역할만 수행
  • VIEW MODEL: 해당 데이터에 대한 로직 및 관리를 맡음

1. 상태로 분리되는 경우

하나의 컴포넌트에서 같이 실행되지 않는 코드들은 따로 분리해서 사용

안좋은 예시

function ConditionComp(){
    const condition = useCustomHook() === true;

    useEffect(()=>{
        if(condition){
            return;
        }
        doWhenConditionTrue();
    }, [condition])

    return condition ? (
        <Text>true</Text>
    ) : (
        <Text>false</Text>
    )
}

분리된 좋은 예시

각 컴포넌트가 실행되는 코드에 대한 내용들만 가지고 있다.

function ConditionComp(){
    const condition = useCustomHook() === true;
    return condition ? <TrueComponent/> :  <FalseComponent/>
}

function TrueComponent(){
    useEffect(()=>{
        doWhenConditionTrue();
    }, [condition])
    return <Text>true</Text>
}

function FalseComponent(){
    return <Text>false</Text>
}

2. 독립적으로 실행해도 되는 로직은 HOC, Wrapper

어던 로직이 독립적으로 실행될 때 그 로직과 결과를 사용하는 컴포넌트와 같이 사용하게 된다면 분리하는게 좋음

안 좋은 예시

function ProcessAssembleComp(){
    // 사전체크로직
    useComplexCheck({
        onChecked: (state)=>{
            if(state==="Checking"){
                doProcess();
            }
        }
    })

    // processA .....

    // processB .....

    return <>{최종 결과}</>  // 
}

좋은 예시(Wrapper)

mobile인지 아니면 web인지 판단해서 Mobile의 경우 MobileComponent를 보여주고 아니면 WebComp를 보여주는 방식

사실 mobile,web구분은 서버에서 userAgent에서 파악해서 사용하므로 Wrapper로 로직을 분리해서 사용하는거에 중점으로 확인

import dynamic from 'next/dynamic';
import MobileComp from '@/components/MobileComp';
import WebComp from '@/components/WebComp';

const DeviceWrapper = dynamic(() => import('@/components/DeviceWrapper'), { ssr: false });

export default function Page() {
  return (
    <main>
      <DeviceWrapper MobileComponent={<MobileComp />} WebComponent={<WebComp />} />
    </main>
  );
}


// components/DeviceWrapper.tsx
'use client';

import { useEffect, useState } from 'react';

type Props = {
  MobileComponent: React.ReactNode;
  WebComponent: React.ReactNode;
};

export default function DeviceWrapper({ MobileComponent, WebComponent }: Props) {
  const [isMobile, setIsMobile] = useState<boolean | null>(null);

  useEffect(() => {
    // 사용자 에이전트를 기반으로 환경 파악 (더 정교하게 하려면 라이브러리 사용 가능)
    const userAgent = navigator.userAgent.toLowerCase();
    const mobile = /iphone|android|ipad|mobile/.test(userAgent);
    setIsMobile(mobile);
  }, []);

  if (isMobile === null) return null; // 로딩 중 처리
  return isMobile ? <>{MobileComponent}</> : <>{WebComponent}</>;
}

3. 커스텀 훅관리

하나의 커스텀 hook 에서는 한 관심사에 대한 내용만 참고하도록 한다.

function useUser(){
    //...
}

function useTheme(){
    //...
}

function useSidebar(){
    //...
}

function Header() {
  const user = useUser();        // 로그인 사용자만 추적
  const { open, setOpen } = useSidebar(); // 사이드바 상태만 관리

  return (
    <div>
      <span>{user.name}</span>
      <button onClick={() => setOpen(!open)}>Toggle Sidebar</button>
    </div>
  );
}