[데브코스] 9.18 - 10.18 한달 회고

jaemin·2023년 10월 23일
0

데브코스

목록 보기
1/1
post-thumbnail

데브코스를 시작한지도 어느덧 한 달이 지났습니다. 데브코스 시작 전, 감사하게도 면접 기회를 얻게 되었고 9월 18일에 면접을 보게 되었습니다. 결국 면접에 떨어져 데브코스를 오게 되었는데, 많이 아쉬우면서도 지금 상태로 운 좋게 붙었다 하더라도 회사에 1인분으로, 역할을 잘 해낼지에 대한 의문과 걱정도 많았으리라 생각합니다.
데브코스 과정에서 다음과 같은 내용을 배웠고 그에 대한 느낀점입니다.

선언적 프로그래밍

데브 코스 강의 중 선언적 프로그래밍에 대한 주제가 나오게 되었습니다. 그동안 선언적 프로그래밍에 대해 제대로 이해하지 못해서 이번 기회에 알아보는 시간을 가졌습니다. 토스의 박서진님이 작성한 선언적인 코드 작성하기를 읽고 이해한 선언적 프로그래밍은 다음과 같습니다.

결과 혹은 원하는 것에 대해 설명하지만 그 모든 과정을 코드를 해석하는 과정에서 알 필요는 없습니다. 그러므로 의미를 알기 어려운 로직을 그대로 드러내지 않고 의미를 가지도록 추상화 하는 것이 선언적 프로그래밍이라고 이해했습니다. 따라서 서진님께서는 "선언적인 코드"를 추상화 레벨이 높아진 코드이다 라고 표현한 것 같습니다.
다음은 이에 대한 이해를 돕기 위한 코드입니다.

<script>
const state = [1, 2, 3, 4, 5];
const $list = document.querySelector(".list");

for (let index = 0; index < state.length; index += 1) {
	const $listItem = document.createElement("li");
	
	$listItem.innnerHTMl(state[index]);
	$list.appendChild($listItem);
}
</script>

위 코드는 해석하는 과정에서 필요하지 않는 과정이 그대로 노출되어 있습니다. state를 순회하는 for문이나 돔을 생성하고 삽입하는 과정은 한 번에 의도를 파악하기 어려워 보입니다. 이러한 과정을 추상화를 통해 과정을 적절히 숨기고 의미를 부여한다면 가독성과 재사용성이 올라갈 것 같습니다.

function List() {
	const [state, setState] = useState([1, 2, 3, 4, 5]);
  
  	return (
      <ul>
      	${state.map((content) => (
        	<li>{content}</li>
        ))}
      </ul>
    );
}

위 코드를 보면 for문은 map을 사용함으로써 불필요한 과정이 숨겨진 것을 볼 수 있습니다. 또, JSX로 인해 돔이 어떤 구조로 생성되는지 한 눈에 파악할 수 있게 되었습니다. 코드를 이해해야 하는 입장에서 몰라도 되는 로직은 감춰지고 필요한 정보만 드러나 있어 빨리 파악할 수 있어졌습니다.

그러나, 무조건 추상화 하는 것이 좋은 방향은 아닙니다. 에어비앤비 클론 코딩을 진행했을때, 프로젝트 전반에서 사용되는 모든 버튼 컴포넌트를 하나의 컴포넌트로 추상화하여 사용하고자 했습니다. 여러 컴포넌트에서 사용하는 컴포넌트들을 각각 구현하는 것보다 하나의 컴포넌트를 사용하는 편이 atmoic한 컴포넌트이고 사용성이 좋을 것이라고 생각했습니다. 막상 common한 버튼 컴포넌트를 구현하고 보니, 버튼 크기, 색깔, 내용, 애니메이션 등 생각보다 너무 많은 정보들을 props로 전달해야 했습니다. 가독성과 사용성을 위해 추상화한 컴포넌트인데 오히려 가독성도, 사용성도 살리지 못했습니다. 아래 코드와 같이 사용해야 했습니다.

import { Button } from "./common";

function Home() {
	return (
    	<main>
 	  		<Button
            	width={200}
              	height={80}
                content={"바로가기"}
              	color={primary}
              	onClick={handleClickButton}
				radius={0.5}
              	hoverColor={secondary}
                // 등등 너무 많았다...
            />
		</main>
    );
}

결국 추상화한 common Button은 폐기하게 되었습니다. 좋은 선언적 코드란 결국, 유지 보수하기 좋도록 추상화한 코드라는 것을 알 수 있습니다. 무엇을 고려하든 항상 읽기 좋고 이해하기 좋은가, 유지 보수 하기 좋은가가 우선이 돼야 한다는 것을 다시 한 번 느꼈습니다.
제가 만든 common Button의 패착은 너무 큰 단위의 컴포넌트를 추상화했기 때문이라고 생각합니다. 작은 단위의, 적은 일을 하는 컴포넌트 또는 함수를 추상화했을때 선언적 프로그래밍의 장점을 잘 활용할 수 있다고 생각합니다.
좋은 예시는 토스의 슬래시 라이브러리에서 많이 배웠습니다. 아래의 ImpressionArea 라는 컴포넌트는 작은 단위의 컴포넌트를 추상화하였습니다.

