리팩토링하며 좋은 코드에 대해 알아보자

Chani·2024년 1월 20일
1
post-thumbnail

본 포스팅은 김민태의 프론트엔드 아카데미 : 제 2강 만들어보며 이해하는 React & Redux 강의를 들으며 공부한 내용을 스스로 정리한 것 입니다.

시작하기 앞서..

소프트웨어는 지속적으로 변화하는 환경에 발맞춰 발전해야 합니다. 개발자들은 이 변화를 어떻게 바라봐야 할까요?

중요한 것은 소프트웨어가 어떻게 변화하든 항상 안정적으로 유지되어야 한다는 점입니다.
즉, 변화가 있더라도 소프트웨어가 정상적으로 작동하도록 안정적으로 개발하는 것이 개발자들의 주요 고려사항이 되어야 합니다.


소프트웨어는 변화의 필연성을 안고 있으며, 이러한 변화는 항상 안전하게 이루어져야 합니다.
그렇다면, 어떻게 하면 소프트웨어를 잘 변경할 수 있을까요?

변화를 잘 관리하기 위해서는 먼저 코드를 명확하게 이해해야 합니다.
그러기 위해서는 쉽게 이해할 수 있는 코드를 어떻게 작성할 수 있을지 고민해야 합니다.
이는 결국, 어떤 형태의 코드가 이해하기 쉬운지에 대한 질문으로 귀결됩니다.


이러한 고민들로부터 디자인 패턴과 프로그래밍 패턴과 같은 개념들이 탄생했고, 결국 대부분이 변경의 용이성에 초점을 맞추어 개발된 것 같습니다.

다양한 패턴과 패러다임들은 결국 어떻게 하면 잘 바꿀 수 있을까?, 안전하게 바꿀 수 있을까?, 빠르게 바꿀 수 있을까?, 변경 후에도 올바르게 동작할 수 있을까?, 또는 잘못된 동작이 발생하더라도 그 영향 범위를 최소화할 수 있을까?라는 질문들에 대한 해답을 제공하려고 하고 있다고 생각합니다.

리팩토링하며 좋은 코드 알아보기

Ver 1.0

function createElement(type, props) {
  switch (type) {
    case "h1":
      return [document.createElement("h1")].map((element) => {
        Object.entries({ ...props, "date-id": "title" }).forEach(
          ([name, value]) => element.setAttribute(name, value)
        );
        return element;
      })[0];
    case "div":
      return [document.createElement("div")].map((element) => {
        Object.entries({ ...props, "data-id": "layout" }).forEach(
          ([name, value]) => element.setAttribute(name, value)
        );
        return element;
      })[0];
  }
}

위 코드는 typeprops를 받아 html element 요소를 만들고, 받은 props를 요소의 attribute에 넣어주고, 반환하는 함수입니다.

만약 h1type으로 받는 코드 부분을 고쳐야한다고 가정한다면, 코드의 case h1 부분을 고치면 됩니다.

그리고 고친 코드의 안전성을 테스트하기 위해 createElement 함수를 테스트 해야합니다.

여기에서 이러한 의문이 하나 들게됩니다.
h1에 해당하는 코드만 고쳤는데 왜 div를 담당하는 부분까지 테스트 해야하는 걸까?

그렇다면 현재 이 코드는 변경에 용이한 구조라고 할 수 있을까요?
변경 이후 얼마나 안정성을 확보해 줄 것인가에 대한 관점으로 본다면 좋은 구조라고 할 수는 없을 것 같습니다.

Ver 2.0

function createH1(props) {
  return [document.createElement("h1")].map((element) => {
    Object.entries({ ...props, "date-id": "title" }).forEach(([name, value]) =>
      element.setAttribute(name, value)
    );
    return element;
  })[0];
}

function createDiv(props) {
  return [document.createElement("div")].map((element) => {
    Object.entries({ ...props, "data-id": "layout" }).forEach(([name, value]) =>
      element.setAttribute(name, value)
    );
    return element;
  })[0];
}

function createElement(type, props) {
  switch (type) {
    case "h1":
      return createH1(props);
    case "div":
      return createDiv(props);
  }
}

