Post

29CM Frontend 전형 과제 회고 (2) - 고민과 후기

본 글은 3월에 진행했던 29CM Frontend Eginner 전형 과제에 대한 회고입니다. 모든 과제 내용은 외부 유출이 금지되어 있기 때문에 공개할 수 없는 점 양해 부탁드립니다.

본 회고는 1부, 2부로 나뉘어 작성되었습니다.

  • 1부 - 기술 선정
  • 2부 - 고민과 후기

1부에서는 서류 지원과 합격 그리고 과제에 사용할 기술 선정에 대해서 이야기했습니다.

이번 2부에서는 과제를 진행하면서 고민한 점과 종합적인 후기에 대해서 이야기할 예정입니다.

고민

기술 선정 이후 했던 가장 큰 고민은 “어떻게 하면 프로그램을 잘 만들 수 있을까?” 였다.

합격을 위해서는 과제에서 요구하는 바를 모두 만족하는 프로그램을 만들어야 한다. 하지만 단순히 요구사항을 만족하는 프로그램을 만드는 것만으로는 합격이 어렵다고 생각했다. 많은 사람들이 원하는 기업인 만큼 뛰어난 사람들이 해당 전형에 지원했을 것이기 때문에 합격을 위해서는 요구사항을 만족할 뿐 아니라 잘 만들어진 프로그램을 제출할 필요가 있다고 생각했다.

그렇다면 잘 만들어진 프로그램(좋은 프로그램)이란 무엇일까?

잘 만들어진 프로그램(좋은 프로그램)을 정의하는 다양한 요소가 있겠지만 지금까지의 경험을 바탕으로 개인적으로 생각하는 가장 중요한 요소는 바로 유지 보수성확장성이다. 그동안 다양한 서비스를 만들어 오며 배운 것이 있다면 바로 상황에 따라 프로그램은 계속 변경된다는 것이다. 제작 중에 요구사항이 변경되거나 완성 이후에 새로운 기능이 추가되는 등 프로그램은 항상 상황에 따른 변경 사항을 맞이하게 된다. 이때 잘 만들어진 프로그램(좋은 프로그램)이라면 큰 어려움 없이 새로운 기능을 추가하고 변경할 수 있을 것이다.

따라서 잘 만들어진 프로그램이란 유지 보수와 확장에 용이한 프로그램이라는 생각을 바탕으로 과제를 진행하는데 있어 아래와 같은 원칙과 개발 방법 등을 고민하게 되었다.

SOLID

이전에 진행했던 프로젝트를 통해 잠시나마 실제로 서비스를 운영해 볼 수 있었다.

첫 배포 이후 운영 과정에서 새로운 요구 사항이 발생하였고 이를 만족하기 위해 기존 기능을 변경하고 새로운 기능을 추가할 필요가 있었다. 금방 끝낼 수 있을거라는 처음의 예상과는 달리 생각 이상의 많은 시간이 소요되었다. 무엇이 문제였을까? 당시 만들었던 버튼 컴포넌트의 구현 과정을 잠시 살펴보며 문제점을 알아보자.

아래의 링크를 통해 당시 만들었던 Button 컴포넌트의 구현 및 리팩토링 과정을 확인할 수 있다.

image초기 버튼 컴포넌트 디자인

초기 버튼 컴포넌트는 스타일에 따른 구분만 존재할 뿐 역할과 책임에 따른 뚜렷한 구분이 존재하지 않았다.

따라서 처음에는 아래와 같이 props를 통해 디자인을 적용할 수 있는 형태의 버튼 컴포넌트를 만들게 되었다.

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
// Button.tsx
interface ButtonProps extends ComponentProps<'button'> {
  width?: number;
  height?: number;
  color?: KeyOfColors;
  backgroundColor?: KeyOfColors;
}

const Button = ({
  width,
  height,
  color,
  backgroundColor,
  children,
  ...rest
}: PropsWithChildren<ButtonProps>) => {
  return (
    <StyledButton
      width={width}
      height={height}
      color={color}
      backgroundColor={backgroundColor}
      {...rest}
    >
      {children}
    </StyledButton>
  );
};

export default Button;

src/components/common/Button/Button.tsx

이러한 버튼 컴포넌트는 다음과 같은 문제점을 가졌다.

현재 버튼 컴포넌트에는 프로젝트의 디자인 시스템이 적용되어 있다. 버튼 컴포넌트는 도메인이 적용된 컴포넌트이다. 하지만 도메인에 따른 역할과 책임은 배제되어있다. 이러한 버튼 컴포넌트의 역햘과 책임은 컴포넌트가 가지는 것이 아닌 컴포넌트를 사용하는 사용자에 의해 결정된다.

