본문으로 건너뛰기

요즘 유인동님의 신간인 멀티패러다임 프로그래밍을 읽고 있다. 유인동님의 함수형 강의는 같은 팀 동료 분이 추천해주셔서 들었던 기억이 있는데, 책이 나왔다는 소식에 반가운 마음 + 흥미로운 주제에 구매하게 됐다.

참고로 책의 예제 코드는 이 레포에서 확인할 수 있다. 어떤 디렉터리에 있는지 찾느라 좀 헤매서 남겨본다.

초반 챕터에서 반복자(Iterator) 패턴에 대해 다루는데, 개인적으로 한국어로 iterator를 설명하는 자료 중 가장 명쾌하게 이해할 수 있는 설명이라고 느꼈다. 또한 이 챕터에서 설명하고 있는 내용이 멀티패러다임이라는 주제를 흥미롭게 소개해주는 것 같아 가벼운 글로 소개해보고자 한다.


기본적으로 반복자 패턴은 선언과 동시에 평가되지 않고, 필요한 시점에 평가할 수 있다. 예제를 통해 살펴보자.

const array = [1, 2, 3, 4, 5];
array.reverse();
console.log(array[0], array[1]); // 5 4

반복자를 사용하지 않는 일반적인 방법은 기본적으로 즉시 평가가 일어나며 원본 배열의 mutation이 발생한다. 물론 원본을 복사해둘 수도 있지만 메모리 효율적인 방법은 아니다.

const array = [1, 2, 3, 4, 5];

function reverse(arrayLike) {
let idx = arrayLike.length;
return {
next() {
if (idx-- >= 0) {
return { value: arrayLike[idx], done: false };
} else {
return { value: undefined, done: true };
}
},
};
}

const iterator = reverse(array);
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 4, done: false }

이 예제에서는 idx라는 변수를 갖는 객체를 리턴하는 함수를 만들었다. reverse()를 통해 객체를 선언하는 시점이 아닌 next()가 호출되는 각각의 시점에 평가가 이루어진다.

즉, 평가는 필요한 시점에 필요한 만큼 효율적으로 이루어진다.

배열의 길이가 커지거나 성능에 대한 요구사항이 엄격해질수록 지연 평가는 큰 장점이 될 수 있다. 성능과는 별개로 원본 배열을 immutable하게 다룰 수 있다는 점에서의 장점도 있다.

여기서 재밌는 지점은 반복자 패턴은 전통적인 객체지향 디자인 패턴이라는 점이다. 그러나 함수가 일급인 자바스크립트에서는 예시 코드에서처럼 함수형 프로그래밍의 장점이 결합되어 더 큰 시너지를 낼 수 있다.

여기서 generator를 사용하여 같은 구현을 명령형 코드로도 구현할 수 있다.

function* reverse<T>(arrayLike: ArrayLike<T>): IterableIterator<T> {
let idx = arrayLike.length;
while (idx--) {
yield arrayLike[idx];
}
}

const array = ["A", "B", "C", "D", "E", "F"];
const reversed = reverse(array);

console.log(reversed.next().value); // F
console.log(reversed.next().value); // E
console.log(reversed.next().value); // D

앞선 reverse 함수는 idx라는 멤버를 가지는 객체를 생성함으로서 반복자를 구현했다. 이 예시에서는 generator를 사용하여 조건문을 사용해 명령형 코드로 같은 기능을 구현했다.

이처럼 다양한 패러다임의 유연한 선택과 그들간의 조합을 통해 문제에 적합한 해결책을 찾을 수 있다.

여기까지 읽고 흥미를 느꼈다면 책을 읽어보는 것도 좋을 것 같다. 초반부는 앞서 링크한 레포에서 읽어볼 수도 있다.

나는 요즘 넘 재미나게 읽고 있어서 추천~


부록

새롭게 추가된 Iterator.prototype의 메서드들로 (map, filter, reduce 등) 반복자에서 지연 평가 로직을 더 간단하게 구현 할 수 있다. 알아두면 좋을 것 같다.

악기 연습할 때 메트로놈을 사용하는데, 웹에서 써본 메트로놈들은 기능적으로 완벽하게 마음에 드는 경우가 잘 없었다.

특히 볼륨 조절이 없는 경우가 많아서 그냥 유튜브에서 xxx BPM metronome 같은 영상을 틀고 연습하는 경우가 많았다.

그런데 유튜브 영상은 BPM을 조절하려면 다른 영상을 서치해서 재생하는 과정이 너무 번거로워서 이럴거면 하나 만들어보자 싶어서 만들었다.

copilot edit 기능을 적극적으로 활용했다. 웹 오디오 API에 대한 지식이 거의 없었는데, copilot이 관련된 코드를 잘 만들어줘서 큰 도움이 됐다. 만들어진 코드를 수정하다 보니 오디오 API에 대해서 이해가 되는 부분도 꽤 있었다.

첫 삽 부터 작동하는 프로토타입을 완성하는 데 2~3시간 정도 사용한 것 같다. 그 이후로도 종종 들여다보고 리팩터 하거나 기능을 추가해주고 있다.

아래 링크에 배포해 두었다!

https://jeonjaewon.github.io/web-metronome/

프로그래밍에서의 많은 문제들이 그러하듯이 상태관리는 정답이 없다고 생각합니다.

상태관리의 형태는 사용하는 라이브러리가 무엇이냐, 어떤 컨벤션에 따라 정의하느냐, 등 조직마다 굉장히 구체적인 차이들이 있을 것입니다.

