kimjeongwonnabout

직관 주도 개발에서 벗어나기

Q: 혹시 지금 사용하시는 직관이 어떤 원리로 동작하는지 아시나요?

공부하는 포스팅입니다. 제가 잘못 이해하고 있는 내용에 대한 지적이나 보충해주는 피드백은 완전 환영입니다.

개발이 좋았던 건 재미있기도 했지만 직관을 잘 활용할 수 있는 분야이기도 했기 때문이었다고 생각한다. 직관을 잘 활용하면 문제의 원인을 빠르게 파악하고 해결책을 잘 찾을 수 있다. 결과의 도출 과정이 크게 중요하지 않은 영역에서는 이런 직관을 그대로 활용하는 것이 큰 도움이 된다. 나는 남들보다는 조금 더 좋은 직관을 가지고 있었다고 생각한다. 그래서 개발을 하면서 큰 고민없이 적당히 좋은 설계를 짤 수 있고 적당히 괜찮은 구현을 만들 수 있었다. 그러나 더 좋은 설계더 좋은 구현을 도출하기에는 한계가 있었다.

한계를 느낀 큰 계기중 하나는, ‘좋은 코드란 무엇인가?’ 혹은 ‘좋은 리액트 컴포넌트란 무엇인가?’ 라는 질문에 대해서 내가 자신있게 답변하고 설명할 수 있을까 라는 고민이 생겼기 때문이다. 평소에 코드를 작성하며 개발을 할 때는 당연히 좋은 코드를 짜려고 한다. 그럼 내가 짠 코드가 왜 좋은건지 설명할 수 있었을까? 의존성을 줄이고 변경에 유연하게 만드는 것이 중요하다는 건 알고있지만 그것들을 어떻게 코드에 반영했는지 설명하지 못한다면 발전에는 한계가 있을 것이다.

기술 면접에서 많이 물어보는 것들 중 하나로 라이브러리가 어떤 원리로 동작하는지 아냐고 묻는 질문이다. 라이브러리의 동작 원리를 파악하고 있어야 더 적절하고 좋은 활용방법에 대해서도 고민해 볼 수 있으며, 디버깅을 할 때도 빠르게 원인과 해결방법을 도출할 수 있다. 같은 맥락에서 직관은 나에게 어떤 편리한 라이브러리 같은 것이 아니었나 싶다. 라이브러리를 이해하기 위해서는 라이브러리 소스코드를 뜯어보고 직접 구현해보기도 한다. 나도 이번 포스팅을 통해서 나의 직관이 어떻게 동작하는지 간단히 살펴보려 한다.

개발에서 사용하는 직관

계산기 어플리케이션을 만들어보자. 직관에 처음 전달하는 명령은 계산기 어플리케이션을 만드려고 하는데 좋은 설계를 짜줘, 그리고 그 설계에 맞는 코드를 작성해줘 이다. 그리고 요구사항을 현실세계의 레퍼런스를 참고하여 소프트웨어 관점으로 추상화하여 구현하는 일들을 하게 될 것이다. 계산기는 하드웨어로 구현된 것이 일반적이기 때문에 그것을 참고하여 생각해보자. 아래는 계산기 회로도이다.

현실세계의 계산기 회로도

출처: https://www.alibaba.com/product-detail/Custom-Tn-Lcd-Display-Screen-10_1600091582653.html

회로도를 보면 크게 4가지로 분리되어 있다. 전원과 디스플레이, 키패드 그리고 컨트롤러이다. 사실 굳이 회로도를 찾아보지 않더라도 어느정도 예상할 수 있는 내용이지만 확실하게 확인 하는 게 좋으니 찾아봤다. 소프트웨어로 만들기 위해서는 크게 세 개의 모듈로 추상화 할 수 있을 것이다. 화면을 보여주는 Display와 숫자와 명령을 입력받을 수 있는 Keypad, 그리고 각 모듈과 통신하여 계산하고 결과를 전달하는 Controller가 필요할 것이다. 이런 사고는 앞서 말했듯 현실세계의 계산기를 생각하면 금방 생각해낼 수 있는 것이다. 1차적으로 정리하면 아래와 같다.


Display 컴포넌트

인터페이스를 통해서 전달받은 값을 화면에 보여준다.

Keypad 컴포넌트

사용자로 부터 명령을 입력받는 버튼을 나열하고 컨트롤러에 전달한다.

Controller 컴포넌트

Keypad로부터 입력받은 값을 통해 계산해서 Display로 보여준다.


