SOLID 원칙과 프론트엔드 개발 (1) SRP, OCP

개발하고싶은편·2022년 8월 9일
4
post-thumbnail

비전공 개발자이다보니 컴퓨터 사이언스에 대한 지식이 다른 전공 개발자에 비해 부족하다는 의식을 늘 갖고 있었다. 그런 찰나에 지난 6월부터 정보처리기사를 준비해서 7월에 필기 시험을 마쳤다. 취업 준비와 여러 사정으로 긴 시간 준비하지 못했지만, 아슬하게 필기 시험에 합격했다. 정보처리기사를 공부하면서 특히 프론트엔드 개발에서도 갖춰지면 좋은 (사실 갖춰져야 하는) 개발 원칙들이 몇 가지 있었다. 그 중 하나가 바로 SOLID 원칙이다.

SOLID 원칙

SOLID 원칙은 로버트 마틴이란 개발자가 2000년에 명명한 객체 지향 프로그래밍의 5대 원칙의 앞 글자를 따서 만든 말이다.

  • Single Responsibility 단일 책임
  • Open/Close 개방-폐쇄
  • Liskov Substitution 리스코프 치환
  • Interface Segregation 인터페이스 분리
  • Dependency Inversion 의존관계 역전

위의 다섯 가지 원칙이 객체 지향 프로그래밍과 설계를 위한 다섯 가지 원칙이고, 이 원칙은 유지 보수나 추후 시스템의 확장성을 고려해서 적용되는 지침이기도 하다.

프론트엔드에서 SOLID

이 원칙은 객체 지향 프로그래밍을 전제한 원칙이기 때문에, JavaScript를 주요 언어로 사용하는 프론트엔드 개발 영역에서는 완전히 어울린다고 보기 힘들 수 있다. 특히 프론트엔드 개발에서 현재 대세인 React의 경우, 함수형이기 때문에 더욱 그럴 수 있다. 하지만 SOLID 원칙이 내세우는 원칙의 개념이 결국 프론트엔드 개발에서도 적용된다고 생각했다.

사실 이 글을 작성하기 위해 자료를 찾으면서, 실제로 SOLID 원칙과 프론트엔드 개발을 연관 지은 블로그 포스트도 여럿 발견할 수 있었다. 이에 SOLID 원칙을 프론트엔드, 특히 React 개발과 결부시켜 해석해보려고 한다.

단일 책임

단일 책임은 '한 컴포넌트나 함수가 하나의 책임만 가져야 한다'고 정의하면 적절할 것 같다. 실제로 프론트엔드 개발 과정에서는 하나의 컴포넌트 안에 여러 기능을 넣지 않고, 150~200줄이 넘는 코드는 여러 방법으로 분리할 것을 강력히 권장한다.

  • 여러 작업을 수행하는 컴포넌트는 역할에 따라 컴포넌트 쪼개기
  • 컴포넌트와 연관성은 있지만 핵심 역할이 아닌 기능은 커스텀 hook으로 캡슐화
// app.jsx
function App () {
  	const [user, setUser] = useState([]);
	useEffect(() => {
      const userData = async () => {
        const res = await fetch('/api');
        const data = await response.json();
        setUser(data);
      };
      userData();
    }, []);
  return (
    <div>
      <img src="/logo.svg" />
      <nav>
      	<ul>
    		<li>{user ? "로그아웃" : "로그인"}</li>
			<li>할 일 전체</li>
    	</ul>
      </nav>
      <ul>
      {user.todo.map((el) => (
          <li key={el.id}>
              <strong>{el.title}</strong>
              {el.content} ({el.isDone ? "완료" : "진행중"})
          </li>
      ))}
      </ul>
	  <>
	</div>
    );
};

이런 컴포넌트 하나가 있다고 가정해본다. 유저 정보를 useState와 useEffect로 가져오는 부분은 useUser라는 이름으로 커스텀한 hook으로 빼내도 좋을 것 같다.
또 한 컴포넌트 안에서 로고 이미지와 내비게이션, Todo 리스트를 모두 보여주고 있으니, 컴포넌트의 역할을 고려하여 내비게이션과 Todo는 별도의 컴포넌트로 분리시켜주고 이 컴포넌트에 분리한 컴포넌트를 import해오는 것이 좋을 것 같다.

개방-폐쇄

개방-폐쇄는 확장을 위해 열려있되 수정을 위해 닫혀 있어야 한다는 원칙이다. 즉 기존의 코드는 변경하지 않으면서 기능은 확장할 수 있도록 개발해야 한다는 의미이다.

특히 선언형 프로그래밍을 채택하는 React는 컴포넌트 UI를 JSX 형식으로 개발하다보니, 선언된 코드 안에 조건에 따라 여러 기능을 중첩시키는 경우가 있다.

// components/Header.jsx
const Header = () => {
  const { pathname } = useRouter();
  return (
    <header>
      <Logo />
      {pathname === "/write" && <button type="submit">제출</button>}
      {pathname === "/" && <button type="button">글 작성</button>}
    </header>
    );
};

// index.jsx
const Home = () => {
  return (
   <div>
   	<Header />
    <List />
   </div>
  );
 };

// post.jsx
const Post = () => {
  return (
   <div>
   	<Header />
    <Form />
   </div>
  );
 };

현재 페이지의 위치를 판단하는 변수 로직에 따라 다른 UI 요소를 렌더링하는 컴포넌트다. 대표적으로 Header 같은 기능을 하는 컴포넌트들이 구현되는 방식이다. 위 코드는 최초엔 Home과 Post 페이지에 적용되는데 문제가 없지만, Header가 필요한 다른 페이지를 생성할 때마다의 Header.jsx의 코드를 수정해야 하는 확장성의 결함이 발생하기 때문에 개방-폐쇄 원칙에 위배된다.

기존에는 Header에 각 페이지마다 렌더링될 UI를 따로 정해두었기 때문에, 페이지간 컴포넌트의 결합도가 높아지고 각 컴포넌트의 응집도가 취약해졌다. 이를 해결하기 위해, children prop으로 페이지마다 사용할 Header에 필요한 요소를 위임할 수 있다.

// components/Header.jsx
const Header = ({ children }) => {
  return (
    <header>
      <Logo />
      <>{children}</>
    </header>
    );
};

// index.jsx
const Home = () => {
  return (
   <div>
   	<Header>
    	<button type="submit">제출</button>
    </Header>
    <List />
   </div>
  );
 };

이 같은 방식을 통해 컴포넌트 자체를 수정하지 않고도 원하는 것을 모두 입력할 수 있는 합성(Composition)을 사용할 수 있다.

참고 자료

profile
이야기를 좋아합니다.

0개의 댓글