그러니 어떻게 하는 것이 "좋은" 상태관리냐? 라고 묻는다면 제각각의 대답이 나올 수밖에 없지 않을까 생각합니다.

하고 있는 고민과, 겪고 있는 문제가 다르니 좋은 것에 대한 정의가 서로 달라지는 것이지요.

상태를 하나 추가한다고 생각해 보겠습니다. 일반적으로는 아래와 같은 고민들이 있을 것입니다.

  • 어떤 파일에, 무엇들과 함께 둘 것인가?
  • 디렉터리는 어떻게 관리할 것인가?

단순히 상태 하나를 추가하려고 해도 생각해야 할 것들이 상당히 많습니다.

즉, 상태관리는 많은 고민과 판단을 요구합니다. 그러니 상태관리가 피곤할 수밖에 없습니다.

이러한 상태관리의 피로를 줄여주는 유용한 오픈소스를 소개하려고 하는데요. 바로 overlay-kit입니다.


생각해보면 우리가 다루는 상태들은 오버레이의 상태인 경우가 꽤 많습니다. overlay-kit은 React에서 오버레이들의 상태를 간편하게 다루는데 큰 도움을 줍니다.

예시 코드와 함께 overlay-kit이 해결하는 문제에 대해 이야기해 보겠습니다.

export const SimpleExample = () => {
const [isOpen, setIsOpen] = useState(false);

const openOverlay = () => {
setIsOpen(true);
};

const close = () => {
setIsOpen(false);
};

return (
<>
<button onClick={openOverlay}>Open with useState</button>
<Dialog opened={isOpen}>
<span>Overlay Created: </span>
<Button onClick={close}>Close</Button>
</Dialog>
</>
);
};

위 코드는 useState를 사용한 <Dialog />를 열고 닫기 위한 간단한 에제입니다. (useState 가 아닌 별도의 상태 관리 라이브러리를 사용하더라도 세부 형태는 다를지라도 본질적으로는 같은 일을 하는 코드를 작성하게 될 것이라 생각합니다.)

위 예제는 아래와 같은 아쉬운 부분들이 있습니다.

  1. isOpen 상태를 어떻게 관리할 것이냐의 문제
  • 앞선 내용처럼 하나의 상태를 정의하는 것은 많은 고민이 따라옵니다.
  1. 열기, 닫기 이벤트의 번거로운 핸들링
  • 이런 열고 닫히는 상태를 갖는 컴포넌트들은 굉장히 많습니다. 그 때 마다 상태를 하나씩 만들고, 각각에 대해서 열기, 닫기 이벤트를 정의하는 일은 번거로운 일입니다.
  1. <Dialog /> 컴포넌트의 트리상 위계
  • <Dialog />를 다른 컴포넌트에서도 열고 싶다고 해보겠습니다. 그렇다면 이 <Dialog /> 컴포넌트는 공통 부모에 위치시키는 등 컴포넌트의 위계 관리가 필요합니다.

다음은 overlay-kit을 사용한 예제입니다.

export const OverlayKitExample = () => {
const openOverlay = () => {
overlay.open(({ isOpen, close }) => (
<Dialog opened={isOpen}>
<span>Overlay Created: </span>
<Button onClick={close}>Close</Button>
</Dialog>
));
};

return <button onClick={openOverlay}>Open with overlay-kit</button>;
};

앞서 언급한 모든 고민들을 한 번에 해결한 모습입니다.

재밌는 점은 상태를 개발자가 직접 관리하지 않음으로써 많은 문제가 해결됐다는 점입니다.

개인적으로는 <Dialog /> 컴포넌트가 트리의 어느 부분에 위치할지를 고민하지 않아도 되는 점이 굉장히 좋다고 생각합니다. 이 컴포넌트는 openOverlay() 함수로 격리되었고, 다른 곳에서 열고 싶다면 그냥 이 함수를 export 해서 사용하면 됩니다.

overlay-kit의 소스를 들여다 보기도 했는데, 사실 마법같이 해결되는 것은 없고 이야기한 모든 문제들은 overlay-kit 내부적으로 다 처리하고 있기에 우리는 그냥 잘 사용하면 되겠습니다.

회사에서 동료 분이 제안해주셔서 도입해보고, 저희 코드베이스에 맞게 수정해서 사용하고 있는데 너무 만족스럽네요.

머리 아픈 상태관리는 overlay-kit에 아웃소싱하고 우리는 더 중요한 문제를 푸는데 시간을 쓸 수 있겠습니다.

이건 내비게이션에 필요 없어요

업무하면서 도로명과 지번주소가 혼재되어 있는 데이터를 다룬 적이 있는데, 예를 들면 이런 데이터였습니다.

  • 주소1: 서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호
  • 주소2: 서울특별시 영등포구 국회대로62길 9 (여의도동)

예시의 주소 1의 경우 지번주소, 주소2는 도로명주소 입니다.

문제는 이 데이터를 토대로 내비게이션 API에 요청을 보내야 했는데, 저희가 사용 중인 API에서는 상세 주소에 대한 처리가 되어 있지 않아서 간혹 응답을 내리지 않는 경우가 있었습니다.

주소 3을 예로 들면 상세주소에 해당하는 반포자이아파트 101동 101호에서 101호는 사실 길찾기에 필요한 정보는 아닙니다. 이걸 포함시켜서 요청하면 길을 제대로 찾지 못하더라구요.