위 요구사항을 기반으로 각 역할의 관계를 아주 단순하게 도표로 정리해보자면 아래와 같은 모습을 생각할 수 있다. (참고로 화살표는 의존성이 아니라 데이터의 흐름을 나타내는 것이다.)

추상화된 전체적인 계산기 동작 도표

사용자로부터 Keypad를 통해 입력받은 데이터를 기반으로 계산한 데이터를 Display로 사용자에게 보여주는 아주 단순한 구조이다. 현실세계의 계산기를 생각해보면 키패드가 고장나거나 기능을 바꿔야 한다면 키패드를 분리해서 손봐야 할 것이다. 같은 맥락에서 소프트웨어 설계의 관점으로 보면 당연히 DisplayKeypad는 단일 책임을 가져야 할 것이며 각 컴포넌트는 계산 로직과는 느슨하게 결합되어야 할 것이라는 판단을 할 수 있다.

Display

계산기의 디스플레이 부품 이미지

출처: https://www.alibaba.com/product-detail/Custom-Tn-Lcd-Display-Screen-10_1600091582653.html

이미지는 우리가 알고있는 평범한 계산기에 주로 사용되는 7-Segment Display 모듈이다. 이 모듈이 하는 역할은, 인터페이스를 통해 입력받은 데이터를 화면에 보여주는 역할을 하게 된다. Display자체는 입력되는 값을 계산하거나, 판단하지 않고 그냥 사용자에게 보여주는 것을 해야한다.

찾아보니 현실세계에서 7-Segment Display를 구동시키기 위해서는 0-9의 숫자 데이터를 7개의 세그먼트의 필요한 부분에 전류를 흘려보내 동작시키는 8051이라 불리는 인터페이스가 사용되는 것 같다.

8051 인터페이스 이미지

출처: https://medium.com/@samuelchi2000/interfacing-7-segment-display-to-8051-b7be77a892cc

물론 우리가 목표로 하는 요구사항(전달받은 데이터를 화면에 보여준다.)을 달성하기 위해 이런 것들을 모두 구현하지 않아도 된다. 실제로는 개발 언어로 추상화된 규칙 안에서 잘 구현하면 되는 것이다. 현실세계와 마찬가지로 각 모듈이 통신할 수 있는 인터페이스가 필요하다. 전자 부품을 뜯어본 경험이 있다면 모듈간의 결합이 납땜이 아니라 단자를 통해 결합되는 것을 봤을 것이다. 단자는 모듈간의 통신을 담당해주는 인터페이스로 하드웨어에서 동작한다. 마찬가지로 소프트웨어에서도 다른 컴포넌트간의 의사소통을 인터페이스에 의존하게 만들어 의존성을 역전시킬 수 있다. 계산기에 이를 적용하게 되면 Controller의 출력과 Display는 서로를 몰라도 되기 때문에 느슨한 결합을 이룰 수 있게된다. 그렇다면 도표를 아래와 같이 수정해야 할 것 같다.

인터페이스를 통해 Controller의 결과를 Display로 전달하도록 변경된 도표

리액트 컴포넌트로 나타낸다면 아래와 같은 모습으로 구현될 수 있을 것이다.

interface DisplayProps {
  displayDigit: number;
  operator: "+" | "-" | "*" | "/";
}

const Display = ({ displayDigit, operator }: DisplayProps) => {
  return (
    <output>
      {displayDigit}
      <span>{operator}</span>
    </output>
  );
};

한눈에 봐도 큰 문제가 없는 구조를 가지고 있는 컴포넌트다. 위에서 약속된 인터페이스, 느슨한 결합등의 키워드를 언급했지만 구현을 봤을때는 깊게 생각하지 않아도 짤 수 있는 코드가 나왔다. 그런데 여기서 생각해볼 것은 ‘왜 이 컴포넌트가 문제가 없는가?’ 이다. 컴포넌트는 외부로부터 들어온 데이터만을 순수 컴포넌트 형태로 렌더링한다. 입력받은 데이터를 보여주는 역할만 하도록 작성되어 단일 책임 원칙(SRP)을 지키고 있다. 데이터를 받아올 때는 Props 인터페이스에 의존한 데이터 형태를 활용한다. 사실 리액트에서 Props의 타입을 정의하고 부모-자식 컴포넌트간의 소통을 구현이 아닌 추상화된 인터페이스에 의존시키는 것으로 부분적으로 의존성 역전 원칙(DIP)을 준수한다고 볼 수도 있다.

