티스토리 뷰

▶SOLID 원칙에 기초한 React 코드 작성법

용어만 들어왔던 SOLID 원칙에 대해 자세하게 정리해 보려고 합니다.

React 공식문서에 React로 사고하기(https://ko.reactjs.org/docs/thinking-in-react.html)라는 단락이 있습니다.

여기에서는 프로그래밍의 유지보수성과 확장성에 도움이 되는 전략을 소개하고 있는데요.

 

🔎 SOLID ?


SOLID는 단일 책임 원칙(SRP), 개방 폐쇠 원칙(OCP), 리스코프 치환 원칙(LSP), 인터페이스 분리 원칙(ISP), 의존관계 역전 원칙(DIP)의 다섯가지 원칙의 앞글자를 딴 명칭입니다.

 

공식문서에서 언급한 만큼 다섯가지 원칙에 대해 하나씩 알아보도록 하겠습니다 :)

 

단일 책임 원칙(Single Responsibility Principle)

함수나 클래스는 한 가지 기능만 수행해야 한다.

 

하나의 함수나 클래스에 여러 가지 기능을 집어넣다보면 시스템은 점차 비대해지고, 한 부분이 고장나면 전체가 무너질 수 있습니다.

행동이 각각 격리되어 있으면 연쇄적인 사이드이펙트가 발생할 여지가 줄어들고 가독성도 올라갑니다.

또한 확장성도 높아지고 유지보수 비용의 감소로 이어집니다.

 

❓ 사이트 이펙트

사이드 이펙트란 원래의 목적과 다르게 다른 효과 또는 부작용을 의미합니다.

쉽게 말해, '의도하지 않은 결과' 말할 수 있겠습니다.

 

React에서는 특히 컴포넌트를 분리할 때 단일 책임 원칙을 적용해야 합니다.

function SomePage() {
  const [data, setData] = useState(null);
  const [inputValue, setInputValue] = useState('');

  function handleChange(e) {
    setInputValue(e.target.value);
  }

  async function handleSubmit(inputValue) {
    const response = await fetch(`https://example.com/search/${inputValue}`);
    const res = await response.json();
    if (res.ok) {
      setData(res.data);
    }
  }

  return (
    <div>
      <header>{/* huge amount code for header */}</header>
      <input value={inputValue} onChange={handleChange} />
      <button onClick={handleSubmit}>Save</button>
      {data.type === 'A' ? <p>A type is ....</p> : null}
      {data.type === 'B' ? <p>B type is ....</p> : null}
    </div>
  );
}

위 코드는 세가지이 문제점을 찾을 수가 있는데요.

1. <header> 안의 코드의 길이가 상당히 길기 때문에 가독성이 좋지 않아 이해하기가 쉽지 않을 것이다.

2. <input>의 글자가 작성되면서 inputValue의 값이 업데이트될 때마다 컴포넌트 전체가 계속 리렌더링될 것이다.(onChange 때문에)

3. data의 또 다른 type 값을 처리해 주어야 하는 경우에 확장성이 좋지 않다.

 

이러한 문제점을 해결하기 위해 단일 책임 원칙을 적용하면 다음과 같이 작성할 수 있습니다.

function SomePage() {
  const [data, setData] = useState(null);

  async function handleSubmit(inputValue) {
    const response = await fetch(`https://example.com/search/${inputValue}`);
    const res = await response.json();
    if (res.ok) {
      setData(res.data);
    }
  }

  return (
    <div>
      <Header />
      <Form handleSubmit={handleSubmit} />
      <Description type={data?.type} />
    </div>
  );
}

function Header() {
  return <header>{/* huge amount code for header */}</header>;
}

function Description({type}) {
  switch (type) {
    case 'A':
      return <p>A type is ....</p>;
    case 'B':
      return <p>B type is ....</p>;
    default:
      return null;
  }
}

function Form({handleSubmit}) {
  const [inputValue, setInputValue] = useState('');

  function handleChange(e) {
    setInputValue(e.target.value);
  }

  return (
    <div>
      <input value={inputValue} onChange={handleChange} />
      <button onClick={() => handleSubmit(inputValue)}>Save</button>
    </div>
  );
}

위 코드는 컴포넌트 함수 하나 당 단일 기능만이 있도록 컴포넌트를 분리했습니다.

또한 data의 type을 switch 문으로 처리함으로써 좀 더 확장성이 좋게 처리했습니다.

 

개방 폐쇄 원칙(Open Close Principle)

확장에는 열려있고, 변경에는 닫혀 있어야 한다.

 

애매모호한 표현이라 조금 헷갈릴 수 있지만,

쉽게 이야기하면, "기능의 작동"이 변경될 수는 있지만, "기능의 작동을 작성한 코드 자체"를 변경하지 않아야 한다는 말입니다.

코드를 통해 이해해 봅시다.

let iceCreamFlavors = ['chocolate', 'vanilla'];
let iceCreamMaker = {
  makeIceCream: (flavor) => {
    if (iceCreamFlavors.indexOf(flavor) > -1) {
      console.log('Great success. You now have ice cream.');
    } else {
      console.log('Epic fail. No ice cream for you.');
    }
  },
};
export default iceCreamMaker;

