리액트에서 의존성 역전 원칙 적용하기(feat. 좋은설계란무엇일까?)

yumyum·2022년 12월 16일
119
post-thumbnail

프론트엔드에서는 어떻게 좋은 설계를 할 수 있을지, 어떻게 객체지향의 원리를 적용해볼 수 있을지 고민하고 계신분들이라면, 혹은 객체지향이 뭔지는 알겠는데 도대체 프론트엔드에선 어떻게 적용할 수 있을지 고민하고 계신 분들이라면 잘 오셨습니다 👋🏻
(혹은 객체지향을 공부해본 적이 없지만, 관심 있는 분들도 충분히 읽고 유익을 얻을 수 있도록 작성되었습니다)

저 또한 비슷한 고민을 가졌으며, 아주 약간이나마 인사이트를 얻고 제 프로젝트에 적용해 본 경험을 여러분들과 공유해보고자 합니다. 이 글을 읽고나면, 여러분은 좋은 설계의 정의와, 어떻게 객체지향의 원리를 리액트 프로젝트에 적용해 볼 수 있을지에 대한 인사이트를 얻게 될 것입니다.

TL;DR

  • 좋은 설계란 무엇인지에 대해서 알아봅니다
  • 의존성 역전 원칙이 무엇인지 알아봅니다
  • ✨ 저는 리액트에서 어떻게 의존성 역전 원칙을 적용했는지 경험을 공유합니다.
  • ✨ 나아가 여러분은 어떻게 리액트에서 의존성 역전 원칙을 적용시킬 수 있을지 그 사고의 프레임워크를 알아봅니다.

좋은 설계란 무엇일까

좋은 설계란 무엇일까요. 아니 그 전에, 설계란 무엇일까요. 설계란 말의 정의부터 알아봐야겠습니다. 너무 깊이 들어 갈 필요 없이, 제가 읽었던 '오브젝트'라는 책에서 정의한 '설계'의 의미를 소개하겠습니다.

설계란 코드를 배치하는 행위다.

맞습니다. 설계란 코드를 배치하는 행위입니다. 똑같은 기능을 만들고 비슷한 코드로 작성되면서도, 그 코드가 배치되어 있는 위치는 달라질 수 있습니다. 예를 들어, 하나의 컴포넌트안에서 5-6 개의 역할을 하는 코드가 몽땅 들어가 있을 수도 있고, 1개의 역할만 하는 컴포넌트가 5-6개 존재할 수도 있습니다. 똑같은 기능이라 할 지라도, 코드를 어떻게, 어디에 배치할 것인가? 이것에 대해서 고민하는 것이 바로 설계의 영역이라고 할 수 있습니다.

그렇다면 좋은 설계란 무엇일까요?

좋은 설계란 무엇인가? 우리가 짜는 프로그램은 두 가지 요구사항을 만족시켜야 한다. 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야 한다. 좋은 설계란 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계다. [오브젝트, 35p]

너무 멋진 정의이지 않습니까? 좋은 설계란 단순히 기능을 온전히 수행하는 것에 그치지 않습니다. 나아가, 끊임없이 요구사항이 변경되는 디지털 프로덕트의 세계에서 변경에 손 쉽게 대처 가능하도록 만들어주는 것. 그것이 바로 좋은 설계입니다. 그러니까, 요구사항이 변경되어도, 많은 수정을 하지 않고서도 변경이 손쉬워야 한다는 의미가 됩니다. 제가 공부했던 오브젝트라는 책에 의하면, 객체지향 프로그래밍이 목표로 하는 것이 바로 변경에 손 쉽게 대처가능한 설계를 만드는 것입니다.

그러므로 오늘 제가 작성한 이 글을 읽으면서 여러분들이 주목해 보셔야 할 내용은 다음과 같습니다.

의존성 역전 원칙을 적용한 이후에 변경의 전파가 얼마나 효율적으로 차단되었는가? 코드의 수정이 생길 때, 변경사항이 얼마나 많이 줄어들었는가?

이렇게 변경의 전파를 줄여서 유지보수를 용이하게 만들고자 하는 객체지향의 원리를, 아주 간략하게 핵심을 추려내 5가지 지침으로 정리해놓은 것이 바로 SOLID 원칙입니다.

오늘 이 자리에서 SOLID 원칙들을 일일이 설명하는 것은 글의 요지와 어긋납니다. 때문에 저는 SOLID원칙 중 의존성 역전 원칙에 대해 설명한 후 해당 원칙을 어떻게 프로젝트에 적용했는지 알아보겠습니다.


의존성 역전 원칙 :