이해를 돕기 위해 주소를 두 가지로 분류해 보겠습니다.

  1. 배송을 위한 주소
  2. 길찾기를 위한 주소

1과 2가 같은 경우도 분명 있지만, 공동주택 등의 경우 정확한 호수를 찾기 위한 상세주소가 더해지기 때문에 표기에 차이가 생깁니다.

제가 겪은 문제의 경우 2만 취급하는 API에 1을 보냈기 때문에 발생한 문제였습니다.

그렇다면 문자열 뒷부분의 상세주소만 제거해서 요청하면 되겠다는 생각이 듭니다. 이제 이 문자열들의 어디서부터가 상세주소인지를 찾아야 합니다.

그렇다면 섞여 있는 도로명주소와 지번주소를 각각 다르게 파싱해야 할까요? 애초에 별도의 칼럼이 없는데 문자열만 보고 이 둘을 구분할 수는 있을까요?


모르겠을 땐 문서를 보기

도로명주소 안내 시스템에 가보면 도로명 주소 소개 pdf 파일을 배포하고 있습니다.

Example banner

눈여겨보아야 할 부분은 가운데의 동/리 지번도로명 건물번호에 대응된다는 점입니다. 자세한 도로명 건물번호 규칙은 아래와 같은데요.

Example banner

이 부분이 중요합니다. 도로명은 붙여 쓰고, 도로명과 건물번호는 띄우기 때문에 이는 기존의 동/리 지번과 공백을 기준으로 호환이 된다고 볼 수 있습니다.

무슨 말인지 앞서 보았던 예시를 다시 보겠습니다.

  • 주소1: 서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호
  • 주소2: 서울특별시 영등포구 국회대로62길 9 (여의도동)

지번주소의 동/리 지번반포동 20-43, 도로명주소의 도로명 건물번호국회대로62길 9에 해당하는 것을 알 수 있습니다. 그리고 이 주소들은 모두 가운데에 공백 문자를 1개 포함한 문자열입니다.

이 주소들 이후부터는 상세주소가 나오는 것을 확인할 수 있습니다.

즉, 도로명이냐 지번이냐에 관계 없이 단순히 문자열을 공백을 기준으로 앞에서부터 4번 취하면 상세주소를 제거한 주소를 얻을 수 있습니다.

"서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호"
.split(" ")
.slice(0, 4)
.join(" ");
// '서울특별시 서초구 반포동 20-43'

"서울특별시 영등포구 국회대로62길 9 (여의도동)"
.split(" ")
.slice(0, 4)
.join(" ");
// '서울특별시 영등포구 국회대로62길 9'

이제 이 문자열이 신주소인지 구주소인지와는 관계 없이 간단한 방법으로 상세주소를 제거할 수 있습니다.

  • 조금 더 common한 코드 베이스에 기여하기 / 목적 조직에서 일하기

웹 코드 베이스에 대한 이해도가 늘어나고 shared, common 정도로 표현되는 공통 모듈에 대한 기여도 자연스레 늘어났다. 목적 조직에서 몇 개월간 일하기도 했는데 작년 한 해 가장 몰입해서 일했던 경험으로 기억됐다. 내가 맡은 부분이 아니더라도 전체 제품에 대한 맥락을 고민하는 경험이 좋았다. 자연스레 업무시간 내 회의와 논의하는 시간은 상대적으로 늘어나고 코드를 집중해서 작업하는 시간은 줄었던 점은 아쉽다.


  • 희망 퇴직과 인수합병 이슈

회사가 경영상 어려움으로 인해 인원을 절반 가량 감축했다. 나도 회사를 계속 다닐지, 혹은 다닐 수 있을지 걱정하고 고민하느라 심적으로 어려웠다. 결과적으로는 계속 회사를 다니고 있다

회사가 감정적으로 격정적인 시즌이었다. 떠나기로 마음 먹은 분들은 그 자리에서 바로 짐을 싸고 나가셨다. 함께 일하던 동료들을 하루아침에 떠나보내는 일이 쉽지 않았다. 희망퇴직 이후로도 회사의 인수/합병 이슈 등이 반복되었다. 거취의 불확실성을 지닌 채로 일하는 것이 직원으로서 불안정하게 느껴진 한 해였던 것 같다.

배운 점이 있다면 이러한 변화 조차도 버텨낼 수 있는 어려움이라는 것을 알았다는 점이다. 그런 상황에서 스트레스를 받는 것은 피할 수 없었지만 할 일을 하다보니 어떻게든 버텨냈다.

당연한 사실이지만 회사와 개인은 계약 관계이고, 그 계약은 생각보다 견고하지 않을 수도 있겠다는 생각도 들었다. 개인으로서 더 길게 이어질 내 삶을 잘 개발하기 위해 노력해야겠다는 생각도 들었다.

어떤 선택을 하셨던지 함께 어려운 일을 겪은 동료 분들이 다들 잘 지내셨으면 좋겠다.


  • 혼자 개발하기

희망 퇴직으로 인해 웹 프론트엔드팀 팀원들이 모두 떠났다. 혼자서 잘 해낼 수 있을지 걱정이 많았다. 가장 오래 시간을 보낸 팀 동료들이 떠나니 일적인 면을 떠나서 심적으로도 어려웠다.

홀로 제품 개발에 투입되니 심리적 압박을 느끼는 경우가 종종 있었다. 어떤 문제가 잘 풀리지 않을 때, 기한은 정해져 있고 함께 논의할 사람은 없으니 그러한 압박감이 주는 스트레스가 있었다.