그럼 모든 Props를 통해 통신하는 리액트 컴포넌트가 DIP를 준수하는 것일까? 그런데 우리들이 만든 컴포넌트는 왜 그 모양으로 의존성 관리가 안되는걸까? Props는 의존성 역전을 구현하기 좋은 수단일 뿐이다. 나쁜 예를 한 번 살펴보자.

import { calculateResult } from "./CalculatorService";

interface DisplayProps {
  displayDigitItemName: number;
  operatorItemName: "+" | "-" | "*" | "/";
}

const Display = ({
  displayDigitItemName,
  operatorItemName,
}: DisplayProps) => {
  const displayNumber =
    window.localStorage.getItem(displayDigitItemName) || "0";
  const operator =
    window.localStorage.getItem(operatorItemName) || "+";

  const result = calculateResult(displayNumber, operator);

  return <output>{result}</output>;
};

세상에 이런 코드를 짜는 사람이 어디 있겠나 싶겠지만, 이정도는 아니더라도 사실 알게 모르게 이런식으로 컴포넌트 내에서 스토어에 의존하고 외부 함수에 의존하는 구현을 할 때가 많다. Props로 가져오는 값 또한 컴포넌트 내부 구현에 강하게 의존되어 있다. 의존성 역전을 위해 사용되는 것이 아니라 내부 로직을 위한 일방향 데이터를 전달받는 용도로 사용되고 있다. 그리고 컴포넌트 내부 로직또한 localStorage의 구현에 완전히 의존하고 있으며 화면을 그리기 위한 렌더링 로직까지 외부의 함수에 의존한다. SRP와 DIP를 한 방에 박살내는 코드가 된 것이다. 그런데 우리의 기특한 직관은 소프트웨어적인 사고에 익숙해져 있기 때문에 여기까지 가기 전에 적당히 좋은 결과를 알아서 찾아내는 것이다.

그렇다면 최초의 구현에서 조금 더 개선할 수 있다면 어떤식으로 가능할까? 개방 폐쇄 원칙(OCP)을 고려해서 나중에 operator의 요구사항이 변경되거나 다른 metadata를 표현해야 하는 상황에 대응하도록 코드를 변경할 수 있다.

interface DisplayProps {
  displayDigit: number;
  rightElement: ReactNode;
}

const Display = ({
  displayNumber,
  rightElement,
}: DisplayProps) => {
  return (
    <output>
      {displayNumber}
      {rightElement}
    </output>
  );
};

첫 번째 코드에서 operator로 지정했던 props를 rightElement라는 이름으로 변경하고 어떤 렌더링 가능한 요소든 받을 수 있도록 했다. 현실세계의 디스플레이 모듈은 모듈 자체의 물리적인 특성상으로 제한된 상태를 표현할 수 밖에 없겠지만 소프트웨어 관점에서는 가능하다. 이렇게 하면 화면에 데이터를 표현한다는 단일 책임은 유지하면서 만약에 요구사항이 연산자가 아니라 다른 상태를 표현해야 하는 상황이 오더라도 변경가능성 있는 구현 자체를 추상화를 한 단계 끌어올려 의존시키는 방법으로 컴포넌트 내부 코드의 수정 없이 확장가능해진다.

Display 컴포넌트는 일단 이 정도에서 마무리 할 수 있지 않을까 싶다.

Keypad

Keypad의 역할은 사용자로 부터 명령을 입력받는 버튼을 나열하고 입력받은 데이터를 컨트롤러에 전달하는 것이다. 여기서 Keypad의 역할이 사실은 두 가지라는 것을 알 수 있다. 하나는 사용자로 부터 명령을 입력받는 버튼을 나열하는 것이고 하나는 버튼을 통해서 입력받은 데이터를 컨트롤러에 전달하는 것이다. 데이터를 컨트롤러에 전달할 때는 역시 인터페이스를 통해서 느슨하게 결합시켜야 한다. 그렇기 때문에 아래와 같이 Keypad의 역할을 분리해서 도표를 수정해야 한다.

Keypad에서 Button을 분리한 도표

사용자는 입력을 Keypad에 직접 하는 것이 아니라 Keypad의 UI를 담당하는 Buttons에 전달하게 된다. Keypad는 Button을 통해 들어오는 입력들에 대한 액션을 매핑하고 Controller와 인터페이스를 통해 통신한다. 직관에 의존해 바로 코드를 작성해서 아래와 같은 리액트 컴포넌트를 만들었다.


Buttons.tsx