의존 관계를 맺을 때, 변화하기 쉬운 것 보다 변화하기 어려운 것에 의존해야한다. [JAVA 객체 지향 디자인 패턴(정인상/채홍석 지음, 한빛미디어]

의존성 역전 원칙은 기본적으로 의존성의 방향에 의문을 가지도록 하는 원칙입니다. 의존성의 방향이 어디로 향하고 있는가? 쉽게 변하지 않는 추상적인 대상에 의존하고 있는가? 맥락에 따라 쉽게 변할 수 있는 구체적인 대상이 의존하고 있는가? 그것을 묻습니다. 결론은 쉽게 변하지 않는 대상에 의존하도록 만들어야한다는 것입니다.

그렇다면, 왜 변하지 않는 대상에 의존하도록 만들어야하는 것일까요? 의존성은 기본적으로 변경의 전파와 관련이있습니다. 만약 제가 만든 컴포넌트가 자주 변경되는 대상에 의존하고 있다면, 그만큼 제 코드에는 변경이 자주 발생할 것입니다. 하지만, 자주 변하지 않는 대상에 의존하고 있다면, 의존했다는 이유로 변경할 이유는 적어집니다.

이는 바꿔 말해 하위정책을 의존하지 말고, 상위정책을 의존하게 만들자는 이야기가 될 수 있습니다. 정책이야기가 나오니 머리가 더 복잡해지는 것 같은데... 예를 들어보겠습니다. 현재 저희가 만들어야 하는 프로그램이 영화예매 시스템이라고 한다면, 이 영화 예매 시스템에는 '할인정책'이라는 상위 정책이 존재할 수 있습니다. 그리고 '생일할인'이나 '조조할인'과 같은 정책들은 그 상위 정책의 하위 정책이라고 할 수 있습니다. 만약에 다양한 할인 정책 속에서 활용되어야만하는 컴포넌트가 세부적인 '조조할인'에 의존하고 있다면, '생일할인'이 필요해졌을 때, 컴포넌트 내부를 수정해야 할 것입니다. 그러나 '할인정책'이라는 추상적인 대상, 상위정책에 해당하는 대상을 의존하게 만든다면, 컴포넌트 내부에는 변경사항이 발생할 확률이 적어집니다.

이를 또 다시 바꿔 말하면,

의존성 역전 원칙은 구체적인 구현보다, 안정적이고 추상적인 인터페이스에 의존하도록 강제하는 원칙이다.

라고 말할 수 있습니다. 안정적이고 추상적인 인터페이스는 잘 변경되지 않는 대상이며, 상위정책에 해당합니다. 때문에 이런 인터페이스에 의존하도록 의존성의 방향을 틀어 주어야합니다.

그렇다면, 이 원칙이 의존성의 방향을 관리하고자 하는 원칙이라는 것은 알겠는데, 왜 '역전'이라는 이름을 사용한 것일까요? 이는 객체지향이 아닌, 전통적인 방식의 개발에서는 상위정책에 해당하는 모듈이 하위정책에 해당하는 모듈에 의존하는 경향이 있었기 때문에 그렇습니다. 의존성의 방향을 상위 -> 하위에서 하위 -> 상위로 바꾸었기 때문에 이것을 '역전'했다고 볼 수 있는 것입니다.

아직까지는 추상적인 내용일 수 있을 것 같습니다. 하지만, 이런 추상적인 내용들은 코드를 통해서 살펴볼 때, 비로소 이해될 것이라고 생각합니다. 제가 프로젝트에서 의존성 역전 원칙을 적용해나간 과정을 함께 살펴보겠습니다.


✨ 리액트에서 의존성 역전 원칙을 적용해보기 :

제가 만들었던 프로젝트는 블로그 프로젝트였으며, 이 글에서 소개하는 예제는 블로그에서 글을 작성할 때 사용되었던 컴포넌트들입니다. 사실 저는 처음부터 의존성 역전 원칙을 적용해야겠다는 목적을 가지고 컴포넌트들의 설계를 변경했던 것은 아닙니다. 그저 한 스텝 한 스텝씩, 지켜야줘야만 할 것 같은 원칙들을 떠올리면서 리팩토링 해나갔더니 의존성 역전 원칙이 떠올랐고 조금 더 세부적으로 원칙을 적용하기까지 이르렀습니다. 때문에 제가 어떤 사고과정을 거쳐 결론의 코드에 다다랐는지 설명해 보려합니다. 이를 통해서 어떤 코드에서 어떻게 개선되었는지, 그 결과 어떤 효과를 얻게 되었는지에 대해서 함께 알아보겠습니다.

1단계 : 구체적인 구현에 의존하던 컴포넌트를 발견하기

제가 의존성 역전 원칙을 공부하던 중, 딱 눈에 들어온 컴포넌트들이 있었습니다. 바로, 블로그 글을 작성할 때 사용되고 있던 컴포넌트들입니다. 처음 상황은 다음과 같았습니다. 아래 코드는 제목을 입력받는 컴포넌트입니다.

const TitleInput = () => {
    const [articleElement, setArticleElement] = useRecoilState(articleState)
    
    const handleOnChange = (e) => {
    	setArticleElement({...articleElement, title : e.target.value}
    }
	...
    return <input onChange={handleOnChange} />
}

간단히 설명드리자면, recoil을 활용해 제목과 관련된 정보를 업데이트 하고 있습니다. 이와 비슷한 방식으로 제목, 컨텐츠, 썸네일url, 태그, 설명글에 해당하는 요소들을 업데이트하고 있었습니다.

만들어진 이 컴포넌트들을 보면서, 눈에 들어온 불편한 사실은 3가지였습니다.
1) 5개나 되는 컴포넌트들 사이에 반복되는 로직이 있다. (아티클 작성에 필요한 요소를 업데이트하는 로직)
2) 구체적인 구현에 의존하고 있다.
3) 각각의 컴포넌트가 상태를 변경하는 로직에 대해 자세히 알고있다. (캡슐화가 덜 되었다)