<ImpressionArea onImpressionStart={() => { /* 보여지면 실행 */ }}>
  <div>내가 보여지면 `onImpressionStart`가 호출돼요</div>
</ImpressionArea>

ImpressionArea로 인해 해당 컴포넌트는 IntersectionObserverApi나 스크롤 핸들러 로직을 직접적으로 사용하지 않을 수 있습니다. 또한, 추상화한 컴포넌트는 어떤 영역이 보이는지 또는 숨겨졌는지에 따라 함수를 호출하는 간단한 일만 하고 있기 때문에 컴포넌트를 사용할때도 유지보수할때도 큰 불편함이 없어보입니다.

결론적으로 좋은 선언적인 코드란 작은 단위의 로직이나 컴포넌트를 유지 보수하기 좋게 추상화한 코드라고 생각했습니다.

코드를 언제 분리할 것인가

코드를 언제 분리할 것인가는 개발을 시작하면서 늘 하게 되는 고민입니다. 바닐라 자바스크립트로 todo App과 노션을 개발하면서 이에 대한 고민을 또 다시 하게 되었습니다.

먼저 todo App을 바닐라 자바스크립트로 구현했을 때 다음과 같은 상황이 있었습니다.

function App() {
	const [todos, setTodos] = useState<Todo[]>([]);

  	const todoFormComponent = createComponent(TodoForm, { setTodos });
    const todoListComponent = createComponent(TodoList, { todos, setTodos});

  	return {
    	element: `
			<div>
				${todoFormComponent}
				${todoListComponent}
			</div>
		`,
    }
}

App에서 todos에 대한 상태를 만들고 todos 상태를 변경할 수 있는 setter를 하위 컴포넌트의 props로 전달해 todos를 변경할 수 있게 했습니다. props로 setter만 전달하여 상태를 변경하면 코드가 더 간결해지고 편할거라 생각했으나 여기엔 여러 가지 문제가 있었습니다.
제가 간과한 것은 컴포넌트 책임입니다. 현재 todos 상태를 책임지고 있는 컴포넌트는 App입니다. 그렇기 때문에 App은 이 todos가 변경되고 사용되는 부분 또한 알고 있고 관리할 수 있어야 합니다. 만약 setTodos를 컴포넌트 props로 넘긴다면 이 todos가 하위 컴포넌트 내에서 어떻게 변경되는지 App은 알 수 없게 됩니다.
이러한 문제를 깨닫고 아래와 같이 사용하게 되었습니다.

function App() {
	const [todos, setTodos] = useState<Todo[]>([]);
  
  	const getTodos = () => { /* 여기서 setTodos 호출*/ };
    const addTodo = () => { /* 여기서 setTodos 호출*/ };
  	const deleteTodo = () => { /* 여기서 setTodos 호출*/ };

  	const todoFormComponent = createComponent(TodoForm, { addTodo });
    const todoListComponent = createComponent(TodoList, { todos, deleteTodo });

  	return {
    	element: `
			<div>
				${todoFormComponent}
				${todoListComponent}
			</div>
		`,
    }
}

여기서 저는 앞에서 나온 컴포넌트 책임에 대해 잘못 이해하고 todos 관련 로직을 분리해내는 것이 컴포넌트의 책임을 어겼다고 생각했습니다.
그러나, 이렇게 변경하면 또 아쉬운 점이 있습니다. 사실 App은 웹 애플리케이션의 엔트리 포인트 같은 역할을 하는 컴포넌트로 다른 컴포넌트를 렌더링하는 역할을 주로 하고 있습니다. 그런데, 지금은 todos라는 상태 또한 관리하고 있습니다. 오히려 todos 관련 로직을 따로 분리하게 되면 각자의 책임이 분리되어 App은 엔트리 포인트로서 단일 책임을 갖게 되고 todos 관련 로직은 useTodos 라는 훅으로 분리한다면 이 훅은 todos 책임만을 갖게 됩니다.
이렇게 어떤 코드를 분리할 것인가에 대한 고민을 하다 보니 컴포넌트의 책임과 선언적 프로그래밍에 대한 고민도 함께 하며 많은 것을 깨닫게 되었습니다.

이러한 고민을 하며 "코드를 언제 분리할 것인가"에 대한 저만의 기준이 필요하다고 느껴 정리를 해보았습니다. 지금 제가 깨달은 바를 통해 정리를 하자면 다음과 같은 상황에서 분리할 것 같습니다.

코드를 언제 분리할 것인가
1. 반복되는, 의미를 가진 코드
2. 반복되지 않더라도 그 자체로 의미를 가지고 있는 로직
3. 추상화가 필요한 경우

지금까지 정말 많은 버전의 Todo를 만들었지만 구조와 책임에 대해 고민하며 만들었던 적은 없었던 것 같습니다. 어떤 것을 만드느냐는 중요하지 않고 어떤 고민을 하며 만드느냐가 중요하다는 것을 깨닫게 되는 시간이었습니다. 기능과 UI 구현보다는 보다 근본적인 것들에 대해 고민하는 시간을 데브코스에서 보내고 싶습니다.

profile
프론트엔드 개발자가 되기 위해 공부 중입니다.

0개의 댓글