토스 | SLASH 21

예비 프론트 엔드 개발자로서 보면 좋을 것 같은 발표들을 정리

SLASH 페이지

1. Micro-frontend React, 점진적으로 도입하기

열어보기
  • 이 발표에서 소개하는 것
1
2
3
4
5

(1) Django MVC 프로젝트에 점진적으로 React를 도입한 방법
(2) 마이크로 프론트엔드 아키텍처를 도입한 이유와 후기
(3) 프론트엔드 빌드 시간을 효과적으로 단축한 비법

(1) Django MVC 프로젝트에 점진적으로 React를 도입한 방법

토스의 서버사이드에서 HTML 템플릿을 렌더링하는 모놀리식 Django MVC 프로젝트에서 모던 리액트 코드로 어떻게 이전했는지?

  • 시작은 create-react-app을 사용해 빠른 프로젝트를 빌드
  • 복잡한 webpack, Babel, ESLint 등의 설정을 건너뛰게 해 줄 것이라 믿음
  • 완전한 SPA가 아니고 Django와 함께 사용할 때엔 설정의 충돌이 많이 있었음 → 처음부터 설정을 해보자

create-react-app

  • webpack-bundle-tracker(wbt), django-webpack-loader(dwl)를 사용
  • wbt는 빌드 결과물의 chunk 정보를 JSON 파일로 추출해주고 dwl은 이 JSON파일을 토대로 스크립트 태그나 링크 태그를 생성해줌
  • 많은 부분이 리액트로 바뀌고 나니 패키지 관리가 어려워지고 빌드 시간이 길어짐
  • 그래서 Micro-frontend 아키텍처를 사용하기로 함
  • 기존의 거대한 소스 코드를 독립적인 패키지(인프라, 라이브러리, 서비스 패키지)로 각각 분리함
  • 이를 위해 Yarn 2와 Workspace Plugin을 사용 중
  • 이로서 소스 코드부터 빌드 설정까지 완벽하게 격리되고, 의존성 지옥을 탈출할 수 있으며, 빌드 속도도 최적화됨
  • 빌드 시간을 줄이는 가장 좋은 방법은 빌드를 하지 않는 것
  • 소드코드가 바뀐 패키지만 빌드하고 나머지는 기존 빌드 결과물을 재사용
  • Git을 통해 패키지의 변화를 캐치(Git의 해시를 사용)하고 변화가 감지된 패키지만 빌드

2. 프론트엔드 웹 서비스에서 우아하게 비동기 처리하기

열어보기

웹 서비스에서 가장 다루기 어려운 부분은 무엇인가?

  • jQuery의 명령형 프로그래밍에서 React/Vue와 같은 프레임워크의 사용으로 선언형 프로그래밍으로 전환
  • 비동기 프로그래밍은 끊기지 않는 60 프레임의 좋은 사용자 경험을 위해서는 필수
  • 예시 코드 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getBazFromX(x) {
if (x === undefined) {
return undefined;
}

if (x.foo === undefined) {
return undefined;
}

if (x.foo.bar === undefined) {
return undefined;
}

return x.foo.bar.baz;
}
  • 문제점
    → 하는 일은 단순하지만 코드가 너무 복잡함
    → 각 프로퍼티에 접근하는 핵심 기능이 코드로 잘 드러나지 않음

  • 해결책
    → OptionalChaining 문법을 활용

1
2
3
4
5
function getBazFromX(x) {
// if문이 사라져 코드가 간결해짐
// 함수의 역할이 ?를 통해 잘 드러남
return x?.foo?.bar?.baz;
}
  • 예시 코드 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function fetchAccounts(callback) {
fetchUserEntity(err, user) => {
if (err != null) {
callback(err, null);
return;
}

fetchUserAccounts(user.me, (err, accounts) => {
if (err != null) {
callback(err, null);
return;
}

callback(null, accounts);
})
}
}

  • 문제점
    → 코드가 너무 복잡함
    → 성공 케이스와 실패 케이스가 섞여서 처리됨
    → 매번 에러 유무를 확인해야 함

  • 해결책
    → async-await 문법을 활용하기
    → 비동기처리를 이용해 성공 케이스만을 다루고 실패 케이스는 catch문을 사용해서 처리함으로써 실패 케이스를 외부에 위임할 수 있다