이 사실을 그림으로 표현하면 다음과 같을 것입니다.

해당 그림을 통해 알 수 있는 것은 각각의 컴포넌트들이 직접적으로 recoil에 의존하여, 상태를 변경하고 있다는 것입니다. 만약 여기서 상태관리와 관련된 로직에 변경이 생긴다면 빨간색으로 칠한 다음의 영역에 변경이 일어날 것입니다. 5개의 컴포넌트 모두에 변경이 전파되네요.

때문에 가장 먼저 시도했던 것은, 반복되는 로직을 공통되게 묶어주고 각각의 컴포넌트에서 의존하고 있던 구체적인 구현을 걷어내는 것이었습니다.

2단계 : 반복과 구체적인 구현을 걷어내기

먼저는 반복되는 로직을 정의내렸습니다.

아티클에 필요한 요소들을 업데이트한다.

이렇게 정의된 로직을 통해 하나의 커스텀 훅을 만들었습니다. useArticleElement라는 훅 입니다.

export const useArticleElement = () => {
  const [articleElement, setArticleElements] = useRecoilState(articleState);

  const setArticleElement = (element : Partial<ArticleElement>) => {
    setArticleElements({ ...articleElement, ...element });
  };

  return { articleElement, setArticleElement };
};

이제 이 훅을 들고가서, 각 컴포넌트에서 사용합니다.

// 아직까지는 각 컴포넌트들이 직접 해당 커스텀 훅을 가져오고 있습니다. 
import { useArticleElement } from 'src/hooks/useHandleArticle'; 


const TitleInput = ( ) => {
  const { articleElements, setArticleElement } = useArticleElement();
  
  ...
  
  return (
    <input
      onChange={(e) => setArticleElement({ title: e.target.value })}
      value={articleElements.title}

	 ...
     

    />
  );
};

뭔가 변경되었는데, 이렇게 변경해서 얻을 수 있는 효과가 무엇일까요? 그 내용을 한번 정리해보겠습니다.

1) 반복을 줄였다 :

각 컴포넌트에서 직접 작성해주고 있던 상태 업데이트 로직을 하나의 훅으로 만들었습니다. 그 결과 useArticleElement라는 훅 하나로 5개의 컴포넌트에서 사용할 수 있게 되었습니다. 반복을 줄이는 것은 '거의' 옳죠?

2) 구체적인 구현에 대한 의존을 없앴다 :

처음에는 리코일을 통한 상태업데이트 로직에 강하게 결합되어 있었습니다. 하지만, 커스텀 훅을 만듦으로써 각 컴포넌트는 상태가 '어떻게' 업데이트 되는지는 모르게 되었습니다. useArticleElement 훅이 상태를 업데이트하기 위해서 리덕스를 사용하는지, 리코일을 사용하는지, jotai를 사용하는지 모르며, 어떤 방식으로 상태를 업데이트 하는지 컴포넌트는 모른다는 것입니다. 그저 그들은 해당 훅을 사용만 할 뿐입니다.

이것이 만들어내는 효과는 앞으로 상태 업데이트와 관련된 로직에 어떤 변화가 생겨도, 컴포넌트 입장에서는(그 훅을 사용하는 입장에선) 코드 상에 어떤 변경도 일어나지 않는다는 것입니다. 처음에 리덕스를 사용하다가 리코일로 마이그레이션을 하더라도, 해당 훅을 사용하는 각 컴포넌트에서는 코드를 수정을 할 이유가 없어집니다. 변경의 전파가 차단된 것이죠.

위의 변경사항을 적용한 결과를 표현해보니 아래와 같은 그림이 나오게 되었습니다.

이제 여기서 상태변경 로직에 변경 사항이 생겼다고 해보겠습니다. 그럼 이때 변경을 가해줘야 하는 영역은 다음과 같습니다.

useArticlElement 훅 내부에서 일어난 변경이기 때문에 useArticlElement에서만 변경이 일어납니다. 나머지 해당 훅을 사용하고 있는 컴포넌트들에서는 무슨 일이 일어났는지 모릅니다. 기존에는 5개의 컴포넌트에서 코드의 수정이 일어났지만, 이제는 커스텀 훅 한 곳에서만 코드의 수정이 발생합니다. 아주보기 좋습니다👏🏻