어떤 태스크를 진행하고 있는데 새롭게 다른 태스크가 들어오는 것의 핸들링이 힘들었다. 원래라면 다른 동료와 잘 분배해서 처리했을 일인데 이제는 어떻게든 혼자 해내야 하는 일이 됐다.

일단 일이 새롭게 들어오면 우선순위와 마감 기한에 대해서 커뮤니케이션을 했다. 급한 일이라면 진행 중인 일이 있더라도 미뤄두고 먼저 처리하는 등의 태스크 관리가 필요했다. 회사 Jira와는 별개로 개인적으로 ToDoist 등 툴을 사용해 일감을 정리했다. 개인 툴에 들어온 일의 우선순위, 마감기한, 업무 내용과 메모 등을 정리해 어쩔 수 없이 많아지는 업무 간 context switching을 효율적으로 할 수 있도록 노력했다.

돌이켜보면 일이 많았던 것도 있었겠지만 혼자라는 사실이 주는 심리적 압박감에 더욱 이것저것 도구나 방법 등을 찾았던 것 같다.

결과적으로 큰 이슈 없이 홀로 회사의 웹을 약 4개월간 운영했다. 신규 입사자 분이 오셔서 1인팀 생활은 마무리할 수 있었다. 웹 개발의 병목이 제품 개발 전체의 병목이 되는 상황이 두려웠는데 그런 상황은 일어나지 않아서 다행이다. 또한 내 의사결정이 미치는 범위가 상대적으로 커지는 상황을 경험하고 익숙해질 수 있었던 것 같다.


  • 채용 프로세스 참여

같이 일할 팀원을 뽑는 채용 프로세스에도 참여했다. 면접관으로서 면접에 참여하는 일이 처음에는 긴장되고 부담스러웠다. 회사의 얼굴로서 공적인 자리에 참여한다고 생각하니 어깨가 무겁게 느껴졌다.

자연스레 지원자를 어떻게 평가할 것인가? 혹은 어떤 질문을 할 것인가? 에 대한 고민도 이어졌다.

개인적으로 퀴즈 식의 면접은 면접자를 검증하는데 큰 도움이 되지 않는다고 생각한다. 왜냐하면 그런 식의 질문들은 면접 준비를 했다면 대답이 가능한 종류의 질문이기 때문이다. 혹은 어떤 질문은 실무와 매우 동떨어져 준비를 '해야만' 대답이 가능한 경우도 있을 것이다.

내가 같이 일하고 싶은 사람은 면접 준비를 잘 하는 사람은 아니었다. 벼락 치기가 가능한 질문에 대답을 잘한다고 해서 조직에서 원하는 역량을 검증할 수 없다고 생각했다.

퀴즈 같은 질문을 최대한 지양하고 면접자 분이 문제를 접근하는 방법이나 어떤 주제에 대한 개인적인 의견들에 대해서 질문했다. 또한 그것을 표현하는 커뮤니케이션 내지는 태도 또한 집중해서 보았다.


  • 정답 맞추기

코드에 정답이 있다고 생각하곤 했다. 작업을 시작하기 전에 best practice 따위의 검색 결과를 주욱 훑었다. 문제에 대해 내가 떠올리지 못하고 있는 정답이 있을 거라고 생각했다.

지금으로서 내린 결론은 설사 그것이 존재하더라도 그다지 중요하지 않다는 것이다. 요구사항은 항상 변화하고 그로인해 '그때는 맞지만 지금은 틀리다'와 같은 상황이 계속 발생한다. 그 때 그렇게 탐닉했던 정답은 지금에 와서는 의미없는 것이 되는 경우가 많았다. 결론적으로는 정답에 대한 탐닉은 그만두기로 했다. 이는 코드 퀄리티를 포기하겠다는 의미는 전혀 아니다. 문제를 바라보는 관점이 달라졌다는 정도의 의미로 볼 수 있겠다. 변경을 감히 예측할 수는 없지만, 변경이 용이한 코드를 만들어야 한다는 생각이다.

지금까지 같이 일하며 '이런 사람과 일하면 좋았다'라는 사람들의 특징을 떠올려 보았다. 그런 사람들은 항상 정답을 제시하는 사람들은 아니었다. 여러 모습들이 있겠지만 그들은 대체로 주어진 기한을 준수하고, 과정과 결과를 공유하고, 꾸준히 개선을 해내는 사람들이라고 생각한다. 편안한 논의 파트너이기도 했다. 그렇다면 나 또한 항상 퀴즈의 정답을 맞추는 사람일 필요는 없을 것이다.

redux-saga는 redux 미들웨어로, 매우 강력한 사이드 이펙트 처리를 가능하게 만드는 라이브러리다.

최근 회사 코드베이스에서 이 의존성을 제거하고자 하여 그 의사결정의 근거들을 남긴다.

이 글은 읽는 사람이 redux-sagaredux-toolkit을 이용한 프론트엔드 개발 경험이 있다고 가정한다.


1. 러닝커브의 관점

Generator는 saga 이외의 사용처를 찾아보기 힘들다. 대부분의 입문자는 saga를 배우려면 먼저 generator부터 공부해야 한다.

또한 saga가 제시하는 멘탈 모델에 익숙해져야 한다. 공식 문서에 따르면,

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects.

사가의 멘탈 모델은 어플리케이션에서 사이드 이펙트를 책임지는 별도의 스레드와 같다. 일반적인 React 개발 경험과는 상당히 이질적이어서 익숙해지는데 시간이 필요하다.


