[항해 플러스 프론트엔드 4기] 4주차 과제 회고

원정·2025년 1월 10일
2
post-thumbnail

어느덧 4주차가 됐다.
4주차부터는 Chapter1 <프레임워크 없이 SPA 만들기>가 종료되고, Chapter2 <클린 코드>가 시작됐다.

Chapter2-1의 주제는 클린코드와 리팩토링이다.
목표는 냄새나는 코드를 리팩토링을 통해 클린하게 바꾸는 것이다.

정답이 없는 문제이기도 하고, 팀원분들과 컨벤션을 맞추는 과정을 넣어주신 거로 미뤄보면, 다양한 의견을 듣고, 논의하고, 수렴하고, 적용하고, 수정하는 과정을 겪는 것이 주요 목표 아닐까?

지난 과제에서 배운 내용 가운데 하나는 '최적화할 상황을 만드지 않는 것이 최적화다!'이다.
섣부른 최적화는 오히려 독이고, 컴포넌트를 잘 분리하면 최적화 메서드를 사용하지 않아도 된다.
물론 볼륨이 커지게 되면 언젠가는 쓰겠지만 그 언젠가는 생각보다 멀리있다고 한다.

아무튼, 최적화를 위해서 먼저 할 일은 클린 코드라는 점이다.
클린 코드는 함수형 프로그래밍과 맞닿아있다.
보통 테스트가 용이한 코드는 클린 코드(테스트 코드를 안 짜봐서 모르겠지만ㅎㅎ)고, 함수형 프로그래밍은 테스트가 용이한 코드를 많이 만들어낸다.

얘기하고 싶은 건 이번 과제에서 함수형 프로그래밍에 관심을 갖게 됐다.
지난번에 추천 받은 책, <쏙쏙 들어오는 함수형 프로그래밍>으로 스터디도 주최했다.
같은 팀원분들끼리 하루에 한 챕터씩 돌아가며 발표하기로 했다.
책 내용이 많아서 아마 클린 코드 챕터가 끝나고 나서 마칠 것 같다.
미숙하지만 책에서 공부한 내용과 여기저기서 주워들은 내용을 과제에 적용해볼 예정이다.

💰 팀 컨벤션 맞추기


과제 시작 전, 팀원분들과 코드 컨벤션을 정하기로 했다.
아래는 팀원분들과 결정한 코드 컨벤션이다.

  • 함수명, 변수명, 인자명
    • 함수명은 함수의 역할이 드러나게 상세히 작성한다.
      • 의미를 전달하며 줄여쓸 수 있다면 축소(예시: calculate -> cal)
    • 함수명은 동사로 시작한다.
    • 인수로 표현할 수 있는 값은 함수명에서 생략한다.
      • 예시: A에서 B로 길을 찾는 함수 -> findPath(A, B)
  • 타입 컨벤션
    • 인터페이스명, 함수 시그니처, 넓은 타입처리
    • 중복을 줄이기 위해 구체적으로 작성한다.
    • 2뎁스까지 표현 (예시: UserDetail)
    • 유틸리티 타입을 활용 (Pick, Omit)
  • 함수 선언 컨벤션
    • 기본 규칙은 함수 표현식 사용(유틸 함수 포함)
    • 내보내기 시 기본 규칙은 export한다.
    • 컴포넌트는 함수 선언문을 사용하고 default export한다.
    • 인수는 객체 형태로 사용
  • 파일/디렉토리 규칙
    • Barrel 파일을 사용
    • FSD 사용

💵 기본 과제 후 조정

기본 과제를 완료하고 팀 컨벤션에 각자가 느낀 불편한 점이 있어 조정했다.

  • 배럴 파일을 사용할 때 export default가 섞이면 default as ...으로 처리해줘야 하는 게 번거롭다! export로 통일하자!
  • 파일명 2Depth 지키기 어렵다!
  • 데이터는 복수, UI는 List, 타입은 단수로 작성하자!
    • 복수형 사용에 대해서 s가 잘 안 보일 수 있다는 의견이 있었는데, 타입스크립트를 쓴다는 가정이어서 타입과 함께 쓰면 실수할 일을 줄일 수 있다는 데에 대부분 동의하여 결정됐다.
  • 과제의 규모를 생각하면 FSD를 적용하기에는 과하다!
  • 축약어를 쓰다보니 헷갈리는 경우가 있다. 상세하게 작성하자!

여러 의견이 오갔고, 처음보다 더 상세한 부분까지 조정할 수 있었다.
팀원분 가운데 한 분이 노션에 깔끔하게 정리해주셨다!👍

아직 리액트와 타입스크립트 경험이 많지 않은 나에게 이런 대화는 생각할 수 있는 범위를 넓혀줘서 유익했다.
사용하면서 느낀 경험자의 의견과 이유를 들으면서, '실제로 사용하면 저런 점이 불편하겠구나?'라고 생각할 수 있었다.

