FrontEnd(React) 관심사 분리 시작하기

11t518s·2023년 4월 9일
2
post-thumbnail

관심사 분리에 대해 이야기해보려고 합니다!

프론트엔드는 특히 React에서는 라이브러라 자유롭게 어디서든 코드를 동작시킬 수 있기 때문에 이 부분에 대해서 사람들마다 나아가야할 방향성이 다른 듯 합니다.

그렇지만 관심사가 명확히 분리해야합니다.

오늘 집중해서 고민할 부분은 아래와 같습니다
1. 컴포넌트에서는 최소한의 계산만 하자.
2. 상태는 컴포넌트에 가장 가까이 둔다.
3. 그리고 함수에서 너무 많은 역할을 담당하지 말자.

코드짜보기

예시 코드를 가져왔습니다!

아래의 로직은 어떤 서비스를 들어갔을 때 모달이 나오는 기능입니다.
조건은 다음과 같습니다.

  1. 모달은 "닫기" 버튼과 "다시 보지않기" 버튼 2가지가 있습니다.
  2. 다시 보지않기를 눌렀을 때는 localStorage에 값을 저장하여 해당 디바이스에서 다시 보지 않게 합니다.
const OnBoarding = () => {
  const [isModal, setIsModal] = useState(false)
  
  useEffect(() => {
  	const isOnboardingModal = localstorage.getItem("onBoardingModal")
    useState(isOnboardingModal === "done")
  }, [])

  return (<>
          {isModal ?? <OnBoardingModal setIsModal={setIsModal}/>}
          // 다른 코드들
          </>)
}

--------------------------------------------------------------------------

type Props = {
  setIsModal: Dispatch<SetStateAction<boolean>>
}

const OnBoardingModal = ({setIsModal}: Props) => {
	return (<>
      	// 다른 코드들
         <button onClick={() => {
      			localStorage.setItem("onBoardingModal", "done")
      			setIsModal(false)
    		}}>
              다시 보지않기
         </button>
         <button onClick={() => {setIsModal(false)}}>
           닫기
		 </button>
      </>)
}

위의 코드는 잘 동작할 듯 합니다!

물론 아직 다른 코드들이 없고, 단순한 로직이라서 불편함이 없습니다. 다만 더 복잡한 로직이 추가되기 전에 관심사를 나누고 개발자들이 더 이해하기 편하게 만들어보려고 합니다!

1. 컴포넌트에서 계산을 최소화하자

여기서 저는 localSotrage에 저장하고 가져오는 로직을 나눠보려고 합니다. 외부에 localStorage담당하는 로직을 구현해주겠습니다!

/** 
* onBoarding에 관련된 Storage 로직들의 모음
* class instance로 안하고 Function으로 해도 Object Literal 무관합니당. 제 취향이 그냥.. 그렇습니다..
*/
class OnBoardingStorage {
	private key = "onBoardingModal";
  	private closeValue = "done";
	
  	// 여기서는 결코 다른 값이 들어오지 않을 것이기 때문에 외부에서 값을 주입받지 않는 것으로 구현함
  	setOnBoardingModalPermermentClose: () => {
    	localStorage.setItem(this.key, this.closeValue)
    };
	
	getOnBoardingModaValue: () => localStorage.getItem(this.key);


	isOnBoardingClose: () => {
    	const onBoardingModalValue = this.getOnBoardingModalValue();
        return onBoardingModalValue === this.closeValue;
    };
}
export const onBoardingStorage = new OnBoardingStorage();

--------------------------------------------------------------------------

import onBoardingStorage from "~~~~"

const OnBoarding = () => {
  const [isModal, setIsModal] = useState(false)
  
  useEffect(() => {
    useState(onBoardingStorage.isOnBoardingClose())
  }, [])

  return (<>
          {isModal ?? <OnBoardingModal setIsModal={setIsModal}/>}
          // 다른 코드들
          </>)
}

--------------------------------------------------------------------------

import onBoardingStorage from "~~~~"

type Props = {
  setIsModal: Dispatch<SetStateAction<boolean>>
}

const OnBoardingModal = ({setIsModal}: Props) => {
	return (<>
      	// 다른 코드들
         <button onClick={() => {
      			onBoardingStorage.setOnBoardingModalPermermentClose()
      			setIsModal(false)
    		}}>
              다시 보지않기
         </button>
         <button onClick={() => {setIsModal(false)}}>
           닫기
		 </button>
      </>)
}

위와 같이 수정해봤습니다.

기존에는 localStoarge에 대한 로직을 컴포넌트에서 직접구현했다면, 해당 관심사를 하나의 기능으로 묶어서 외부로 뺐습니다.