2. 코드 변경으로 인한 사이드 이펙트 통제의 관점

사가는 generator function과 (이하 saga function) 이들을 트리거 시키는 action의 watcher가 함께 쓰이는 것이 일반적인 패턴이다.

이러한 패턴으로 인해 saga function을 실행하는데는 크게 세 가지의 방법이 있다.

  1. call
  2. fork
  3. put / dispatch

이 세 방식은 플로우 컨트롤에 있어 다른 특징을 가지므로, 어떤 구현을 원하느냐에 따라 적절히 선택해야 한다.

문제는 특정 saga function의 body를 변경하려면, 해당 saga function이 세 가지 중 어떤 방식으로 호출되고 있느냐를 살펴봐야 한다는 점이다. 즉 호출부를 면밀히 살펴봐야 선언부를 고치기 위한 확신을 얻을 수 있었다.

이는 결국 코드를 적극적으로 변경하기 힘들게 만든다. 코드의 변경으로 인한 사이드 이펙트 통제가 어렵다고도 표현할 수 있다.


3. 재사용의 어려움 관점

saga function을 재사용하기 위해서는 로직을 추출하고 그 로직에 대한 플로우 컨트롤이 필요하다. 여기서 플로우 컨트롤에는 자연스럽게 그 로직에 대한 호출이 포함된다.

예를 들어 saga function 내에서 다른 saga function을 호출한다면 2에서 언급했던 세 가지 방식 중 하나로 호출하게 된다. 즉 재사용을 하기 위해서 2에서 말한대로 적극적으로 변경하기 어려운 코드를 작성하게 되는 것이다.

또한 saga를 새로 만드려면 slice에 이를 위한 action들을 추가로 선언해주어야 한다. 이는 어느정도는 필연적으로 어려운 것으로 보았다. 왜냐하면 slice는 기본적으로 데이터를 담기 위한 단위이다. 여기에 로직의 재사용을 위한 saga를 위한 action을 담는다는 것은 본질적인 불일치가 존재한다.

결국 해당 saga의 action들을 넣을 적절한 slice가 없는 상황이 종종 발생한다. 이런 상황 때문에 우리는 종종 어쩔 수 없이 saga의 action만을 담고있는 slice를 만들곤 했다.


4. redux store가 비대해지는 문제

saga는 redux store를 비대하게 만든다. 여기서 비대하게 만든다는 표현의 정확한 뜻은, 본래 필요한 정도보다 비대해 진다는 뜻이다.

saga는 비동기 로직을 처리한 뒤 redux store에 담기 위한 의존성이다. 즉, store에 담기는 해당 데이터 외의 추가적으로 redux store를 비대하게 만드는 것은 saga가 해결하는 문제와는 관련 없는 불필요한 오버헤드라고 생각했다.

redux store가 비대해지면 전반적인 코드베이스 파악이 힘들어지고, 번들도 불필요하게 커진다.

또한 우리 코드는 side effect 의 제어를 제외한 부분까지도 saga로 구현된 부분이 다수 존재했다. 예를 들면 팝업을 열거나 닫는 로직조차 saga로 작성되어 있다. 어떻게 보면 비즈니스 로직까지 saga라는 레이어에 포함시킨 것이다. 그 결과 saga에서 select 혹은 put하기 위해 redux에 있을 필요 없는 상태 조차 redux에 넣고 있었다. 이는 또 한 번 store를 비대하게 만들었다.


5. Typescript 타이핑의 관점

saga는 Typescript native로 작성된 라이브러리가 아니다. 타이핑을 개선하기 위한 커뮤니티 라이브러리 (i.e. typed-redux-saga) 들이 있지만 이것도 그리 만족스러운 타입스크립트 코딩 경험을 주진 못했다.

다만 개인적으로 DX는 결국 비즈니스를 지탱하기 위한 최소 생산성을 담보하기 위한 선만 지켜도 된다고 생각하고, typed-redux-saga를 이용한 개발 경험이 아슬아슬하지만 그 선 이상은 지켜준다고 생각하기에 언급만 하고 넘어가겠다.


위와 같은 이유들로 saga를 떠나 다른 server state 관리 라이브러리로 마이그레이션하는 것으로 결정했다. 그리고 우리의 다음 스텝은 rtk-query로 결정했다. 관련되어 어떤 어려움이 있었고, 좋았던 점은 무엇이 있는지 등 관련된 이야기는 다음 글에서 다뤄 봐야겠다.

지금까지 우리의 웹 개발 스택을 잘 지탱해준 saga에게 감사를 표하며...

ts-pattern은 패턴 매칭을 Typeacript 환경에서 사용할 수 있는 라이브러리다. 패턴 매칭은 주로 함수형 프로그래밍 언어에서 조건 및 분기 구현을 위해 사용한다고 한다.

React 컴포넌트에서 삼항 연산자 등을 사용해 조건부 렌더링 구현을 하다가 대안이 없을까 싶어 찾아보다가 발견한 라이브러리다. Github에 걸려있는 적용 이전과 이후를 비교한 예시를 보고 혹해버렸다. 회사 프로젝트에 적용할 수 있을지 검토해보고 내린 결론과 그 근거를 정리해보았다.

예시 코드

지도의 마커를 정의하기 위해 아래와 같이 타입이 있다고 하자.

export type Path = "Start" | "Middle" | "End";