그런데, 만약 여기서 useArticleElement가 아닌 useDummyArticleElement라는 훅이 생겼고, 그 훅에 의존해야한다면 무슨일이 일어날까요?

useDummyArticleElement는 새롭게 생겼기 때문에 '변경'이라치고, 각 컴포넌트들은 직접적으로 해당 훅을 'import'해오고 있었기 때문에, 이번에는 다시 useDummyArticleElement를 'import'해오도록 수정해주어야 합니다.

분명히 모든 컴포넌트들이 하나의 훅에 의존하는 상황이 되면서, 저희가 얻을 수 있는 효과들이 있었습니다. 하지만 의존성 역전원칙이라는 기준 아래에서 볼 때, 약간 아쉬운 점은 상태 업데이트라는 상위정책이 인터페이스의 형태로 정의되지 않았다는 것입니다. 그리고 그 인터페이스에 의존하지 않은 결과 2번째 그림과 같은 변경의 여파를 맞게 되었습니다. 그래서 상태 업데이트를 위한 인터페이스를 만들고, 그 인터페이스에 컴포넌트들이 의존하도록 만들어야겠다고 판단했습니다.

3단계 : 인터페이스를 정의하고, 해당 인터페이스에 의존하게 만들기

우선 타입스크립트를 활용해 useArticleElement라는 커스텀훅이 준수해야만하는 상위 정책을 정의했습니다. 제가 작성한 상위정책의 내용이 무엇인지 이해하는 것은, 글을 읽는 여러분에게 번거로운 과정일 수 있습니다. 때문에 궁금하지 않은 분들은 코드에 대한 설명은 넘어가셔도 좋습니다. 해당 상위정책을 컴포넌트가 어떻게 의존하도록 만들었는지를 확인하신다면, 글의 요지를 분명하게 파악하신 것입니다.

상위정책 정의하기 :

export type UseArticleElement = () => Return;

type Return = {
  articleElements: ArticleElement;
  setArticleElement: HandleArticleElementFunction;
};

export type HandleArticleElementFunction = (
  arg: Partial<ArticleElement>,
) => void;

해당 훅은 함수이며, Return이라는 type을 반환해야합니다. 이 Return이라는 타입은 하나의 객체이며, 이 객체는 articleElement라는 요소와, setArticleElement라는 함수를 포함하고 있어야 합니다.

상위정책을 준수하는 커스텀 훅 :

export const useArticleElement: UseArticleElement = () => {
  const [articleElements, setElement] = useRecoilState(articleState);

  const setArticleElement: HandleArticleElementFunction = (element) => {
    setElement({ ...articleElements, ...element });
  };

  return { articleElements, setArticleElement };
};

위에서 정의했던 상위정책을 커스텀 훅이 준수하도록 만들어주었습니다. 상위정책의 내용대로, 어떤 객체를 반환하는 함수이며, 그 반환된 객체가 가지고 있는 내용은 articleElements라는 객체와 setArticleElement라는 함수입니다. 정확히 상위정책을 준수하는 함수입니다.

컴포넌트가 상위정책에 의존하게 만들기

이제 이 상위정책을 컴포넌트들이 의존하도록 만들겠습니다.

import { UseArticleElement } from 'src/types/article'; // 직접적으로 훅을 가져오던 코드가 사라지고, 인터페이스를 가져오고 있습니다. 

interface Props {
  useArticleElement: UseArticleElement; // props 에는 UseArticleElement 인터페이스를 준수하는 커스텀훅이 들어와야하도록 정의해줍니다.  
}

const TitleInput: React.FC<Props> = ({ useArticleElement }) => {
  const { articleElements, setArticleElement } = useArticleElement();
  ...

컴포넌트들의 인터페이스에 UseArticleElement를 정의해주었습니다. 이렇게 수정하고나니 각 컴포넌트들은 더 이상 직접적으로 훅에 의존하지 않게 되었습니다.(컴포넌트 내부에서 직접 import 해와서 사용하지 않고 있습니다.)

이 시점에서 다시 의존성의 방향을 그림으로 표현하면 다음과 같습니다.

이제 컴포넌트들은 구체적인 구현에 의존하지 않고, 상위정책에 해당하는 인터페이스에 의존하게 되었습니다. 의존성 역전 원칙에 대한 정의를 준수하고 있다고 볼 수 있습니다. 하지만 2단계까지만 해도 구체적인 구현에 의존하지 않도록 캡슐화가 되어있는 상황이었는데, 굳이 인터페이스를 만들어서 해당 인터페이스에 의존하도록 만들어 준 것, 그러니까 2단계에서 3단계로 넘어오면서 우리가 얻을 수 있는 어떤 효과에는 무엇이 있었을까요?

인터페이스에 의존함으로 얻을 수 있었던 효과 :

1) 인터페이스를 준수하기만 한다면, 어떤 훅이든 사용할 수 있다.