기능으로 묶을 때도 단순히 저장하고 불러오는 것 뿐 아니라 isOnBoardingClose라는 메서드를 넣어서 실제로 close된지 확인하는 로직까지 함께 구현해줬습니다.

이렇게 되면
1. 컴포넌트에서 계산이 사라지게 됩니다.
2. 다른 화면에서 해당 계산이 필요할 때 동일한 함수를 중복해서 만들지 않아도 됩니다.

개발자들은 컴포넌트에 들어왔을 때 불필요한 계산 로직이 사라지기 때문에 더 편하게 로직을 확인할 수 있습니다.

2. 상태는 컴포넌트에 가장 가까이 둔다.


import onBoardingStorage from "~~~~"

const OnBoarding = () => {  
  // isModal state는 OnBoardingModal 내부에서만 사용되고, 관심사가 OnBoardingModal에만 있으므로 부모 요소에서 제거하고 OnBoardingModal에서 만들어줍니다. 
  return (<>
          	<OnBoardingModal />
          	// 다른 코드들
          </>)
}

--------------------------------------------------------------------------

import onBoardingStorage from "~~~~"

const OnBoardingModal = () => {
  	const [isModal, setIsModal] = useState(false); // isModal state를 내부로 옮겼습니다.
  
    useEffect(() => {
      useState(onBoardingStorage.isOnBoardingClose())
    }, []);
  
  	if(!isModal) {
    	return null
    }
    
	return (<>
      	// 다른 코드들
         <button onClick={() => {
      			onBoardingStorage.setOnBoardingModalPermermentClose()
      			setIsModal(false)
    		}}>
              다시 보지않기
         </button>
         <button onClick={() => {setIsModal(false)}}>
           닫기
		 </button>
      </>)
}

isModal이라는 상태는 외부에서 사용되지 않고 오직 OnBoardingModal컴포넌트에서만 사용됩니다. 그렇다면 굳이 props로 내려주지 않고 내부에서 상태를 만들어서 관리해주는 것이 더 좋습니다.

이렇게 되면
1. 다른 화면에서 불필요한 리렌더링이 사라집니다.
2. 내가 원하는 로직을 컴포넌트 하나에서 응집도 있게 확인할 수 있습니다.

(물론 위의 예시는 적절하지 못할 수 있습니다. 모달을 관리하는 global state를 만드는 케이스도 있고, 회사마다 모달을 다루는 방법은 다양합니다. 다만 상태를 컴포넌트에 가장 가깝게 배치하는것을 신경써야한다는 것만 알아주시면 감사할 것 같습니다.)

3. 그리고 함수에서 너무 많은 역할을 담당하지 말자.

이 역시 아쉽게도 아직 너무 많은 로직이 담겨있는 함수가 있진 않지만 예시로 나눠보겠습니다.

import onBoardingStorage from "~~~~"

const OnBoardingModal = () => {
  	const [isModal, setIsModal] = useState(false);
  
  	const closeModal = () => {
    	setIsModal(false);
    }
  
  	const handleCloseModalClick = () => {
		closeModal();
    };
  
	const handlePermermentCloseModalClick = () => {
		onBoardingStorage.setOnBoardingModalPermermentClose();
		closeModal();
    };
  
  
    useEffect(() => {
      useState(onBoardingStorage.isOnBoardingClose())
    }, []);
  
  	if(!isModal) {
    	return null
    };
    
	return (<>
      	// 다른 코드들
         <button onClick={handlePermermentCloseModalClick}>
              다시 보지않기
         </button>
         <button onClick={handleCloseModal}>
           닫기
		 </button>
      </>)
}

위와 같이 작성해줬습니다. (만들고 보니 여기서는 굳이.. 라는 생각이 들지만.. 그래도 느낌만 이해해주시면 감사하겠습니다! 나중에 추가할만한 괜찮은 소스 있으면 추가해서 내용을 좀 보강해보겠습니다..!)

이렇게 되면
1. 각 함수는 각각의 역할만 담당하게 됩니다.
ex_ closeModal은 오로지 모달을 닫는 역할만 합니다. 또한 handle~~click함수는 onClick될 때 어떤 동작을 하는지만 정의합니다. 그 내부에 어떤 로직이 오는지는 관심을 갖지 않습니다.
2. 원래는 코드의 재사용성도 높아지고 깔끔해져야 합니다... (예시를 더 생각해보겠습니다...)

이런식으로 코드의 관심사를 나눠봤습니다.

물론 다 만들고 나니 예시가 좀 적당하지 못했을 수 있지만, 그래도 충분히 설명은 됐다고 생각합니다.
완성된 코드는 다음과 같습니다.