export type Marker = {
iconType: "Place" | "Path"; // 마커의 종류는 두 가지로, Place 마커의 경우에는 pathType을 가지지 않는다.
pathType?: Path;
};

ts-pattern을 적용하기 이전의 코드는 아래와 같다.

const getPathType = (marker: Marker): Path | undefined => {
if (marker.iconType === "Path") {
if (marker.pathType === "Start") {
return "Start";
}
if (marker.pathType === "Middle") {
return "Middle";
}
if (marker.pathType === "End") {
return "End";
}
}
return undefined;
};

export const BeforePattern = () => {
const marker: Marker = {
iconType: "Path",
pathType: "Start",
};
return <div>{getPathType(marker)}</div>;
};

중첩된 if문을 통해 구현했다.

ts-pattern을 적용할 시 아래와 같은 구현이 가능하다.

export const AfterPattern = () => {
const marker: Marker = {
iconType: "Pathway",
pathType: "Start",
};
return (
<div>
{match(marker)
.with({ iconType: "Pathway", pathType: "Start" }, () => "Start")
.with({ iconType: "Pathway", pathType: "Middle" }, () => "Middle")
.with({ iconType: "Pathway", pathType: "End" }, () => "End")
.otherwise(() => undefined)}
</div>
);
};

이외에도 놓치는 분기 케이스가 없도록 exhaustive() 를 추가하거나, 분기 및 type narrowing을 위해 when()등을 유용하게 쓸 수 있을 것 같다.

내 생각

  1. 새로운 언어를 배우는 것 같아 재밌었고, 확실히 일반적인 if, switch 분기보다 강력하다
  2. 그러나 추가적인 사용법을 배울만큼 리턴이 뚜렷하지 못한 듯
  • 코드만 보고 동작을 100% 확실하게 예측하기 어려웠다.
  • 반대로 표현하면 어떤 동작을 원할 때 바로 코드로 옮기기 쉽지 않았다.
  • 예를 들어 어떤 구현을 특정 조건에 공통으로 적용시켜야 한다면? 굉장히 일상적인 구현이지만 ts-pattern으로 구현하려니 몇 가지 의문점이 있었고, 이런 단순한 코드조차 새로 학습해야한다는 점은 코드베이스에 익숙하지 않은 개발자에게는 큰 진입장벽이 될 수 있다고 생각했다.
  • 조건과 분기는 구현에서 큰 비율을 차지하는데, 이걸 러닝커브가 있는 라이브러리를 도입할 만큼 뾰족하게 좋아진다는 느낌이 없었다. 확실한 이득이 있다면 도입을 고려했겠지만, 그 정도는 아니라고 판단했다.
  1. 실제 프로덕션 코드에서 적용할 만한 사례를 찾지 못함
  • ternary operator의 bracket안에서 변수 선언을 한다거나 하는 일은 굳이 할 일 없는 것 같다.
  • exhaustive()는 좋은 것 같은데, 린트로 대체할 수 있을 것 같아 그 쪽을 찾아보는게 나을 것 같다.
  • when()의 콜백으로 type predicates (i.e: (val: any): val is undefined => { ... })가 포함된 타입 가드 함수를 또 만들어야 한다는게 지나친 오버헤드 같다. 그런 함수를 선언해야 한다면 원래 방식으로도 잘 분기할 수 있을 것 같았다.
  • 개인적인 취향으로 특정 조건에 대한 handler를 with()의 두 번째 인자인 function으로 넘겨야 하는데, 값으로 충분할 때도 함수 형태로 넘겨야 하는 점이 가독성이 조금 떨어진다고 느꼈다.
  1. 조건이 복잡한 경우는 switch(true) 패턴으로 대체 가능할 듯
  • Typescript 5.3 이전에는 switch(true) 에서 조건 내의 type narrowing이 되지 않는 문제가 있어서 어려움이 있었는데, 5.3 에서 개선되었기 때문에 굳이 ts-pattern을 쓸 이유는 없어 보인다.
  • Typescript 릴리즈 노트 링크

결론적으로 도입하지 않기로 결정했다.

발견한 사이드 케이스

when() 에 넘기는 함수에서 generic을 사용할 경우 type narrowaing이 되지 않았다.

// 이런 함수들을 넘겨도 이후 블럭에서 val이 T가 아닌 T | undefined로 추론되었다.
const isDefined = <T>(val: T | undefined): val is T => {
return val !== undefined;
};

issue도 생성되어 있는데 당장 해결은 어려운 듯 하다.

재밌었지만 전체적으로 조금 아쉬운 도구라고 느꼈다.

개요

퍼널을 이탈할 때, 유저가 업데이트한 상태를 초기 상태로 clear하는 요구사항에 대해서 고민하다가 생각한 내용들을 정리해보았다.

TL;DR

  • useEffect 내의 코드 없이 cleanup만 정의된 코드는 문제가 될 수 있다.
  • 컴포넌트의 side effect는 가능하다면 이벤트와 이벤트 핸들러로 관리하고, useEffect는 그렇게 하기 어려울 때만 사용한다.

cleanup을 통한 솔루션

가장 먼저 떠올린 방법은 페이지 컴포넌트의 cleanup 시점에 clear시켜주는 방법이다.

function Payment() {
// ...

useEffect(() => {
return () => {
dispatch(paymentActions.clear());
};
}, []);

// ...
}

문제는 개발 환경에서 StrictMode 동작으로 인해 컴포넌트가 두 번 렌더되며 cleanup이 한 번 실행된다는 점이다. 결과적으로 원하는 값으로 유지시킬 수 없는 문제가 발생한다.