만약 새로운 훅이 만들어져서, 컴포넌트에서 사용해야한다 할지라도, 더 이상 컴포넌트 내부에서는 어떤 변경도 일어나지 않습니다. 만약 useDummyArticleElement라는 훅이 만들어졌고, 그 훅이 UseArticleElement라는 인터페이스를 준수하고 있다면, 컴포넌트의 props로 해당 훅을 넘겨주기만하면 됩니다. 애초에 각 컴포넌트에서 직접적으로 훅을 import 해오는 상황이 아니었기 때문에, import문 조차 수정 할 필요가 없습니다.

2) 커스텀 훅에 대한 의존성은 부모 컴포넌트에게만 생긴다.

2단계까지만 도달했어도, 구체적인 구현에 의존하지 않게 되었기 때문에 내부로직이 변경되어도, 컴포넌트들 사이에 변경사항이 전파되지 않을 수 있었습니다. 하지만, 2단계에선 각 컴포넌트들이 직접 훅을 가져와서(import) 사용하고 있었기 때문에, 다른 훅으로 변경되었을 경우에 그 훅을 사용하던 컴포넌트들에 약간의 변경사항이 전파되게 됩니다.
하지만 3단계에서 인터페이스에 의존하게 만듦으로써, 새로운 훅을 사용하게 된다고 할 지라도, 부모에서만 훅을 가져와 사용하면 되기 때문에 컴포넌트 내부에서는 변경 사항이 전파되지 않습니다. 부모에서 가져와 컴포넌트로 넘겨주는 부분을 보면 다음과 같습니다.

 import {
  useArticleElement,
} from 'src/hooks/useHandleArticle';

const CreateArticleModal = () => {
      ... 
  return (
        ...
        <TitleInput useArticleElement={useArticleElement} />
	
		<TextEditor useArticleElement={useArticleElement} />	
	
        <ImageUpload useArticleElement={useArticleElement} />

        <TagInput useArticleElement={useArticleElement} />

        <DescInput useArticleElement={useArticleElement} />
        ...

이처럼 실제 구현에 해당하는 함수는 부모에서 내려주게 됩니다. 그리고 그 함수가 각각의 컴포넌트들이 의존하고 있는 UseArticleElement라는 상위정책(인터페이스)를 준수하기만 한다면 어떤 함수든지 내려줄 수 있습니다. 때문에 다른 함수를 이용하게 된다고 할 지라도, 각각의 컴포넌트에서는 더 이상 코드의 변경이 일어나지 않습니다. 해당 내용을 그림으로 표현하면 다음과 같습니다.

CreateAritcleModal라는 부모 컴포넌트가 현재 5개의 컴포넌트에 의존하고 있는 상황이며, 직접 커스텀훅을 가져오면서 의존하고 있습니다. 이제 이 상황에서 상태 업데이트와 관련된 로직에 변경이 생긴다면, 자식 컴포넌트든, 부모 컴포넌트든 어디에서도 변경이 일어나지 않고 커스텀 훅 내부에서만 변경이 생깁니다.

만약 추가로 useDummyArticleElement가 추가된다면, 2단계에서 봤던 상황과는 달리 바로 위의 그림처럼 딱 부모 컴포넌트에서만 코드의 변경이 일어납니다. 새로운 useDummyArticleElement를 가져와 각각의 컴포넌트에 해당 커스텀훅을 내려줄 지 결정하기만 하면 됩니다.

지금까지 제가 프로젝트에 어떻게 의존성 역전 원칙을 적용했는지, 그 결과 얻을 수 있는 효과는 무엇이었는지를 소개해보았습니다. 가장 중요한 효과는 코드의 변경이 줄어들었다는 점입니다. 사실 제가 프로젝트에서 적용한 내용은 꽤나 미미한 수준이라고 생각됩니다. 하지만, 제가 이번에 적용했던 방식에서 더욱 추상화된 원리를 잘 뽑아내기만 한다면, 더욱 복잡한 구현에 대해서도 의존성 역전 원칙을 적용해나갈 수 있을 것이라 생각합니다.

원리를 뽑아낸다는 차원에서 생각해볼 때, 아직 어떻게 해당 원칙을 적용할 수 있을지에 대하여 정돈되었다는 생각이 들지 않습니다. "여러분, 이 글을 읽고 의존성 역전 원칙을 적용해보세요." 라고 한다면 쉽지 않을 것 같습니다. 그래서 해당 내용을 단계별로 조금 더 정리해보려고 합니다.


✨의존성 역전 원칙을 적용하기 위한 사고의 프레임워크

해당 사고의 프레임워크는 어디에서 읽은 적도 없고, 들은 적도 없습니다. 그저 이번 시간에 제가 프로젝트에 의존성 역전 원칙을 적용해나가면서, 다음 번에도 적용할 일이 있을 땐 이런 사고의 과정을 거쳐야겠다고 생각하며 정리한 것입니다. 그럼 바로 해당 프레임워크를 알아봅시다.

1) 구체적인 구현에 의존하고 있는 컴포넌트를 발견하기

말씀 드렸지만, 의존성 역전 원칙은 의존성이 적절한 방향을 가지도록 강제하는 원칙입니다. 이를 통해서 프로젝트를 유연하게 만들고, 코드의 수정을 용이하게 만드는데에 목적이 있습니다. 그렇다면 저희가 먼저 해야 할 일은 내가 작성하고 있는 컴포넌트가, 특정 로직에 지나치게 의존하고 있는 것은 아닌지 발견하는 일입니다. 만약 컴포넌트가 구체적인 로직을 너무 잘 알고 있는 상황이고, 해당 로직을 변경할 때마다 컴포넌트 내부에 수정을 가해줘야 하는 상황이라면, 의존성 역전 원칙을 적용할 시점은 아닐까 의심해보아야 합니다.

2) 이것이 오버엔지니어링은 아닌지 판단하기