따라서 버튼 컴포넌의 역할과 책임을 이해하기 위해서는 항상 주변 컨텍스트를 이해해야만한다. 이것은 코드의 가독성을 해치고 생산성을 떨어트린다.

따라서 이러한 문제를 해결하기 위해서는 버튼 컴포넌트가 역할과 책임을 가질 수 있게 만들어야 한다.

버튼 컴포넌트 개선에 대한 상세한 내용은 추후에 따로 작성할 예정이다.

위와 같이 버튼 컴포넌트의 개선 과정을 겪으며 컴포넌트의 역할과 책임에 대해서 고민하기 시작했다. 그리고 곧 이러한 고민이 SRP(Single Responsibility Principle)와 닮아있다는 것을 알게 되었다. SRP에 대해 고민하기 시작하면서 개념적으로만 알고있던 SOLID 원칙에 대해 보다 자세히 공부하게 되었다.

아래의 글들을 통해 SOLID, 프론트엔드에서의 SOLID에 대해 보다 깊게 이해할 수 있었다.

과제를 진행하며 모든 원칙을 만족시킬 수는 없었지만 적어도 SRP(Single Responsibility Principle)를 충실히 따르기 위해 노력했다. 아래는 SRP를 지키기위해 가장 먼저 고민했던 프로젝트의 폴더 구조이다.

과제의 내용이 노출될 수 있는 부분은 domain으로 표현하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
📦src  
 ┣ 📂apis  
 ┣ 📂app  
 ┣ 📂components    
 ┃ ┣ 📂domain01  
 ┃ ┣ 📂domain02  
 ┃ ┣ 📂domain03
 ┃ ┣ 📂provider  
 ┃ ┗ 📂ui  
 ┣ 📂hooks  
 ┣ 📂mocks  
 ┣ 📂queries  
 ┣ 📂services   
 ┣ 📂stores    
 ┣ 📂styles  
 ┣ 📂types  
 ┗ 📂utils  

각각의 폴더는 다음과 같은 역할과 책임을 가진다.

  • apis: Axios를 이용한 데이터 패칭과 관련된 파일을 모아 놓은 디렉토리
  • app: 어플리케이션의 Page를 모아 놓은 디렉토리, Next의 App Router 사용시 페이지를 구현하기 위해 app 디렉토리를 사용해야만 한다.
  • components: 어플리케이션에서 사용하는 모든 컴포넌트를 모아 놓은 디렉토리. 도메인에 구애 받지 않는 UI 컴포넌트는 UI 폴더에, 도메인과 관련된 컴포넌트는 따로 적절한 도메인 폴더에 모아 놓는다.
  • hooks: 어플리케이션의 비지니스 로직 중 custom hook을 모아 놓은 디렉토리
  • mocks: MSW를 이용한 mock 서버와 관련된 파일을 모아 놓은 디렉토리
  • queries: 서버 상태 관리를 위한 React-Query와 관련된 파일을 모아 놓은 디렉토리
  • services: 어플리케이션의 비지니스 로직 중 hook이 아닌 파일을 모아 놓은 디렉토리
  • stores: 전역 상태 관리를 위한 Zustand와 관련된 파일을 모아 놓은 디렉토리
  • styles: 스타일과 관련된 파일을 모아 놓은 디렉토리
  • types: 전역 혹은 중복으로 사용되는 타입을 모아 놓은 디렉토리
  • utils: 유틸 함수를 모아 놓은 디렉토리

Layered Architecture

위와 같은 폴더 구조를 구성한 이유에는 SRP(Single Responsibility Principle)를 따르기 위함 뿐 아니라 Layered Architecture와도 연관이 있다.

처음 Layered Architecture에 관심을 가지게 된 계기는 함수형 프로그래밍을 공부하기 시작하면서부터였다. 당시 쏙쏙 돌아오는 함수형 코딩이라는 책을 통해 함수형 프로그래밍에 대해 공부하고 있었는데 여기서 계층형 설계 파트를 통해 소프트웨어를 계층으로 구성하는 Layered Architecture에 대해 알게되었다.

이후 다음의 글들을 통해 Layer Architecture에 대한 이해를 높일 수 있었다.

Layered Architecture를 통해 계층화를 하게 되면 관심사를 분리할 수 있을 뿐 아니라 각 모듈 교체테스트에서 이점을 가질 수 있다. 이러한 특징 덕분에 Layered Architecture를 사용하는 것이 유지 보수와 확장성에서 이점을 가질 수 있다고 생각하였고, 다음과 같은 계층을 구성하게 되었다.