1
2
3
4
5
async function fetchAccounts() {
const user = await fetchUserEntity();
const accounts = await fetchUserAccounts(user.no);
return accounts;
}
  • 예시를 통해 알 수 있는 좋은 코드와 나쁜 코드의 특징

    • 좋은 코드 : 성공, 실패 케이스를 분리해 처리 가능, 비즈니스 로직을 한눈에 파악할 수 있다
    • 나쁜 코드 : 성공, 실패 케이스가 서로 섞임, 비즈니스 로직의 파악이 어려움
  • 비동기를 처리하는 부분을 정의하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SWR이나 React Query를 사용하여 비동기를 정의
const { data, error } = useAsyncValue(() => {
return fetchSomething();
});

// 컴포넌트에서 로딩과 에러 처리를 동시에 수행함
function Profile() {
const foo = useAsyncValue(() => {
return fetchFoo();
});

if (foo.error) return <div>로딩에 실패했습니다.</div>;
if (!foo.data) return <div>로딩 중입니다...</div>;
return <div>{foo.data.name}님 안녕하세요!</div>;
}

→ 이는 나쁜 코드

  • 비동기 코드가 여러개가 섞이게 되면 비동기 지옥이 됨

  • 2개의 비동기 리소스를 가져올 때 상태가 각각 ‘로딩, 에러, 완료’로 나뉜다면 이 때 상태는 3의 제곱으로 9가지 상태를 가질 수 있음

  • 리액트에서는 더더욱 이 비동기 처리가 어려운데 React팀에서는 React Suspense for Data Fetching도구를 제공함(아직은 실험 버전에서만 사용 할 수 있음)

  • 어떻게 에러 상태와 로딩 상태를 분리하는가?

1
2
3
4
5
6
7
8
9
10
11
12

<!--
1. 컴포넌트를 쓰는 쪽에서 로딩처리와 에러 처리를 한다
2. 로딩 상태는 가장 가까운 Suspense의 Fallback으로 그려진다
3. 에러 상태는 가장 가까운 ErrorBoundary가 componentDidCatch()로 처리
-->
<ErrorBoundary fallback={<MyErrorPage />}>
<Suspense fallback={<Loader />}>
<FooBar />
</Suspense>
</ErrorBoundary>

  • Recoil에서는 Async Selector를, SWR, React Query에서는 { suspense: true}를 정의해주면 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getUserName(id) {
var user = JSON.parse(fetchTextSync("/users" + id));
return user.name;
}

function getGreeting(name) {
if (name === "Seb") {
return "Hey";
}
return fetchTextSync("/greeting");
}
function getMessage() {
let name = getUserName(123);
return getGreeting(name) + ", " + name + "!";
}
runPureTask(getMassage).then((message) => console.log(message));
  • Recoil의 비동기 셀렉터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const templateSetSelector = selectorFamily({
key: "@messages/template-set",
get: (no: number) => async () => {
return fetchTemplateSet(no);
},
});

export const historiesOfTemplateSetSelector = selectorFamily({
key: "@pages/messenger/template-set/histories",
get: (templateSetNo: number) => async ({ get }) => {
return fetchHistoriesOfTemplateSet(templateSetNo);
},
});

function TemplateSetDetails({ templateSetNo }: Props) {
const templateSet = useRecoilValue(templateSetSelector(templateSetNo));
/* 이 아래에서는 templateSet이 보장됨(타입가드가 필요 없음) */
}
  • React hooks와 suspense의 유사도

  • Hooks에서는 useState, useMemo, useCallback, useEffect와 같은 선언적 코드를 통해 웹 서비스의 코드 복잡도를 낮춰줌

  • 실제 상태 관리, 메모이제이션과 같은 작업은 React 프레임워크가 대신 수행함

  • suspense를 사용할 때에도 컴포넌트에서 비동기적인 리소스를 선언하고 그 값을 읽어온다고 선언하면 컴포넌트를 감싸는 부모 컴포넌트가 대신 수행함

  • try-catch문을 통해 실패할 수 있는 함수는 throw를 통해 부모 함수로 던지고 이 에러 처리를 부모 함수가 수행함

  • 이런 책임 분리 방식을 대수적 효과라고 함

  • 하지 못한 이야기들…
    React Concurrent Mode, useTransition, useDeferredValue
    → React에서 부분적으로 렌더 트리를 완성함으로써 더 나은 사용자 경험 향상 가능

참고자료1 : 데이터를 가져오기 위한 Suspense (실험 단계)
참고자료2 : Algebraic Effects for the Rest of Us


3. JavaScript Bundle Diet