컴포넌트가 특정 로직에 구체적으로 의존하고 있다고 해서, 곧바로 의존성 역전 원칙을 적용하면 되는 것일까요? 그렇지는 않습니다. 주어진 컴포넌트에 의존성 역전 원칙을 적용하는 것이 어떤 효과를 가져올 것인지 따져보아야 합니다. 장기적으로 봤을 때, 이것이 정말로 유지보수를 하기에 이점이 있는지를 판단해보아야 합니다. 굳이 큰 효과를 가져오지 않을 상황인데도, 무턱대고 특정 원칙을 적용하려 한다면 이는 오버엔지니어링이 될 가능성이 있습니다. 때문에 팀원 간의 의사소통이 필요하며, 심사숙고 후 필요하다고 판단되었을 때 시도하는 것이 좋습니다.

저는 어떤 과정을 통해 원칙을 적용해야겠다고 판단했는지 말씀드리겠습니다. 만약 블로그 글을 작성하기 위해서 상태를 업데이트 해줘야하는 컴포넌트가 단 하나밖에 없었다고 가정해보겠습니다. 그 컴포넌트에서만 상태를 업데이트하는 로직을 가지고 있었다면 저는 의존성 역전 원칙을 적용하겠다고 결정하지 않았을 것입니다. 하지만 유사한 컴포넌트가 여러 개 존재하며, 그 특정 로직을 여러 컴포넌트에서 사용하는 상황이었기 때문에, 해당 로직이 변경되어야 할 때 코드의 변경이 많이 일어날 것으로 판단되었습니다. 그래서 변경의 전파를 차단하기 위해 리팩토링을 하던 중 의존성 역전 원칙을 적용하는 것이 좋겠다는 생각을 하게 되었습니다.

저는 이런 사고의 과정을 거쳤습니다. 특정 로직에 구체적으로 컴포넌트가 의존하고 있다고 무작정 특정 원칙, 패턴을 적용하려하는 것에는 주의가 필요합니다.❗️ 특정 로직에 변경이 생겼을 때 코드의 변경 사항이 얼마나 전파될 것일지를 판단하며 설계를 진행해야 한다는 것입니다. 변경사항의 전파가 위험하게 느껴진다면, 객체지향의 원리들을 떠올리며, 의존성 역전 원칙도 함께 고려해봅시다.

3) 안정적인 인터페이스를 정의하기

3단계까지 왔다면 이제 의존성 역전 원칙을 적용하기로 마음먹은 시점입니다. 이때 우리가 해 줘야 하는 일은, 컴포넌트가 의존하고 있는 대상이 무엇인지 확인하는 것입니다. 그리고 해당 대상을 조금 더 추상화 시킨 것들에는 무엇이 있는지 생각해 보아야합니다. 예를 들어서, 특정 컴포넌트가 '조조할인'이라는 할인의 구체적인 방법에 의존하고 있다면, 더 추상화 시킨 개념인 '할인 정책'을 떠올리면 되는 것입니다.

저의 프로젝트를 예로 들어보겠습니다. 저의 프로젝트에는 제목 입력 컴포넌트, 설명 글 입력 컴포넌트, 본문 입력 컴포넌트 등등이 존재했습니다. 기존에는 '제목'과 관련된 상태를 업데이트 하는 로직, '설명글'과 관련된 상태를 업데이트하는 로직, '본문'과 관련된 상태를 업데이트하는 로직이라는 구체적인 로직에 의존하고 있었습니다. 이런 로직들을 한 단계 더 추상화시킨다면, '블로그의 글 요소들의 상태를 업데이트하는 로직'으로 정의내릴 수 있습니다.

이와 같은 방법으로 여러분들의 프로젝트에서 구체적인 로직에 해당하는 내용을 더 추상적인 개념으로 승급(일반화)시키는 것이 필요합니다. 그런 추상적인 대상, 특정한 문맥을 벗어나서도 사용될 수 있는 대상, 잘 변하지 않는 인터페이스에 우리의 컴포넌트들이 의존하게 될 것이니까요.