/** 
* onBoarding에 관련된 Storage 로직들의 모음
* class instance로 안하고 Function으로 해도 Object Literal 무관합니당. 제 취향이 그냥.. 그렇습니다..
*/
class OnBoardingStorage {
	private key = "onBoardingModal";
  	private closeValue = "done";
	
  	// 여기서는 결코 다른 값이 들어오지 않을 것이기 때문에 외부에서 값을 주입받지 않는 것으로 구현함
  	setOnBoardingModalPermermentClose: () => {
    	localStorage.setItem(this.key, this.closeValue)
    };
	
	getOnBoardingModaValue: () => localStorage.getItem(this.key);


	isOnBoardingClose: () => {
    	const onBoardingModalValue = this.getOnBoardingModalValue();
        return onBoardingModalValue === this.closeValue;
    };
}
export const onBoardingStorage = new OnBoardingStorage();

--------------------------------------------------------------------------

import onBoardingStorage from "~~~~"

const OnBoarding = () => {
  useEffect(() => {
    useState(onBoardingStorage.isOnBoardingClose())
  }, [])

  return (<>
          {isModal ?? <OnBoardingModal setIsModal={setIsModal}/>}
          // 다른 코드들
          </>)
}

--------------------------------------------------------------------------

import onBoardingStorage from "~~~~"

const OnBoardingModal = () => {
  	const [isModal, setIsModal] = useState(false);
  
  	const closeModal = () => {
    	setIsModal(false);
    }
  
  	const handleCloseModalClick = () => {
		closeModal();
    };
  
	const handlePermermentCloseModalClick = () => {
		onBoardingStorage.setOnBoardingModalPermermentClose();
		closeModal();
    };
  
    useEffect(() => {
      useState(onBoardingStorage.isOnBoardingClose())
    }, []);
  
  	if(!isModal) {
    	return null
    };
    
	return (<>
      	// 다른 코드들
         <button onClick={handlePermermentCloseModalClick}>
              다시 보지않기
         </button>
         <button onClick={handleCloseModal}>
           닫기
		 </button>
      </>)
}

관심사 분리를 해야하는 이유

1. 테스트의 용이성

함수형 프로그래밍 페러디임에서 흔하게 사용하는 액션 - 계산 - 데이터의 논리로 다가갔을 때 좀 더 명확히 알 수 있습니다.. 결국 테스트하기 쉬운 것은 유저의 액션이나 데이터 그 자체이기 보단 계산의 영역이 더 테스트하기 쉽습니다..

여기서 말하는 계산은
반드시 입출력으로 이루어져야 하며, 같은 입력에 대해서는 항상 같은 출렵값을 내놓는 함수 즉 순수함수를 이야기 합니다.

조금 더 이야기 하면 함수 내부에 if문이나 이런 조건에 따른 다른 return값도 존재하지 않고 (input: inputType) => outputType 이 명확한 함수들을 의미합니다.

따라서 관심사를 나누면서 액션의 영역이 많아지면 (위의 코드를 예시로 하면 isOnBoardingClose메서드 같은 친구들이 액션이 됩니다. 물론 외부에서 값을 주입받을 수 있는 예시가 더 좋습니다.)

또한 지금 당장 테스트를 하고 있지 않더라도 미래에는 서비스의 안전성과 개발에서의 안전성을 위해 테스트 코드를 넣을 때가 있을텐데 그 때를 위해서 지금부터 준비하는 것도 좋아보입니다!

2. 리팩토링 / 디버깅의 용이성

리팩토링이나 디버깅이나 가장 중요한 것은 해당 로직이 어디서 어떤일을 하는지 알아야 합니다.

먼저 디버깅에 조금 더 집중해서 보면,
1. 우리는 유저가 어떤 액션을 취했을 때 버그가 생긴지 찾으러 갑니다.
2. 그 다음 어떤 액션했을 때 나오는 함수를 찾습니다.
3. 그 함수의 동작에서 어떤 부분이 오류가 났는지 찾습니다.

이 과정에서 함수의 역할이 방대하면 어떤 부분에서 오류가 났는지 찾기 어렵고, 상태가 컴포넌트와 멀리 떨어져있다면(예를들어 부모의 부모의 부모요소에 있다면) 혹시 다른 곳에서 상태가 사용되진 않았는지, 내가 확인한 함수에서 생기는 오류가 맞는지 등등 추적해야하는 것이 다양해지는 어려움이 있습니다.

이 부분에 대해서 피드백 주셔도 너무 감사합니다. 혹은 아니면 더 좋은 예시를 들어주실 수 있는 분이 있으시다거나 궁금한 점 있으시면 말씀 부탁드립니다!

profile
사람들에게 행복을 주는 서비스를 만들고 싶은 개발자입니다!

0개의 댓글