열어보기
  • 사용자는 느린 로딩을 참지 못한다 (5초 초과시 이탈율 38%)
  • API 호출이 너무 많거나 이미지 처리 등 다양한 원인으로 느려질 수 있음
  • 번들 사이즈는 그 중 하나
  • 자바스크립트는 파일을 다운로드하고 파싱한 후에 컴파일을 하고 실행까지 하는 등 같은 용량이더라도 처리 비용이 크다
  • Webpack Analyse는 가장 다양한 정보를 주지만 사용하기 어려움
  • Webpack Visualizer는 깔끔한 시각화를 보여주지만 기능이 부족함
  • 그래서 Webpack Bundle Analyzer를 추천함
  • 번들 용량을 줄일 때 가장 먼저 해야 할 일 : 용량이 큰 라이브러리는 가벼운 라이브러리로, 용도가 겹치는 라이브러리는 하나로 통일
  • 여러 라이브러리가 다른 버전의 라이브러리를 참조하는 경우 Dependency confilict가 일어남
  • npm은 라이브러리를 트리 구조로 저장하기 때문에 node_modules가 과도하게 커지게 됨
  • npm dedupe 명령어와 yarn deduplicate 패키지, yarn 2에선 dedupe 명령어가 생김
  • webpack alias기능을 이용한다면 동일한 라이브러리의 중복을 피할 수 있음
  • lodash는 기능에 비해 용량이 클 수 있다
  • 따라서 가능한 네이티브 함수를 이용하거나 더 가벼운 함수로 구현하여 사용 중임 (참고자료)
  • polyfill도 고려할 것
  • Bundle Phobia를 통해 버전별 번드 용량, 해당 라이브러리의 디펜던시를 분석 할 수 있다
  • 더 가벼운 라이브러리를 만들 때엔 tree-shaking를 고려할 것
  • 하지만 tree-shaking은 side effect가 없을 때에만 가능
  • terser를 사용하면 terser가 /*#__PURE__**/를 발견하면 해당 코드에 side effect가 없다고 판단함 (Babel은 이런 pure annotation을 인식하지만 ts는 인식하지 못함)
  • 무거운 라이브러리의 영향을 줄이기

4. 실무에서 바로 쓰는 Frontend Clean Code

열어보기
  • 이 발표에서 소개하는 것
1
2
3
4
5
6

1. 실무에서 클린 코드의 의의
2. 안일한 코드 추가의 함정
3. 로직을 빠르게 찾을 수 있는 코드
4. 액션 아이템

(1) 클린코드가 의미 있는 이유란?

“그 코드는 안건드리시는 게 좋을거에요. 일단 제가 만질게요.^^;;”

→ 흐름 파악이 어렵고, 도메인 맥락 표현이 안 되어 동료에게 물어봐야 알 수 있는 코드
→ 이런 코드는 개발 시 병목되고 유지보수 시 오래 걸리고 심한 경우 기능 추가가 불가능한 상태가 됨

  • 실무에서 클린 코드의 의의는 유지보수 시간의 단축을 뜻한다.

(2) 안일한 코드 추가의 함정

  • 기존 코드에 기능을 추가하다보면 일어날 수 있는 함정들
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function QP() {
const [popupOpened, setPopupOpened] = useState(false);

async function handleQuestionSubmit() {
const 약관동의 = await 약관동의_받아오기();
if (!약관동의) {
await 약관동의_팝업열기();
}
await 질문전송(questionValue);
alert("질문이 등록되었습니다.");
}

async function handleMyExpertQuestionSubmit() {
await 연결전문가_질문전송(questionValue, 연결전문가.id);
alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}

return (
<main>
<form>
<textarea placeholder="어떤 내용이 궁금한가요?" />
<Button onclick={handleQuestionSubmit}>질문하기</Button>
</form>
{popupOpened && (
<연결전문가팝업 onSubmit={handleMyExpertQuestionSubmit} />
)}
</main>
);
}

이 코드는…

  • 하나의 목적인 코드가 여러 블럭으로 흩어져 있음
  • 하나의 함수가 여러가지 일을 하고 있음
  • 함수의 세부 구현 단계가 제각각임

그 때는 맞고 지금은 틀리다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

function QP() {
const 연결전문가 = useFetch(연결전문가_받아오기);

// 새 함수
async function handleNewExpertQuestionSubmit() {
await 질문전송(questionValue);
alert("질문이 등록되었어요.");
}

//
async function handleMyExpertQuestionSubmit() {
await 연결전문가_질문전송(questionValue, 연결전문가.id);
alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}

return (
<main>
<form>
<textarea placeholder="어떤 내용이 궁금한가요?" />
{연결전문가.connected ? (
<PopupTriggerButton
popup={{
<연결전문가팝업
onButtonSubmit={handleMyExpertQuestionSubmit}/>
}}
>
질문하기 </PopupTriggerButton>
) : (
<Button onClick={async () => {
await openPopupToNotAgreedUsers();
await handleMyExpertQuestionSubmit();
}}
>
질문하기
</Button>
)}
</form>
</main>
);
}