4) 인터페이스를 준수하는 훅을 만들기

인터페이스가 정의내려졌다면, 정의된 인터페이스를 준수하는 커스텀 훅을 만들어줍니다. 실제 구현에 해당하는 내용입니다. 여기서 구현되어있는 내용들은 사용하는 컴포넌트 입장에서는 그 내부에서 어떻게 구현되어있는 지 알 수 없습니다. 컴포넌트 입장에선 해당 훅을 '사용'만 합니다.

5) 컴포넌트들이 해당 인터페이스에 의존하게 만들기

컴포넌트가 인터페이스에 의존하게 만들어줍니다. 인터페이스는 구체적인 구현보다 한 단계 더 추상화된 내용입니다. 추상화되었다는 것은 구체적인 문맥에 좌우되지 않는다고 볼 수 있습니다. 구체적인 문맥에 좌우되지 않는 대상에 컴포넌트를 의존하게 만들어두면, 컴포넌트는 변경으로부터 훨씬 더 안전해질 수 있습니다.

6) 컴포넌트들의 부모 컴포넌트에서 해당 훅을 내려주기

이제 각 컴포넌트는 인터페이스에 의존하게 되었습니다. 그리고 이제 실제로 해당 인터페이스에 적절한 구현을 넣어주면됩니다. 저희가 해당 인터페이스를 준수하도록 만들어 둔 커스텀 훅을 넣어주는 것입니다.

이렇게 사고의 프레임워크을 알아보았습니다. 이를 다시 요약정리하자면 다음과 같습니다.

1) 구체적인 구현에 의존하고 있는 컴포넌트를 발견하기
2) 오버엔지니어링은 아닌지 판단하기
3) 안정적인 인터페이스를 정의하기
4) 인터페이스를 준수하는 훅을 만들기
5) 컴포넌트들이 해당 인터페이스에 의존하게 만들기
6) 컴포넌트들의 부모 컴포넌트에서 해당 훅을 내려주기


혹자의 의문🤔

누군가 품을 수 있는 의문에 대해서 다루어보겠습니다.

의존성 역전 원칙을 적용했다기엔 너무 간단한 로직에 적용한거 아닌가요?

맞습니다. 저도 그렇게 생각합니다. 이번에 제가 적용했던 로직은 단순한 상태 업데이트와 관련된 로직이었고, 생각보다 간단한 요구사항이었습니다. 사실 조금 더 멋드러진 예시를 가지고 오고 싶었지만, 해당 원칙을 공부하던 와중 제 눈에 들어온 내용이 딱 이것이었습니다.

그러나 해당 로직이 단순하고 단순하지 않고를 떠나서, 이 로직에 의존성 역전 원칙을 적용함으로써 변경의 여파를 확실히 줄일 수 있었습니다.

이렇게 간단한 구현 속에서도 적용했던 경험에서 추상화 된 패턴을 찾아내기만 한다면, 더욱 복잡한 구현의 영역에서도 적용할 수 있을 것이라 생각합니다. 이 내용이 바로 독자 여러분이 얻어가길 바라는 인사이트입니다.

리코일에서 다른 상태 관리 라이브러리로 옮길 가능성이 있을까요?

아마 미미한 가능성일 수도 있습니다만, 상태관리 라이브러리를 마이그레이션 할 가능성은 언제든지 있습니다.

제가 다니던 회사에서도 리덕스에서 리코일로 마이그레이션을 논의중이었으며, 지인의 회사에서도 리덕스에서 zustand로 옮겼다는 소식을 들었습니다. 만약 이렇게 마이그레이션을 하는 상황이 왔는데, 각각의 컴포넌트에서 리덕스코드를 무작정 사용하고있다가는 변경의 여파를 쎄게 맞을 수도 있습니다. 물론 직접적으로 사용하는 모든 리덕스코드에 대한 의존성을 걷어내야 한다는 것은 아니지만, 적절한 수준에서 구체적인 구현에 대한 의존성을 피하는 방향을 모색해보는 것이 필요합니다. 적절하게 변경사항을 잘 격리해놓았다면, 리팩토링이든, 마이그레이션이든 자신감있게 해나갈 수 있을 것이라 확신합니다.

아무튼, 이 글의 요지는 리코일에서 다른 상태관리 라이브러리로 옮길 가능성이 있느냐 아니냐가 아닙니다. 그런 일이 일어나지 않는다 할지라도, 변경가능성이 있는 구체적인 구현에 의존하다가는 변경에 심각하게 취약하게 될 가능성이 있다는 것을 깨닫는 것이 중요합니다.

어, 근데 이거 개방 폐쇄 원칙이라고도 할 수 있는게 아닌가요? 기존의 코드에는 변경이 생기지 않고 기능 확장에는 열리게 된 것 같은데?!

