비전공 개발자이다보니 컴퓨터 사이언스에 대한 지식이 다른 전공 개발자에 비해 부족하다는 의식을 늘 갖고 있었다. 그런 찰나에 지난 6월부터 정보처리기사를 준비해서 7월에 필기 시험을 마쳤다. 취업 준비와 여러 사정으로 긴 시간 준비하지 못했지만, 아슬하게 필기 시험에 합격했다. 정보처리기사를 공부하면서 특히 프론트엔드 개발에서도 갖춰지면 좋은 (사실 갖춰져야 하는) 개발 원칙들이 몇 가지 있었다. 그 중 하나가 바로 SOLID 원칙이다.
SOLID 원칙은 로버트 마틴이란 개발자가 2000년에 명명한 객체 지향 프로그래밍의 5대 원칙의 앞 글자를 따서 만든 말이다.
위의 다섯 가지 원칙이 객체 지향 프로그래밍과 설계를 위한 다섯 가지 원칙이고, 이 원칙은 유지 보수나 추후 시스템의 확장성을 고려해서 적용되는 지침이기도 하다.
이 원칙은 객체 지향 프로그래밍을 전제한 원칙이기 때문에, JavaScript를 주요 언어로 사용하는 프론트엔드 개발 영역에서는 완전히 어울린다고 보기 힘들 수 있다. 특히 프론트엔드 개발에서 현재 대세인 React의 경우, 함수형이기 때문에 더욱 그럴 수 있다. 하지만 SOLID 원칙이 내세우는 원칙의 개념이 결국 프론트엔드 개발에서도 적용된다고 생각했다.
사실 이 글을 작성하기 위해 자료를 찾으면서, 실제로 SOLID 원칙과 프론트엔드 개발을 연관 지은 블로그 포스트도 여럿 발견할 수 있었다. 이에 SOLID 원칙을 프론트엔드, 특히 React 개발과 결부시켜 해석해보려고 한다.
단일 책임은 '한 컴포넌트나 함수가 하나의 책임만 가져야 한다'고 정의하면 적절할 것 같다. 실제로 프론트엔드 개발 과정에서는 하나의 컴포넌트 안에 여러 기능을 넣지 않고, 150~200줄이 넘는 코드는 여러 방법으로 분리할 것을 강력히 권장한다.
// 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)을 사용할 수 있다.