h1에 해당하는 코드를 고치는 경우에, 그 부분만을 테스트하기 위해, 좀 더 변경에 용이한 구조를 만들기 위해 각각의 케이스에 들어가는 코드를 함수로 분리하였습니다.

이렇게 구현하면 앞서 이야기한 문제를 해결할 수 있을 것입니다.

h1 에 해당하는 코드를 고치면 createH1 함수만 테스트해보면 안정적으로 코드가 동작한다는 것을 알 수 있습니다.
코드가 변경된 부분은 createH1 함수밖에 없으니까요.

하지만 새로운 p 태그에 대한 함수를 추가해야하는 경우라면 어떨까요?

function createElement(type, props) {
  switch (type) {
    case "h1":
      return createH1(props);
    case "div":
      return createDiv(props);
    case 'p':
      return createParagraph(props);
  }

위 코드처럼 createElement 함수에 새로운 case를 추가해야하고, 이렇게 되면 결국 처음과 같은 문제점으로 다시 돌아오게 됩니다.

새로운 case를 추가했을 뿐인데, createElement 함수도 같이 테스트 해야하게 됩니다.

Ver 3.0

function createH1(props) {
  return [document.createElement("h1")].map((element) => {
    Object.entries({ ...props, "date-id": "title" }).forEach(([name, value]) =>
      element.setAttribute(name, value)
    );
    return element;
  })[0];
}

function createDiv(props) {
  return [document.createElement("div")].map((element) => {
    Object.entries({ ...props, "data-id": "layout" }).forEach(([name, value]) =>
      element.setAttribute(name, value)
    );
    return element;
  })[0];
}

const creatorMap = {
  h1: createH1,
  div: createDiv,
};

function createElement(type, props) {
  return creatorMap[type](props);
}

앞서 이야기한 문제를 해결하기 위해 위와 같이 코드를 작성하였습니다.

creatorMap 이라는 맵핑을 위한 객체를 만들고, createElement 함수에서는 이를 사용하여 맵핑된 함수를 호출하게 만들어주면, 새로운 케이스가 추가된다고 하더라도 creatorMap에 한줄을 추가해주면 됩니다.

이정도만 되어도 큰 문제는 없을 것 같지만 아쉬운 부분이 있다면 그것은 createElement 함수가 외부의 creatorMap 이라는 객체를 참조하고 있기 때문에 side effect가 생길 수 있다는 것입니다.

Ver 3.1

function createH1(props) {
  return [document.createElement("h1")].map((element) => {
    Object.entries({ ...props, "data-id": "subject" }).forEach(
      ([name, value]) => element.setAttribute(name, value)
    );
    return element;
  })[0];
}

function createDiv(props) {
  return [document.createElement("div")].map((element) => {
    Object.entries({ ...props, "data-id": "layout" }).forEach(([name, value]) =>
      element.setAttribute(name, value)
    );
    return element;
  })[0];
}

const creatorMap = {
  h1: createH1,
  div: createDiv,
};

const coupler = (map) => (type, props) => map[type](props);
const createElement = coupler(creatorMap);

위 코드와 같이 creatorMap 을 함수의 변수로 받아서 closure를 활용하여 createElement로부터 외부 의존성을 끊어낸다면 위에서 이야기한 문제점도 해결 할 수 있습니다.

정리

처음에 이야기하고자 했던 좋은 코드란 무엇일까요?
저는 이렇게 생각합니다.

변경하더라도 안정적으로 동작하는 코드

하지만 결국 변경에 용이한 코드를 만드려면 변경된 부분만 테스트 할 수 있어야하고, 사이드 이펙트에 코드가 크게 영향을 받지 않도록 해야하는 것 같습니다.

그리고 이러한 코드들이 결국 테스트에 용이한 코드가 되고, 다른 개발자가 파악하기 쉬운 코드가 된다고 생각합니다.

전부터 많은 개발자분들이 테스트 코드를 짜봐야 한다, 테스트를 고려하면 좋은 코드가 나온다 등의 테스트 코드의 중요성에 대해 이야기하는 말을 자주 들었습니다.

이번에 코드를 리팩토링 하는 과정을 통해 그 말이 무엇을 의미하는지 조금은 알게된 것 같습니다.

profile
프론트엔드에 스며드는 중 🌊

0개의 댓글