이에 대한 해결책으로 두 가지를 생각했다.

  1. 환경변수를 참조하는 등(dev인지 확인하고 분기) 어떻게든 cleanup을 단 한 번 실행시킨다.
  2. useEffect가 아닌 다른 방법을 통해 구현한다.

cleanup이 솔루션이 될 수 없는 이유

useEffect cleanup이 솔루션이 될 수 없다고 생각한 이유는 그 멘탈모델이 동기화를 구현하는 것 이기 때문이다. 요구사항은 유저가 특정 퍼널에 진입하거나, 어떤 버튼을 누르는 등의 동작을 했을 때 그에 반응하는 동작을 정의하면 될 뿐이다. 즉, 단순히 화면에서 사라질 때를 캐치하기 위해 cleanup을 사용하는 것은 완전 잘못된 사고방식일 수 있다.

그렇다면 다른 해결책은 무엇이 있을까?

기본적으로 리액트에서 컴포넌트는 렌더에 대해서 퓨어해야 한다. 공식처럼 계산만 해야하지, 변경해서는 안된다.

<Component props={1} /> 
<Component props={1} /> // 같은 input (props, state, context) 에 대해서 같은 결과를 리턴해야 한다

단, 렌더 이후에 side effect가 필요한 시점이 있다. 이런 side effect에는 데이터를 바꾸거나 API를 요청하는 등 다양한 행동이 있을 수 있다. 이러한 side effect는 대부분 이벤트 핸들러로 처리가 가능하다. 이벤트는 렌더 중에 일어나지 않으므로 퓨어할 필요가 없다.

Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure.

발생시켜야 하는 side effect가 그 어떤 이벤트 핸들러로도 처리가 어렵다면, 마지막 옵션으로 useEffect를 활용한다.

결론적으로, 우리가 제어하고 싶은 상황은 이벤트다. 퍼널을 이탈하는 동작은 뒤로 가기 버튼을 클릭하는 것 처럼 이벤트로서 처리될 수 있기 때문이다.


데이터를 어떻게 관리할 것인가?

한 퍼널에 해당하는 코드들을 하나의 모듈에 위치시켜 응집도를 높여야 전체적인 데이터 흐름이 관리될 수 있다고 생각했다.

이벤트 핸들러로 클리어하는 로직들이 곳곳에 산재되어 있으면 전역 상태에 대한 관리가 힘들어진다. 어떤 퍼널에서는 어떤 데이터를 클리어해야하고, 그 후 진입 시에는 어떤 데이터가 남아 있을 것이고.. 이런 것들을 생각하면 상태를 추적하기에 어려움이 있을 수 있다.

toss의 slash 라이브러리에 useFunnel라는 hook이 있다, 이처럼 한 퍼널에 해당하는 스텝들을 관리하는 방법도 좋은 것 같다.