위의 코드가 있을 때, 만약 아이스크림의 맛을 새로 추가하고 싶다면 iceCreamFlavor의 배열을 직접 변경해야 합ㄴ디ㅏ.

즉, 코드 자체를 변경해야 합니다.

이러면 사이드 이펙트 문제가 발생할 수 있기 때문에 폐쇠 원칙을 사용해서 다음과 같이 해결할 수 있습니다.

let iceCreamFlavors = ['chocolate', 'vanilla'];
let iceCreamMaker = {
  makeIceCream: (flavor) => {
    if (iceCreamFlavors.indexOf(flavor) > -1) {
      console.log('Great success. You now have ice cream.');
    } else {
      console.log('Epic fail. No ice cream for you.');
    }
  },
  addFlavor: (flavor) => {
    iceCreamFlavors.push(flavor);
  },
};
export default iceCreamMaker;

addFlavor 메소드를 사용해서 코드를 직접 수정하지는 않지만 어느 곳에서든 아이스크림의 맛을 추가할 수 있습니다.

 

앞서 단일 책임 원리를 설명하기 위해 위에 작성한 예시 코드에서 Description 컴포넌트를 또 다른 예로 들 수 있는데요.

function Description({type}) {
  switch (type) {
    case 'A':
      return <p>A type is ....</p>;
    case 'B':
      return <p>B type is ....</p>;
    default:
      return null;
  }
}

이처럼 컴포넌트 코드 자체를 직접 고치는 것이 아니고 Props를 사용해 type별로 리턴값을 변경할 수 있습니다.

 

리스코브 치환 원칙(Liskov Substitution Principle)

파생 클래스는 기본 클래스로 대체 가능해야 한다.

 

확장을 통해 파생된 클래스는 그 기초가 되는 클래스의 기능을 다 사용 가능해야 한다는 말입니다.

이건 자바스크립트의 객체 지향 프로그램 측면의 큰 특징이기 때문에 상속을 사용해서 확장한다면 자동으로 적용되는 부분입니다.

 

React에서는 많이 사용되지는 않는 개념이겠지만

요즘은 React와 함께 Typescript를 많이 사용하기 때문에 Type을 확장할 때 사용될 거 같습니다.

interface Person {
  name: string;
  age: number;
}

interface Worker extends Person {
  hasCareer: boolean;
}

 

인터페이스 분리 원칙(Interface Segregation Principle)

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.

 

인터페이스는 말 그래도 우리가 사용하고 있는 많은 기기들의 조작장치입니다.

스마트폰의 터치스크린, 컴퓨터의 키보드, 마우스, 전자레인지의 버튼, 자동차의 핸들, 브레이크 등 기기를 작동하기 위해서 사용하고 있는 모든 조작장치들이 인터페이스입니다.

 

만약 자동차의 백미러에 브레이크 메서드가 들어가 있다면 어떨까요?

이상은 없겠지만 쓸데 없는 로직이 들어가 있어 낭비일 것입니다.

마찬가지로 객체 지향 프로그래밍의 상속에서도 이러한 낭비가 발생할 수 있습니다.

 

리스코브 치환 원칙에 대한 설명을 위해 작성한 위의 타입스크립트 코드를 보면 Worker가 사용되는 시점에 hasCareer값과 age값만 필요하다면 name은 낭비가 됩니다.

 

의존성 역전 원칙(Dependency Inversion Principle)

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다.

 

실생활의 예를 들어봅시다.

버스기사와 승객은 의존 관계입니다.

승객이 버스기사에게 의존하는 상태입니다.

그런데 승객이 버스기사에게 떼를 써서 자신의 집앞까지 태워달라고 운전기사에게 요구하면 어떻게 될까요?

엉망이 될 것입니다.

그래서 의존하는 사람(승객, 저수준의 모듈)은 의존 받는 사람(버스기사, 고수준의 모듈)에게 직접 무언가를 하면 안 된다는 것입니다.

 

React 코드에서 다시 이해해 봅시다.

import axios from 'axios';
import React, {useEffect, useState} from 'React';

function PostComponent() {
  const [post, setPost] = useState(null);

  function getPost() {
    axios.get('https://example.com');
  }

  useEffect(() => {
    getPost();
  }, [])

  return <div>{post.title}</div>;
}

이 코드에서 PostComponent는 axios에 의존하고 있습니다.

그런데 post값을 업데이트 하기 위해 axios 오픈 소스를 fork해서 뜯어고친다면 어떻게 될까요?

역시 엉망이 될 것입니다..

 

그래서 의존 받는 모듈인 axios에서 이와 같은 처리를 위한 로직을 구현하고 의존 하는 모듈로 내려줘서 처리하는 원칙을 세운 것입니다.

여기에서는 axios에서 만든 callback 함수를 의존하는 모듈인 PostComponent에서 사용해야 합니다.

async function getPost() {
    await axios.get('https://example.com').then((data)=> {
      setPost(data)
    });
}

 

728x90
LIST
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함