export type ButtonsType<T> = { label: string; value: T };

interface ButtonsProps<T> {
  buttons: ButtonsType<T>[];
  onButtonPress: (value: any) => void;
}

export const Buttons = <T,>({
  buttons,
  onButtonPress,
}: ButtonsProps<T>) => {
  return (
    <div>
      {buttons.map(({ label, value }) => (
        <Button onClick={() => onButtonPress(value)}>
          {label}
        </Button>
      ))}
    </div>
  );
};

Buttons 컴포넌트는 화면에 버튼을 나열하고 각 버튼을 클릭할 수 있도록 하는 역할을 한다. 버튼의 나열과 이벤트의 전달이 목적이기 때문에 Buttons컴포넌트는 어떤 버튼이 있어야 하는지, 각 버튼이 눌렸을 때 어떤 동작을 해야하는지는 몰라도 된다. '버튼을 화면에 누르고 인터렉션을 받을 수 있다’가 책임과 한계인 것이다.

버튼의 요구사항이 변경되어야 할 때 props로 전달하는 객체만 변경하면 UI를 수정할 수 있게 만들어 OCP를 준수할 수 있다. 구현이 아니라 추상화에 의존하기 때문에 가능한 설계이다. 조금 더 개선해본다면 buttons를 2차원 배열로 받아서 행과 열까지 결정해 줄 수 있을 것이다.

onButtonPress 콜백을 따로 전달받아 버튼을 클릭했을 때 식별할 수 있는 값을 전달해 버튼별로 어떤 동작을 할지를 내부 구현이 아니라 인터페이스에 의존시켜 목적을 추상화했다. 이렇게 해서 로직과 분리시켜 SRP를 확실하게 해준다. SRP를 준수했다는 건 이 Buttons컴포넌트가 꼭 계산기가 아니더라도 각종 UI에서 버튼을 나열하고 특정 버튼에 이벤트를 설정할 수 있게 동작할 수 있다는 의미가 된다. 처음 요구사항에 맞는 컴포넌트가 만들어 진 듯 하다.


Keypad.tsx

type OperatorType =
  | "PLUS"
  | "MINUS"
  | "MULTIPLY"
  | "DIVIDE"
  | "RESULT";

const BUTTONS: ButtonsType<OperatorType | number>[] = [
  { label: "0", value: 0 },
  { label: "1", value: 1 },
  { label: "2", value: 2 },
  // ...
  { label: "*", value: "MULTIPLY" },
  { label: "/", value: "DIVIDE" },
  { label: "=", value: "RESULT" },
];

interface KeypadProps {
  onClickDigit: (integer: number) => void;
  onClickOperator: (operate: OperatorType) => void;
}

const ButtonKeypad = ({
  onClickDigit,
  onClickOperator,
}: KeypadProps) => {
  const handleInputValue = (value: OperatorType | number) => {
    if (typeof value === "number") {
      onClickDigit(value);
    }
    if (typeof value === "string") {
      onClickOperator(value);
    }
  };
  return (
    <div>
      <Buttons
        buttons={BUTTONS}
        onButtonPress={handleInputValue}
      />
    </div>
  );
};

Buttons컴포넌트를 합성해서 만든 ButtonKeypad컴포넌트이다. 그냥 Keypad가 이나라 Button을 활용해 만든 키패드라는 맥락을 이름에 같이 전달해줘서 Button UI를 통해 구현한 Keypad라는 의미를 전달할 수 있다. 변수나 컴포넌트의 이름은 데이터나 로직의 책임과 한계에 대한 지향을 결정하기 때문에 정말 중요하다.

Buttons 컴포넌트를 합성해서 감싸는 방식으로 ControllerKeypadProps라는 인터페이스를 통해서 통신할 수 있도록 하는 역할을 해주며 동작을 정의한다. 인터페이스로 어떤 버튼을 눌렀는지 구분하는 로직을 포함하고 있다. ButtonKeypad 컴포넌트의 역할은 Buttons에서 입력받은 값을 판단해서 Controller에 전달하는 것이다. 이 때 버튼은 입력하는 값들을 어떻게 처리하는지 모르기 때문에 이것을 판단해서 Controller로 부터 받은 콜백을 호출해서 Controller와 통신하는 방식으로 처리했다.