const KyoboLifeFunnel = () => {
const [Funnel, state, setState] = useFunnel(['아파트여부', '지역선택', '완료'] as const).withState<{
propertyType?: '빌라' | '아파트';
address?: string;
}>({});

const 상담신청 = useLoanApplicationCallback();

return (
<Funnel>
<Funnel.Step name="아파트여부">
<아파트여부스텝 지역선택으로가기={() => setState(prev => ({...prev, step: '지역선택', isApartment: true})} />
</Funnel.Step>
<Funnel.Step name="지역선택">
<지역선택스텝 지역선택완료={(지역정보) => setState(prev => ({...prev, step: '완료', region: 지역정보})} />
</Funnel.Step>
<Funnel.Step name="완료">
<완료스텝 신청={() => 상담신청(state)} />
</Funnel.Step>
</Funnel>
);
};

약 3개월 정도 개발에 참여한 제품이 3월 경 드디어 세상에 나왔다.

주요 제품이라고 할 만한 정도로 크기가 큰 경우는 처음인 것 같다.

회사에서 운영 중인 서비스의 특징으로는 투 사이드 마켓 플랫폼 서비스라는 점이 있다. 자연스럽게 어떤 한 제품을 개발한다는 것은 수요자(이후 '유저') 사이드에 제공되는 서비스와, 공급자(이후 '드라이버') 사이드에 제공되는 서비스를 동시에 개발하게 되는 경우가 많다. 또한 현재 목적 조직 형태로 일하고 있는데, 이러한 특징들 덕분에 개발자로서는 재미있는 경험을 할 수 있다.

  • 유저-드라이버 사이드를 모두 고려하며 제품의 아이디에이션부터 UI / UX 개선, 전반적인 사용 경험까지 폭넓게 참여할 수 있었다.
  • 프론트엔드 개발자로서는 생각해야 하는 사용자의 경험이 전혀 다르다는 점이 재밌었다.

입사 후 사실상 첫 프로젝트였고, 프론트엔드 관련 개발은 대체로 혼자 진행했다. 초반에는 생각했던 것보다 크기가 큰 제품이어서 당황스럽기도 했지만, 같이 작업한 동료들의 도움 덕분에 잘 마무리할 수 있었다. 프로로서 처음으로 내놓은 볼륨 있는 결과물이자, 그 과정에 주도적으로 참여할 수 있는 점이 의미 있었다고 생각한다. 배포 이후 큰 이슈 없이 정상적으로 프로덕션에서 운영 중인 점도 프로젝트로서 좋은 마무리였다고 생각한다.

아쉬웠던 점은 다음처럼 정리해 볼 수 있겠다.

  • 예상 개발 시간 산정이 부정확했다.

    • 코드베이스와 기존 업무 프로세스 이해도가 다소 부족한 상태였기에 산정했던 것과 실제 개발 마무리 시간이 다른 경우가 꽤 있었다.
    • 프로젝트 이해도 부족과, 역량을 증명하고 싶다는 욕심 때문에 조금 무리한 일정을 잡기도 했다. 1~2 주야 괜찮았지만 프로젝트 기간이 길어지자 조금 무리였다는 생각이 든다.
  • 너무 큰 PR을 만들었다.

    • 초반의 프로젝트 이해도 부족으로 인해 작업 단위를 너무 크게 나눴고, 이는 PR이 너무 커지는 결과로 이어졌다. 코드 리뷰하는 동료들도 부담스러웠을 것이고, 갈수록 세부적인 작업의 파악들이 쉽지 않다고 느꼈다.
  • 원인은 복합적이겠지만 생각보다 많은 수요를 만들어 내지 못했다.

어쨌든 이 제품이 성공적으로 랜딩한 덕분에 많은 것을 배울 수 있었다. 과정에서 겪은 어려움도 있었지만 전보다 한 단계 성장했음에 의심은 없다.

  • 프로로서의 마음가짐

2022년은 돈을 받고 소프트웨어 개발자로 일하기 시작한 첫 해다. 학생 꼬리표를 떼고 처음으로 회사에서 일을 하며 참 많이 배웠다고 생한다.

어떤 태도로 일을 대하고, 사람들을 대할지에 대한 나만의 기준을 배워나갈 수 있었다. 돈을 받고 회사에서 일을 하는 한 프로로서의 태도를 잃지 않으려고 노력했다.


  • 이직

비교적 일찍 이직 프로세스를 경험하며 많은 경험과 고민을 할 수 있었다. 여담이지만 새 직장 첫 출근 전에 혼자 다녀온 여행 덕분에 소중한 경험을 할 수 있었고, 인간적으로도 성장할 수 있었다. 그렇기에 '2022년의 사건 단 하나'를 선정한다면 이직을 꼽아야겠다.

서류를 여러번 고치며 이력서를 어떻게 작성해야하는지 고민을 많이 했다. 경력에서 어필할 내용은 없었기에, 전 직장에서 맡은 프로젝트들에서 제가 했던 기술적인 판단들의 근거 위주로 작성다. 최종 이력서로 현 직장에 최종 합격 후 이야기해 보니 이력서에 확실한 플러스 요인은 없었더라도 마이너스는 딱히 없었던 것 같다. 이력서란 결국 면접 기회를 얻기 위한 수단 정도로만 생각한다면 성공이지 않았나 싶다.

과제, 전화면접부터 라이브 코딩까지 많은 전형을 경험했다 면접은 확실히 하면 할수록 느는 것 같다. 특히 라이브 코딩은 한 번 해보고 안해보고의 차이가 굉장히 크다고 느꼈다. 컬쳐핏 내지 인성 면접은 꾸며진 답변을 하기보다는 자신의 생각을 이야기하는게 면접관도, 면접자도 더 좋다고 느꼈다.


  • 솔직한 커뮤니케이션

현 직장의 기업 문화에서 크게 강조하는 내용 중 하나가 솔직한 커뮤니케이션이다. 사실 입사 전에는 걱정도 있었는데, '솔직함'과 '무례함'을 구분하지 못하는 사례들을 겪다 보니 과연 내부 문화가 실제로 어떨지 걱정이 앞섰다. 그렇기에 최종 면접 자리에서 조직 내에서 어떤 시각으로 솔직함을 바라보고 있는지 질문하기도 했다.

입사 후 느낀 점은 걱정과는 달리 이러한 문화가 잘 유지되고 있다는 점이다. 솔직함을 방패삼아 부적절한 의사소통을 하는 사례는 아직까지 경험해보지 못했다.

그전에는 스스로 굉장히 조심스러운 의사소통을 해왔다고 생각한다. 그렇기에 입사 후 최대한 솔직하고 직관적인 의사소통을 하기 위해 노력했다. 이미 많은 동료들이 그런 모습을 보여주고 있기에 어렵지 않게 적응할 수 있었던 것 같아 감사하다.

솔직함의 장점은 오해가 없다는 점이다. 누군가의 말이 다른 의도가 있을지 의심하고 고민하는 일은 상당히 스트레스받는 일이다. 직장에서 들은 말의 의미가 무엇인지에 대한 고민을 퇴근 후 집까지 가져오는 경우도 있을 것이다. 그러나 서로가 솔직하게 말한다는 믿음이 생기면 있는 그대로 받아들일 수 있어 간단하고 명료하게 대화를 마무리지을 수 있다. 이런 점이 업무에서 오는 어려움을 크게 줄여주고, 서로간의 신뢰를 쌓는데 도움을 주는 것 같다.


  • 기술적 배움

초기 온보딩과 이후 맡은 업무를 진행하며 회사에서 사용하고 있는 기술들에 익숙해질 수 있었다. redux와 saga를 익히고 있는데, 트렌디하지는 않더라도 충분히 뾰족한 장점이 있는 조합이라고 생각한다. 빠르게 프로젝트 하나를 맡아서 출시를 앞두고 있는데, 많은 도움을 주는 동료들과 함께 할 수 있어 즐겁게 일했다고 생각이 든다.