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

huurray·2021년 3월 13일
82
post-thumbnail

들어가기

최근 React 공식문서를 다시 읽다가 React로 사고하기 단락에서 단일 책임 원칙을 보았다. 이 원칙은 Robert C. Martin가 정의한 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의 값이 업데이트 될 때마다 컴포넌트 전체가 계속 리랜더링 될것이다.
  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>
  );
}

개방 폐쇄 원칙 (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)
    });
  }

참고

The Single Responsibility Principle by Robert C. Martin
SOLID principles in your React applications
웹돌이님 블로그 - 객체지향 설계 5원칙

profile
Frontend Developer.

11개의 댓글

comment-user-thumbnail
2021년 3월 23일

👍
https://www.w3.org/WAI/tutorials/
저는 이사이트를 사용하는데, 공유합니다

1개의 답글
comment-user-thumbnail
2021년 3월 24일

백미러에 브레이크 메서드를 들어있으면 어떨까 생각하니 너무 확 와닿아요 🤣
좋은 글 감사합니다.

1개의 답글
comment-user-thumbnail
2021년 3월 30일

좋은 글 감사합니다!

1개의 답글
comment-user-thumbnail
2021년 4월 30일

적절한 예제를 들어주셔서 너무 잘 와닿았습니다. 잘 읽었어요!

1개의 답글
comment-user-thumbnail
2021년 8월 11일

감사합니다.

1개의 답글
comment-user-thumbnail
2023년 7월 26일

잘보갑!

답글 달기