graph TD
    A["Page Layer"] --> B["Component Layer"]
    B --> C["Business Layer
    (Hooks and Services)"]
    C --> D["Store Layer
    (Queries and Stores)"]
    D --> E["Utility Layer
    (APIs, Utils, Mocks, ...ETC)"]

계층 구성에는 다음의 자료를 참고하였다.

Business Layer에서 Utility Layer를 참조하거나 Component Layer에서 Store Layer를 참조하는 등 Opened Layer를 만들어 완벽히 Layered Architecture를 따르지는 못했지만 계층을 구성하고 각 계층에 맞는 적절한 함수의 위치를 고민하면서 함수가 가지는 역할과 책임에 대해 더욱 깊게 고민할 수 있게되었다.

CDD (Component-Driven Development)

계층형 설계를 통해 프로젝트의 구조를 구성한 이후 어떻게 컴포넌트를 개발할 것인지 생각해보았다.

평소 컴포넌트를 개발할 때 주로 바텀-업 방식을 이용한다. 이러한 바텀-업 방식은 점진적으로 결합(조립) 하여 컴포넌트를 개발하기 때문에 관심사를 분리하고 작은 단위의 테스트하기 용이한 컴포넌트를 만들 수 있게 해준다.

image

Storybook을 이용하면 이러한 바텀-업 방식의 개발 방법을 적용하여 독립적으로 컴포넌트를 구성할 수 있을 뿐 아니라 자연스럽게 CDD(Component-Driven Development) 를 적용할 수 있게 된다. CDD를 통해 각 컴포넌트의 역할과 책임에 대해 고민하고 결과적으로 더 나은 컴포넌트를 설계할 수 있게 되었다.

각 컴포넌트에 대한 story를 만드는데에는 보일러플레이트가 필요했기 때문에 코드 스닛펫(code snippets)을 이용하여 스토리북 작성의 효율성을 높였다.

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
{
	"storybook-template": {
		"prefix": "sbtemplate", 
		"scope": "typescriptreact",
		"body": [
			"import type { Meta, StoryObj } from '@storybook/react';",
			"",
			"import { ${2:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}} } from './${1:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}}';",

			"",
			"const meta: Meta<typeof ${1:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}}> = {",
			"  title: '${1:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}}',",
			"  component: ${1:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}},",
			"};",
			"",
			"export default meta;",
			"",
			"type Story = StoryObj<typeof ${1:${TM_FILENAME_BASE/(.*)(.stories)/${1:/pascalcase}/gi}}>;",
			"",
			"export const Default: Story = {",
			"  args: {}",
			"};",
			""
		],
		"description": "Creates a Storybook story template based on the filename."
	}
}

선언형 프로그래밍 (Declarative Programming)

코드가 어떻게 동작해야하는지에 대해 설명하는 명령형 프로그래밍(Imperative Programming) 과 대비되는 개념인 선언형 프로그래밍(Declarative Programming) 은 어떻게(How)가 아닌 무엇(What) 인지에 초점을 맞춘 프로그래밍 방식으로 추상화 수준을 높여 전체적인 코드의 가독성을 높일 수 있는 프로그래밍 방법이다.

실제로 React는 이러한 선언형 프로그래밍을 적용하여 상호작용이 많은 UI를 만들 때 생기는 복잡함을 줄이고 캡슐화된 컴포넌트를 통해 쉽게 UI를 만들 수 있게 만들어 준다.

바텀-업 방식으로 컴포넌트를 개발하다 보면 아래에서 만든 컴포넌트를 통해 더욱 큰 컴포넌트 혹은 페이지를 구성하게 된다. 이때 적절한 역할과 책임을 통해 컴포넌트를 나누었다면 아래와 같이 가독성 높은 UI 로직을 구성할 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
function MainPage() {
// ...
return ( 
	<>
		<Header />
		<MainView />
		<SubView />
		<Footer />
	</>
	);
}

각 컴포넌트가 동일한 추상화 수준을 유지함으로써 더욱 가독성을 높일 수 있다.

실제로 컴포넌트를 합성하여 복잡한 컴포넌트를 구성할 때 레이아웃을 위해 div 태그를 통해 스타일을 적용해야하는 상황이 빈번히 발생하게된다. 이러한 경우 아래와 같이 다른 추상화 수준을 가지게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function MainPage() {
// ...
return ( 
	<>
		<Header />
		<div style={...}>
			<MainView />
			<SubView />
		</div>
		<Footer />
	</>
	);
}

아래와 같이 레이아웃과 관련된 부분을 컴포넌트로 만들면 재사용성을 높일 수 있을 뿐 아니라 동일한 추상화 수준을 통해 가독성 또한 높일 수 있게 된다.

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
50
51
52
// Flex.tsx
import { assignInlineVars } from '@vanilla-extract/dynamic';
import type { PropsWithChildren } from 'react';

import {
  alignItems,
  flexDirection,
  flexStyles,
  flexWidth,
  flexWrap,
  justifyContent,
} from './Flex.css';

interface FlexProps extends React.HTMLAttributes<HTMLDivElement> {
  direction?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
  justify?:
    | 'flex-start'
    | 'flex-end'
    | 'center'
    | 'space-between'
    | 'space-around'
    | 'space-evenly';
  align?: 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch';
  wrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
  width?: string;
}

export const Flex = ({
  children,
  direction = 'row',
  justify = 'flex-start',
  align = 'stretch',
  wrap = 'nowrap',
  width = '100%',
  className: userStyles,
  ...props
}: PropsWithChildren<FlexProps>) => {
  const dynamicStyle = assignInlineVars({
    [flexWidth]: width,
    [flexDirection]: direction,
    [justifyContent]: justify,
    [alignItems]: align,
    [flexWrap]: wrap,
  });

  return (
    <div className={`${flexStyles} ${userStyles}`} style= {...props}>
      {children}
    </div>
  );
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Flex.css.ts
import { createVar, style } from '@vanilla-extract/css';

export const flexWidth = createVar();
export const flexDirection = createVar();
export const justifyContent = createVar();
export const alignItems = createVar();
export const flexWrap = createVar();

export const flexStyles = style({
  display: 'flex',
  width: flexWidth,
  flexDirection,
  justifyContent,
  alignItems,
  flexWrap,
});

이렇게 만든 레이아웃 컴포넌트를 이용하면 아래와 같이 가독성 높은 UI 코드를 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MainPage() {
// ...
return ( 
	<>
		<Header />
		<Flex>
			<MainView />
			<Space />
			<SubView />
		</Flex>
		<Footer />
	</>
	);
}

과제 전형 탈락, 아쉬웠던 부분

아쉽게도 과제 전형에서 탈락하게 되었다.

피드백으로 받은 주 원인은 기능 구현에서 발생한 버그들이었다.

피드백 외에 개인적으로 아쉬웠던 부분은 다음과 같다.

새로운 기술 사용

Next14, Zustand, MSW와 같은 기존에 사용하지 않았던 기술들을 단시간에 학습하고 적용하려다보니 과제 2일차가 되어서야 본격적으로 개발을 시작할 수 있었다.

Next14와 MSW를 함께 사용하면서 수 많은 문제들에 부딪혔고 이 문제들을 해결하기 위해 많은 글과 이슈, PR등을 확인하는데 많은 시간을 사용해야만 했다.

새로운 기술에 대한 욕심을 조금만 버리고 기능 구현에 집중했더라면 발생한 버그들을 어느정도 해결하고 과제를 제출할 수 있지 않았을까 하는 아쉬움이 남는다.

완벽함 추구

새로운 기술들을 사용하게 되면서 학습에 많은 시간을 소모하게 되었기 때문에 남은 시간을 잘 분배하여 과제를 진행하는 것이 중요했다. 하지만 당시 남은 시간 안에 모든 것을 할 수 있다는(?) 자신감이 있었고 처음부터 완벽함을 추구하며 과제를 진행했다.

과제를 진행하며 예상치 못한 복잡한 상황과 자잘한 문제들을 마주하게 되었고 점점 과제의 진행 속도가 느려졌다. 코드는 점점 복잡해졌고 기능 구현에는 버그들이 발생하기 시작했다. 결과적으로 모든 동작을 제대로 테스트하지 못한 채로 과제를 제출하게되었다.

능력을 벗어나는 완벽함을 추구하기 보다 가능한 선에서 과제 진행에 집중했더라면 더 나은 결과를 얻을 수 있지 않았을까 하는 아쉬움이 남는다.

결론

결과적으로 전형에 탈락하게 되었지만, 처음으로 과제 전형을 경험하며 많은 것을 배울 수 있었다.

내가 사용하는 기술들이 프로젝트에 정말 필요한 기술인지 더욱 면밀히 따질 수 있게 되었고 좋은 프로그램을 만들기 위해 따라야 할 원칙과 방법들에 대한 개념을 정리할 수 있었다.

이러한 경험을 바탕으로 다음에는 더 나은 결과를 낼 수 있기를 바라며 회고를 마친다.

This post is licensed under CC BY 4.0 by the author.