async function openPopupToNotAgreedUsers() {
const 약관동의 = await 약관동의_받아오기();
if(!약관동의) {
await 약관동의_팝업열기();
}
}

  • 클린코드란 원하는 로직을 빠르게 찾을 수 있는 코드

(3) 로직을 빠르게 찾을 수 있는 코드

  1. 응집도: 같은 목적의 코드는 뭉쳐두자
  • 무엇을? 당장 몰라도 되는 디테일
  • 코드 파악에 필수적인 핵심 정보를 뭉치면 오히려 답답해짐
  • 어떻게 응집시킬까?
    • 첫번째, 남겨야 할 핵심 데이터와 숨겨야 할 세부 데이터를 나누기
    • 두번째, 핵심 데이터는 밖에서 전달, 나머지는 뭉친다
  • 이를 선언적 프로그래밍이라 한다
  • 그렇다고 선언적인 코드가 무조건 좋은 것은 아니다
  1. 단일책임: 하나의 일을 하는 뚜렷한 이름의 함수를 만들자
  • 일단, 함수 이름을 지어보자 → 함수가 하는 일을 모두 표현할 수 있는 이름을 짓자
  • 한 가지 일만 하는 명확한 이름의 함수로 쪼개기
  • 비슷한 방식으로 한 가지 일만 하는 기능성 컴포넌트 만들기 (React Hooks)
  • 조건이 많아지면 오히려 한글 변수명을 사용하는 게 더 좋을 수도 있다 (마치 주석을 달아놓은 것만 같은 효과도 있음 )
  1. 추상화: 핵심 개념을 뽑아내자
  • 컴포넌트로 추상화하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 팝업 코드 제로부터 구현
<div style={팝업스타일}>
<button onClick={async () => {
const res = await 회원가입();
if (res.success) {
프로필로이동();
}
}}>전송</button>
</div>

// 중요 개념만 남기고 추상화
<Popup
onSubmit={회원가입}
onSuccess={프로필로이동}
/>

  • 함수를 추상화하기
1
2
3
4
5
6
// 설계사 라벨을 얻는 함수 구현
const planner = await fetchPlanner(plannerId);
const label = planner.name ? "새로운 상담사" : "연결중인 상담사";

// 중요 개념을 함수 이름에 담아 추상화
const label = await getPlannerLabel(plannerId);

얼마나 추상화할 것인가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Level 0
<Button onClick={showConfirm}>전송</Button>;
{
isShoConfirm && (
<Confirm
onClick={() => {
showMessage("성공");
}}
/>
);
}

// Level 1
<ConfirmButton
onConfirm={() => {
showMessage("성공");
}}
>
전송
</ConfirmButton>;

// Level 2
<ConfirmButton message="성공">전송</ConfirmButton>;

// Level 3
<ConfirmButton />;
  • 꼭 레벨3가 정답이 아님 → 상황에 따라 다름
  • 추상화 수준이 섞여있다면 코드 파악이 어려우니 주의할 것
  1. 액션 아이템
  • 담대하게 기존 코드 수정하기: 두려워하지 말고 기존 코드를 씹고 뜯고 맛보고 즐기자
  • 큰 그림 보는 연습하기: 그 때는 맞고 지금은 틀리다. 기능 추가 자체는 클린해도 전체적으로는 어지러울 수 있다
  • 팀과 함께 공감대 형성하기: 코드에 정답은 없다. 명시적으로 이야기를 하는 시간이 필요하다
  • 문서로 적어보기: 글로 적어야 명확해진다. 향후 어떤 점에서 위험할 수 있는지, 어떻게 개선할 수 있는지를 정리해두기

느낀점

  • 프론트엔드 엔지니어는 끝없이 더 나은 사용자 경험을 위해 투쟁하는 사람들이다
  • 개발에 필요한 스킬만이 중요한 것이 아니다, 결국은 협업, 팀플레이.
  • 결국 코드를 적는 일도 글쓰기와 다르지 않다. 누군가에게 잘 읽히는 을 써야 하는 것.
  • 실무 레벨에서 필요한 스킬을 배웠다기 보다는 어떤 마음가짐을 배웠다는 게 더 중요했던 강의들이었다.