맞습니다. 현재 저의 변경된 코드는 개방폐쇄원칙이 적용되었다고 할 수도 있습니다. 기능에 확장이 생긴다 할 지라도(ex. useDummyArticleEleme
nt가 추가된다 할지라도) 기존의 코드에는 어떤 변경도 일어나지 않기 때문입니다. 사실 개방폐쇄원칙을 지키게 되면, 어느 정도는 의존성 역전 원칙도 함께 달성하게 됩니다. 개방폐쇄원칙도 마찬가지로 추상 인터페이스에 의존하게 만들기 때문입니다. 사실 SOLID 원칙들은 엄밀하게 구분되어있다고 말하기가 쉽지 않습니다. 각각의 원칙들이 서로를 보완하는 방식으로 이루어져있기 때문입니다. 이를테면, 리스코프 치환원칙이 잘 적용되었다는 것은, 인터페이스 분리원칙이 잘 적용되었다고도 볼 수 있는 것입니다.


결론

글을 마무리하려 합니다. 객체지향과 관련된 대부분의 학습자료는 자바코드를 기반으로 되어있었습니다. 프론트엔드인 저의 입장에서, 이를 어떻게 리액트 프로젝트에 녹여낼 것인가하는 것은 그리 쉬운 문제는 아닌 것 같았습니다. 때문에 관련된 여러 자료들을 찾아보면서, 다시 한번 생각해보게 되었습니다. 결국 객체지향이 목적하는 바가 무엇인지를 이해하고, 각각의 원칙들에서 내가 적용할 수 있는 추상화된 원리를 끄집어내는 것이 중요하다는 것입니다.

오늘 저는 좋은 설계란 무엇인지, 의존성 역전 원칙이 무엇인지 알아보았습니다. 또한 해당 원칙을 저의 프로젝트에서는 어떻게 적용했고, 나아가 여러분들은 어떤 방식으로 원칙을 적용해나갈 수 있을지에 대해서 다루어보았습니다. 제가 정리한 내용이 정답은 아닐 수 있지만, 부디 바라는 것은 코드의 변경을 어떻게 차단할 것인가에 대한 인사이트를 여러분이 얻어가는 것입니다.

부디 여러분의 프로젝트가 오늘의 기능을 성공적으로 수행하고, 내일의 변경에도 유연하게 대처하게 되기를 바라며 글을 맺겠습니다🙏


참고자료 :
Applying SOLID principles in React
Apply the Dependency Inversion Principle in React
Dependency Inversion in React
객체지향 개발 5대 원리 : SOLID
오브젝트

profile
맛있는 세상 shdpcks95@gmail.com

10개의 댓글

comment-user-thumbnail
2022년 12월 18일

좋은 글 감사합니다.

1개의 답글
comment-user-thumbnail
2022년 12월 19일

정말 좋은 글입니다. 잘 읽었습니다. 중요한 것은 아니지만, 중간에 jotai에 대한 오타가 있습니다. z -> j

1개의 답글
comment-user-thumbnail
2022년 12월 19일

I am extremely satisfied with your post and the information in your blog. I hope to see more excellent posts soon tanuki sunset.

1개의 답글
comment-user-thumbnail
2022년 12월 24일

I'm especially stayed aware of the article and I will get many benefits from it. Subsequently, thank you for sharing it. LTD Commodities Catalog Online

답글 달기
comment-user-thumbnail
2023년 5월 4일

좋은 글 감사합니다.

6) 컴포넌트들의 부모 컴포넌트에서 해당 훅을 내려주기
저는 여기서 vac패턴이 생각났어요.
결국은 컨테이너 패턴 => 커스텀훅 => 부모에서 자식으로 커스텀훅을 내려주기 이런 방향으로 가는게 맞는것같다는 생각이드네요.(물론 상황에 따라 적절한 방법을 선택해서 오버엔지니어링을 막아야하지만)

저는 보통 코딩할때 2단계를 적용하고 있었고 최근에 3단계의 필요성을 느끼고 적용하려고 있을 때 딱 이 글을 봤네요. 조금 막연한 생각을 이 글을 통해서 정리할 수 있었어요.

답글 달기
comment-user-thumbnail
2023년 8월 11일

좋은 글 감사합니다. 선임 개발자분이 커스텀 훅을 만들어 컴포넌트에 넘겨주는 방식으로 리팩토링을 제안하셨을 때, 신입 개발자였던 저는 그 의도를 완전히 이해하지 못하고 있었던 기억이 납니다. 이제서야 비로소 그 의미를 깨닫게 되네요.

최근 리액트 컴포넌트를 잘 설계하고 싶다는 고민을 많이 했었는데, 큰 도움이 되었습니다!

답글 달기
comment-user-thumbnail
2023년 12월 13일

Applying object-oriented principles in React mainly begins with treating components as objects and designing interactions between them in an object-oriented backpack battles manner. While designing the structure, think about principles such as single responsibility, encapsulation, and inheritance!

답글 달기