이렇게 만들게 되면 숫자와 연산자로 이루어진 버튼을 구별해서 컨트롤러에 전달하기 때문에 ButtonKeypad컴포넌트는 계산기라는 도메인에 의존하는 컴포넌트가 된다. 또한 리액트 컴포넌트는 높은 자유도 덕분에 UI요소에 대한 렌더링과 함께 로직도 구현할 수 있고, ButtonKeypad컴포넌트는 UI와 로직이 강결합되어 있다. 하지만 소프트웨어에서 목적이 있다면 의존성을 피할 수는 없다. 때문에 이런식으로 하나의 컴포넌트에 의존성에 대한 책임을 주고 다른 컴포넌트를 의존성에 대해서 무책임하게(?) 만들 수 있기도 하다. 적당한 추상화 레벨을 결정하고 구현하는 것도 개발자의 중요한 역량중에 하나이다. 리액트 컴포넌트를 추상 클래스처럼 추상화와 구현을 분리할 수 있다면 관리가 좀 더 좋았을 것이다.

Controller

Controller의 요구사항은 Keypad로부터 입력받은 값을 통해 계산해서 Display로 보여준다.이다. 즉 입력을 받아서 판단한 뒤 계산하고 어떻게 보여줘야 할 지 가공하는 역할을 한다. 입력받은 요청들(수를 입력하는지, 연산을 하는지, 결과를 출력하는지 등등…)을 통해서 내부적으로 어떤 동작을 할 지 판단해서 상태를 바꾸거나 계산을 해주고 그에 따라 Display에 보여줘야 하는 내용을 결정할 수도 있어야 한다. 그러면 Controller의 세부 요구사항을 도표에 반영하면 아래와 같은 것이다.

Controller의 parse, compute, render 과정을 추가한 도표

Controller는 입력받은 데이터를 잘 판단해서 내부 상태 데이터를 다루고 계산한 뒤 화면에 보여줄 수 있는 데이터로 Display에게 보내주면 된다. 캡슐화를 통해서 값을 다루면 좋을 것 같기 때문에 hook으로 로직을 구성해보면 아래와 같다.

controller.ts

구체적인 구현은 생략하고 구조만 작성

export const useController = () => {
  const currentStatus = useState<"STATIC" | "OPERATE">("STATIC");
  const currentOperator = useState<OperatorType | null>(null);
  const accumulatedValue = useState<number>(0);
  const inputValue = useState<number>(0);

  const pushDigit = (digit: number) => {
    // 입력받은 숫자를 누적시켜서 숫자를 저장함
  };

  const compute = () => {
    // 결과 상태로 만들고 현재 연산자와 누산할 값을 참조하여 결과를 저장
  };

  const changeOperator = (operator: OperatorType | null) => {
    // 계산할 연산자를 지정
  };

  const renderDisplay = () => {
    // 현재 상태에 따라 화면에 보여줄 데이터를 계산해서 보여준다.
  };

  export { pushDigit, changeOperator, compute, renderDisplay };
};

세부 구현은 생략하고 대략 이런식으로 hook의 구조를 만들었다. pushDigitchangeOperator메서드를 통해서 입력받은 값들을 계산가능한 형태와 상태로 만들고, compute함수를 통해 실제 계산을 하고 내부 상태를 갱신한다. 그리고 renderDisplay함수로 현재 Display에 보여줘야하는 값을 결정해준다. 만약 숫자를 입력하는 OPERATE 상태라면 입력중인 숫자를 보여줘야 할텐고 계산된 결과를 보여주는 STATIC 상태라면 계산된 값만 보여줄 것이다. 그리고 해당 hook과 DisplayKeypad가 통신할 수 있도록 하는 컴포넌트를 만들면 아래와 같다

Calculator.tsx

const Calculator = () => {
  const;
  const { pushDigit, changeOperator, compute, renderDisplay } =
    useController();

  const { digit, operator } = renderDisplay();

  return (
    <div>
      <Display
        displayDigit={digit}
        rightElement={<span>{operator}</span>}
      />
      <ButtonKeypad
        onClickDigit={pushDigit}
        onClickOperator={(operator) => {
          changeOperator(operator);
          compute();
        }}
      />
    </div>
  );
};

결과적으로는 ButtonKeypad에서는 콜백을 통해서 입력값을 useController로 전달해주고 계산 된 값들을 Display컴포넌트로 전달해주는 방식으로 각각의 컴포넌트의 관심사를 분리하고 컴포넌트와 로직 사이의 소통을 인터페이스에 의존시키면서 느슨하게 결합할 수 있게되었다. 각각의 컴포넌트는 책임과 한계가 명확하기 때문에 요구사항이 변경될 때 각 컴포넌트만 수정해도 다른 컴포넌트와 계산로직에는 영향을 주지 않기 때문에 변경에도 유연하다고 볼 수 있다. 예를 들어 입력은 Button UI가 아니라 키보드를 통해서 받아야 하는 상황이 온다고 하더라도 다시 KeyInputKeypad컴포넌트를 만들어서 바꿔주기만 하면 된다.