💰 Pipeline 패턴


함수형 프로그래밍에 대해서 알아보다가 우연히 동영상(https://www.youtube.com/watch?v=OgBk44TRjLs)을 보게됐다.

영상의 주제는 합성 함수다.
간단하게 정리하면 a 함수의 결과값을 b 함수의 인자로 넘기고 결과값을 c 함수로 넘겨서 최종 결과값을 반환한다고 했을 때,

const result = c(b(a(input)));

위와 같이 중첩된 고차 함수로 사용할 수 있다.

하지만 중첩된 고차 함수의 단점은 합성하는 함수가 많아질 수록 가독성이 좋지 않고, 유지 보수나 재사용이 힘들다.

이를 해결하고자 나온 패턴이 ComposePipeline이다.

먼저 내가 사용한 Pipeline 패턴의 함수는 아래와 같다.

const pipe = (...functions) => (input) => functions.reduce((acc, fn) => fn(acc), input);

먼저 사용될 함수들을 왼쪽부터 나열하여 첫 번째로 넘겨주고 다음으로 첫 번째 함수에 전달할 input을 넘겨준다.

const result = pipe(a, b, c)(input);

앞선 예시 코드를 pipe 함수를 통해 바꾸면 위와 같다.

Compose 패턴은 Pipeline 패턴과 동일하게 동작하지만 먼저 사용될 함수를 오른쪽부터 나열하는 차이만 있다.
구현할 때 reduce 대신, reduceRight를 사용하면 된다.

과제 코드에서 사용한 예시를 살펴보자.

const pipe =
  (...functions) =>
  (input) =>
    functions.reduce((acc, fn) => fn(acc), input);

const querySelector = (selector) => {
  return document.querySelector(selector);
};

const renderContent = (content) => (element) => {
  element.innerHTML = content;
  return element;
};

export const renderToElement = (selector, content) => pipe(querySelector, renderContent(content))(selector);

DOM을 조작하는 로직을 DOM 선택, DOM 조작으로 나눴다.

renderToElement("#cart-total", CartTotalPrice(cartTotalPrice));

사용하는 쪽에서는 해당 DOM의 선택자와 넣어줄 내용을 인수로 넘겨주면 된다.
그러면 pipe 함수가 실행되면서 querySelector에 인자로 selector가 넘어간다.

다음으로 renderContent(content)가 실행된다.
contentquerySelector를 통과하지 않았음에도 사용할 수 있는 이유는 클로저 덕분이다.
renderContent(content)(element) => {...}을 반환하고 이 함수의 element 인자로 querySelector의 반환값이 들어가 실행된다.

최종적으로 querySelector에서 반환한 element의 요소로 content가 들어가고 element를 반환하며 함수가 종료된다.

이 부분만 보면 '사용하는 게 좋은 건가?'싶긴 하다.

하지만 다른 함수를 조합해서 비슷한 역할을 하는 다른 함수를 만들어 낼 수 있다.

const pipe =
  (...functions) =>
  (input) =>
    functions.reduce((acc, fn) => fn(acc), input);

const querySelector = (selector) => {
  return document.querySelector(selector);
};

const renderContent = (content) => (element) => {
  element.innerHTML = content;
  return element;
};

const appendContent = (content) => (element) => {
  element.innerHTML += content;
  return element;
};

const replaceContent = (content) => (element) => {
  const cloneElement = element.cloneNode();
  cloneElement.innerHTML = content;
  element.replaceWith(cloneElement);
  return element;
};

export const renderToElement = (selector, content) => pipe(querySelector, renderContent(content))(selector);

export const appendToElement = (selector, content) => pipe(querySelector, appendContent(content))(selector);

export const replaceToElement = (selector, content) => pipe(querySelector, replaceContent(content))(selector);

DOM 안에 요소를 새로 채울 경우, 추가할 경우, DOM 자체를 바꿔야 하는 경우 모두 중간 함수만 바꿔서 만들 수 있다.

사실 위 상황은 querySelector만 재사용된 거라 큰 의미는 없어보인다.

하지만 이렇게 잘게 쪼개진 함수가 많고 여러 함수를 합성해서 사용한다면 유용하게 쓰일 것 같다.

이를 보고 리액트에서 컴포넌트를 재사용하고 조립해서 사용하는 모양과 비슷하다고 생각했다.

const delay = (ms) => (callback) => setTimeout(callback, ms);

const repeat = (ms) => (callback) => () => setInterval(callback, ms);

export const scheduleInterval = (callback, intervalMs, delayMs) => pipe(repeat(intervalMs), delay(delayMs))(callback);

pipe 함수를 활용해서 이런 함수도 만들었다.

Pipeline 패턴이란 걸 처음 사용해봐서 이렇게 사용하는 게 알맞는 건지 모르겠다.
'중간 과정으로 쓰이는 게 아니라 최종적으로 합쳐주는 역할로 사용되어야 할 것 같은데?'라는 느낌은 있는데, 확실하지 않아서 많은 사람들과 의견을 공유해봐야 할 것 같다.

💰 React + TypeScript로 마이그레이션


기본 과제가 더러운 코드를 클린 코드로 바꾸는 작업이었다면, 심화 과제는 클린 코드를 리액트와 타입스크립트로 마이그레이션 하는 것이었다.
그래서 기본 과제에서도 심화 과제를 고려해서 최대한 리액트스럽게 작성하는 방향으로 가라고 코치님께서 언질을 주셨다.

기본 과제를 하며 함수 분리에 집중하다보니 리액트를 고려하지 않은 방향으로 간 것 같아서 마이그레이션이 어려울 거라 예상했다.

하지만 예상과 다르게 이벤트나 유틸 함수들을 커스텀 훅에서 재활용할 수 있었다.
기본 과제에서 이벤트와 유틸 함수로 작성한 코드들이 조금의 변형을 거쳐 커스텀 훅에서 모두 사용됐다.

나름 뿌듯함을 느끼고 있긴 한데, 다시 생각해보니 동작을 하려면 이벤트나 유틸 함수는 원래 다시 써야만 하는 거지 않나...?

💰 마치며


함수형 프로그래밍을 기법을 이용하여 클린 코드를 만들고 싶었지만 아직 함수형 프로그래밍의 개념도 잡지 못했다.

액션과 계산, 데이터를 구분하고 추상화 계층으로 분리하고 타임 라인을 짠다는데, 내 코드는 다 액션같고 어떻게 액션을 계산으로 바꿀지 감이 안 잡힌다.

아직 <쏙쏙 들어오는 함수형 프로그래밍> 스터디 진행이 챕터2라서 뒤에서 자세히 설명한다고만 하고 추상적인 개념만 알려주고 있긴 하다.
이 스터디가 끝났을 때는 능숙하고 빠르진 않더라도, 조금의 고민하는 시간을 거쳐 코드를 분리하고, 비지니스 로직과 도메인 로직을 분리할 수 있는 판단을 가졌으면 한다.

리팩토링할 때, 나는 단순히 '이거는 함수로 뺄 수 있겠네?' 또는 '이건 중복이네?' 정도만 생각한다.
'이건 책임이 두 개네?!', '이건 부수 효과를 한 함수에 몰고 이 부분은 분리할 수 있겠네?!'라는 생각을 할 수 있도록 좀 더 예민하게 바라볼 수 있는 눈썰미가 필요하다고 생각했다.

💵 Keep: 현재 만족하고 계속 유지할 부분

아직 2일차지만... 스터디 주최는 잘 했다.
확실히 나는 약속(책임)으로 묶여있어야 움직인다.
이번 스터디가 잘 마무리 된다면 다음 챕터인 테스트 코드도 진행하고 싶다.

올해부터 아침마다 할 일 목록을 작성한다.
파워 P인 나는 평생을 계획을 세우진 않았지만, 요즘 부쩍 필요성을 느낀다.
유튜브를 보면 갓생사시는 분들은 루틴을 정해서 시간 별로 할 일을 정해놓던데, 나는 그렇게는 못하겠더라.
몇 번 시도는 해봤지만, 번번히 실패했다.
그래서 과하지 않게 할 일 목록을 만들고 하루 중에 다 지운다는 생각으로 하고 있는데, 은근 효과가 있다.

💵 Problem: 개선이 필요하다고 생각하는 문제점

할 일 목록을 작성할 때, 두루뭉술하게 적으면 기준이 모호해지고 다른 할 일을 추가하려고 해도 해도 되는지 가늠이 오지 않는다.

하루중에 버려지는 시간이 정말 많다.
그리고 이상하게 뭔가 하기 싫을 때 허기가 진다.
스트레스를 받으면 먹는 걸로 푸는 건가...?

💵 Try: 문제점을 해결하기 위해 시도해야 할 것

할 일 목록 작성 시 명확하게 적어야겠다.
여러 할 일을 최소한의 단위로 만들어서 많이 적는 게 더 좋을 것 같다.
지금까지는 굵직한 할 일만 적었는데, 막상 그러니 두루뭉술하게 적기도 하고, 하나만 오래 붙잡고 있으니 집중도 떨어진다.

버려지는 시간은 잘 써야겠다.
집에서만 하지말고 카페를 나가볼까?
먹는 건 아직 살이 찌는 느낌은 아니라 당분간 지켜봐야겠다.

2개의 댓글

comment-user-thumbnail
2025년 1월 11일

Pipeline 패턴,,, 과제에 적용한 부분 놀랍습니다!

1개의 답글

관련 채용 정보