이렇게 계산기 어플리케이션을 평소대로 만들어봤고, 그 과정에서 활용된 직관이 어떤 사고과정을 거쳤는지 다시 확인해봤다. 이런 과정을 계속 의식하지 못한다면 분명 간과하는 부분들이 생길 것이고 점점 더 설명 불가능한 영역들이 많아질 것이다. 개발 속도야 빨라지겠지만 분명 좋은 코드로부터는 멀어지는 길이라고 생각한다.

후기

전에 포스팅에서도 언급한 적이 있었지만 개발 이론서들을 좀 멀리하는 경향이 있었는데 그 이유는 '저걸 굳이 다 읽어가면서 알지 않아도 되는 내용’같았기 때문이다. 컴포넌트는 당연히 책임과 한계를 주어서 변경에 유연하게 만들어야 하고, 횡단 관심사의 경우는 Render Props같은 기법을 통해서 의존성을 끌어올려 관리해야 하며 컴포넌트 합성과 같은 기법을 통해서 로직과 UI의 의존성을 강결합하지 않아야 한다는 것들을 굳이 'SOLID 원칙’이나 결국에는 다 의존성을 관리하는 방법들에 대한 내용인 '디자인 패턴’등을… 굳이 알아야 하는지 의문이 있었기 때문이다. 그러니까 그냥 '직관적으로 알 수 있는 것들 아니야?'라는 착각을 꽤 오래 하고있었던 셈이다.

그래도 모르는 것 보다는 아는 게 낫다고 생각해서 올 해 들어서는 이런 설계 원칙이나 디자인 패턴들에 대해서 개인적으로 많이 알아보고 이해하려고 노력했다. 덕분에 이 글도 완성할 수 있었을 것이고… 아무튼 앞서 했던 오만방자했던 생각들을 많이 고칠 수 있었다. 얼핏 아는 것과 아는 것은 천지 차이다… 뭐든 아는만큼 보이는 법이라는 것도 느꼈다.

사실 이제는 어떤 '구현’에 있어서는 자신감이 많이 생겼다. '돌아가게 짜는 것’은 어렵지 않게 수행할 수 있는데 (이 오만함도 곧 박살나는 날이 오겠지만…) 그 다음 단계가 좋은 코드를 짜는 것이라고 생각한다. 좋은 코드란 무엇일까… 많은 개발자들이 끊임없이 고민하는 질문이고 나는 이제 그 영역에 발을 제대로 디뎌야 하는 책임이 생긴 연차가 된 것이다. 나름 생각해본 '좋은 코드’를 작성하기 위한 방법으로는, '소프트웨어 다운 코드’를 지향하는게 근사(近似)한 방법이 아닐까 싶다. 소프트웨어 다움을 극대화 하기 위해서는 말 그대로 변경이 쉬워야 한다. 그것이 소프트웨어니까…[고이즈미 짤] 그리고 변경이 쉬운 코드를 만드는 방법으로는 다양한 개발서에서 언급하는 '의존성’과 '복잡도’를 줄이는 것이다. 기존의 원칙과 방법들은 객체지향을 중심으로 설명하는 경우가 많았기 때문에 프론트엔드에서는 이런 것들을 어떻게 소화할 수 있을지 대한 의문도 있었는데, 피상적으로 그런 원칙과 방법들을 이해할려고 하는 것을 넘어서 이런 원칙과 방법들이 어떤 문제들을 어떻게 해결하려고 하는지를 꿰뚫어볼 수 있다면 근본적인 이해가 될 것이고 그 이해를 바탕으로 더 의식적으로 좋은 코드를 작성해보면 분명 좋은 답을 찾을 수 있을거라고 생각한다.

그 첫 단계로 내가 코드를 짜거나 설계를 할 때에 무의식적으로 동작하는 멘탈모델을 파악하고 그걸 토대로 더 개선하고 나아질 수 있게 하는 것이다. 그 과정을 이 포스팅에 한 달넘게 조금씩 작성하면서 담아봤다. 생각보다 긴 글이 되었는데 확실히 글을 좀 자주 써야 할 것 같다고 다시 느꼈다…