온룸 코드 리팩토링하기 - 1편 : 안티 닌자 코드
TL;DR
- 닌자 코드 패턴의 존재를 알았고, 안티 닌자 패턴으로 기존 코드(ownroom.link)를 개선해보고 싶었습니다.
- 첫 진입점인 Home컴포넌트에서 banner +1,2,3,4,5로 무분별한 네이밍을 발견했습니다
- 이 부분이 문제인 줄 알았는데요, 진정한 닌자 코드는 따로 있었습니다! 컨테이너가 너무 많은 역할을 맡았습니다.
- 기존의 Container & absolute 방식 대신, 플러터에서 아이디어를 얻어 컴포넌트 위치를 체계화시켰습니다.
최근 들어 사이드 프로젝트가 하고 싶어졌습니다. 그런데 막상 무언가를 하려고 보니 어떤 걸 새로 만드는 것보다 만들어 둔 것을 일단 다듬고 코딩 스타일을 가다듬는 것이 제 실력 향상에 도움이 될 것이라는 판단이 들었습니다. 좋은 코드와 나쁜 코드의 기준은 이전에 적어둔 코드들이 앞으로의 코드들에 도움이 되는지 짐이 되는지 라고 생각했습니다. 그런데 지금은 예전에 쌓아둔 코드가 쳐다보고 싶지도 않은 상황이기 때문에(...) 짐이 된다는 것을 실감했고 고쳐보려 합니다.
리팩토링 방향성 잡기
최근 흥미로운 글이나 책, 강의들을 몇 가지 접했는데, 그 내용들을 반용하여 리팩토링해보고 합니다. 자료 목록은 아래와 같습니다.
- 테오의 프론트엔드 - 다시 쓰는 함수형 프로그래밍
- 쏙쏙 들어오는 함수형 코딩
- 웹에 날개를 달아주는 웹 성능 최적화 기법
- 닌자 코드 패턴
- 테오의 프론트엔드 - Atomic Design Pattern
그리고 위 글들을 읽으면서 세운 리팩토링의 목표는 다음과 같습니다.
- 안티 닌자코드 패턴으로 코딩하자. 특히, 좋은 네이밍을 계속 생각해보자.
- 효과적인 디렉토리 구조를 생각해보자.
- 함수형 코딩 패턴으로 상태를 관리해보자.
- 기존 웹페이지 성능을 측정하고 최적화를 해서 성능을 개선시켜보자.
이렇게 한 번 리팩토링하는 경험을 거치고 난다면 다음에는 어떤 프로젝트를 하던 안좋은 습관을 최소화하고 깔끔한 코딩을 할 수 있다고 생각했습니다.
그럼 시작해볼까요?
1편. 안티 닌자코드 패턴으로 코딩하기
닌자코드 패턴에 관한 글 을 요약하자면 아래와 같습니다.
- 코드 짧게 쓰기 (복잡한 삼항연산자 등, 압축코드 쓰기..)
- 글자 하나만 사용하기 (의미없는 짧기만 한 변수 쓰기)
- 약어 사용하기 (과도한 모음제거로 변수 파악 어렵게 하기)
- 포괄적인 명사 사용하기 (obj, data, value, elem, item + 자료형 등 의미 없는 네이밍 쓰기)
- 철자가 유사한 단어 사용하기 (data , date ..)
- 동의어 사용하기
- display, show, render 등 의미가 같으면 그때그때 마음에 드는 단어쓰기
- 의미가 달라도 같은 단어쓰기 printPage, printText
- 변수 재사용하기. 같은 이름에 변수 재할당하기
- 재미로 언더스코어 사용하기
- 과장 형용사 사용하기
- 함수 외부와 내부에 동일한 변수 이름 쓰기
- 부작용이 있는 코드 작성하기(순수함수에 사이드이펙트 추가하기)
- 함수에 다양한 기능 넣기
적고 나니 하나같이 주옥같은(...), 개발자를 열받게 할 만한 요소들이라는걸 다시금 느꼈습니다. 그렇다면 안티닌자패턴은 어떻다고 할 수 있을까요? 위 요소들을 반대로 뒤집어 보았습니다.
- 성능에 영향을 주는 요소가 아니라면 코드 풀어서 쓰기
- 변수명이 길어지더라도 풀어서 쓰기
- 모음 제거하지 않기
- 단순히 포괄적인 명사보다, 변수명이 길어지더라도 구체화해주기
- 철자가 유사한 단어 피하기
- 비슷한 역할을 하는 단어는 하나로 정해서 쓰기
- 꼭 필요하지 않다면 재할당 금지.
const
사용하고, 객체불변성 지키기- 의미없는 언더스코어 금지, 변수명에는 camelCase를 쓰고, 내부적으로 관리되는 변수에만 접두어로 언더스코어 붙이기
- 과장 형용사 사용 금지. 의미없는 형용사 사용금지
- 함수 외부, 내부 변수이름 분리하기
- 함수는 웬만하면 순수함수.
- 하나의 함수는 하나의 역할만 하도록 하기
이렇게 뒤집어서 적고 나니까, 클린코드에 한발짝 다가선 것 같은 느낌이 들었습니다.
실제로 코드를 개선해보기 (⚠️안구테러주의..!!!)
이제 실제로 작업을 해봅시다. 우선 홈화면을 봅시다. 홈화면은 이렇게 생겼습니다.
보시다시피 아주 기다랗게 생겼습니다. 그리고 이를 나타낸 코드는 아래와 같습니다.
import React from 'react';
import { PortfolioPreviewList } from '../consultantPortfolio/components/portfolioList';
import { ConsultantTitle } from './components/consultantTitle';
import { Banner1 } from './components/banner1';
import { Banner2 } from './components/banner2';
import { Banner3 } from './components/banner3';
import { Banner4 } from './components/banner4';
import { Banner5 } from './components/banner5';
import { Banner6 } from './components/banner6';
const Home = () => (
<>
<Banner1 />
<Banner2 />
<Banner3 />
<Banner4 />
<ConsultantTitle />
<PortfolioPreviewList />
<Banner5 />
<Banner6 />
</>
);
export default Home;
무엇이 잘못되었을까요?
새로운 변수명이 더는 떠오르지 않는다면 어떻게 해야 할까요?
data1, item2, elem5
처럼 옆에 숫자를 붙여주면 됩니다.-닌자코드
그렇습니다. 저는 닌자가 될 천부적인 재능이 있었나 봅니다!
... 가 아니고, Banner1, Banner2, Banner3 ... 이렇게 네이밍을 해 놓은 부분이 닌자코드 패턴의 data1, data2 ... 와 상당히 비슷한 느낌을 준다는 점이 상당히 저를 불편하게 했습니다! 또, PortfolioPreviewList
라는 이름도 좋지 않은 것 같아요. 우선 현재 상태에 Navbar와 Footer를 제외하고 컴포넌트 이름을 붙여보면 이렇게 됩니다.
이 글을 읽는 독자 여러분의 생각은 다를 수도 있겠지만, 이렇게 이름을 붙여놓고 봤을 때 Banner
는 그렇게까지 이상한 네이밍은 아니라는 생각이 들었어요. 오히려 Banner
보다 눈에 거슬린 건 ConsultantTitle
과 PortfolioPreviewList
를 합쳐서 PortfolioGrid
라는 하나의 컴포넌트로 묶으면 더 좋지 않았을까 싶었어요.
Banner + 숫자 패턴이 괜찮다고 생각한 이유는, 일단 'web banner'라고 구글에 쳤을 때 이 컴포넌트들과 비슷한 이미지들이 많이 나와서 적절한 단어 선택이라는 생각이 들었습니다. 숫자를 붙인 것도 위에서부터 차례로 붙였고 각 변수가 한 번씩만 쓰이기 때문에 그렇게 가독성을 해치지 않는다고 생각했어요. 닌자코드에서 말하는 data1, data2 등이 혼재를 주는 이유는 무분별하게 정의한 변수들이 반복적으로 등장하는데 변수가 무엇을 의미하는지 모르는 상황이 발생하고 계속 선언된 부분을 다시 읽게 만드는 경우라고 생각했습니다. 하지만 이 경우는 배포된 화면과 순서대로 숫자를 부여했고, 내용이 궁금하면 타고 들어가서 보면 되니까 닌자코드는 아니라고 판단했습니다.
그런데 이 부분에서 나름대로 합리화를 했음에도 불구하고, 제 자신이 제 코드가 계속 불편하게 느껴졌습니다. 왜 그랬을까요? 그건 첫 컴포넌트인 Banner1.tsx
를 열어보니 알 수 있었습니다.
Home/Banner1.tsx
export const Banner1 = () => (
<>
<Container height="440px" style={Banner1BackgroundCSS}>
<Container width="1920px" position="relative">
<Img
width="1920px"
height="600px"
src={process.env.PUBLIC_URL + 'img/home/home1.png'}
style={Banner1ImgCSS}
/>
<Container width="1136px" position="absolute">
<Text className="ENHeadline-0 graywhite" style={Banner1Text1CSS}>
Home styling with ownroom
</Text>
<Text className="KRBody-1 graywhite" style={Banner1Text2CSS}>
누구나 나다운 집에 살 수 있도록
</Text>
<Text className="ENBody-1 graywhite" style={Banner1Text3CSS}>
Make your own room
</Text>
</Container>
</Container>
</Container>
</>
);
const Banner1BackgroundCSS: CSSProperties = {
backgroundColor: 'var(--brand-yellow-001)',
};
const Banner1ImgCSS: CSSProperties = {
position: 'absolute',
};
const Banner1Text1CSS: CSSProperties = {
position: 'absolute',
top: '58px',
left: 0,
};
const Banner1Text2CSS: CSSProperties = {
position: 'absolute',
top: '181px',
left: 0,
};
const Banner1Text3CSS: CSSProperties = {
position: 'absolute',
top: '217px',
left: 0,
fontStyle: 'normal',
};
문제점이 보이시나요? 글로벌에서 정해준 선택자(클래스)와 CSSProperties를 섞어서 쓰는 모습을 볼 수 있습니다. 글자 색이나 크기 등은 전역 css에서 클래스로 내려받지만, 배경색이나 글자의 위치 등은 컴포넌트마다 별개로 정의합니다. 이는 유지보수와 가독성 모두를 해친다고 생각했습니다. 예를 들면 "누구나 나다운 집에 살 수 있도록"이라는 텍스트 컴포넌트는 색은 글로벌스타일에서 가져오고, 위치는 해당 모듈에서 정의해서 씁니다. 컴포넌트의 위치를 정하는 방식이 absolute로 되어 있기 때문에 모든 컴포넌트에게 위치(절대좌표와 여백 등) 정보를 달아줘야 하고, 이 때문에 글로벌 스타일과 더불어 이중으로 스타일을 해주게 됩니다. 즉, 무조건적으로 absolute로 위치를 설정하는 방식이 잘못되었다는 것을 깨달았습니다. 이것이 원인이 되어 'css를 주는 규칙이 획일화되어있지 않다'는 느낌 때문에 저를 불편하게 했던 것입니다.
요소 위치 규칙 재정비하기
기존 코드에서는 반응형 컨테이너 를 만들어서 컨테이너 타입, 정렬방식, 크기 등을 정했습니다. 성질이 다른 컨테이너를 여러 겹 겹치고, 마지막에는 무조건 absolute로 하위 요소들의 위치를 관리하는 방식으로 요소들의 위치를 관리했습니다. 그리고 앞서 이 "Container & absolute" 방식에 문제가 있음을 알았습니다. 놀랍게도, 이 문제는 이 글의 주제인 닌자 코드 패턴
과도 관련이 있습니다.
함수의 기능을 확장합시다. 함수가 할 수 있는 동작을 함수 이름에 한정 짓지 맙시다.
-닌자 코드
"컨테이너" 라는 컴포넌트에 너무 많은 기능을 넣다보니 결국은 크기가 정해진 div 정도로 사용하게 되었고, 마지막에 강제로 absolute를 이용해 위치를 잡아주는 식으로 코드가 흘러갈 수 밖에 없었던 것입니다. 그래서 이제부터는 새로운 방법으로 요소 위치를 관리해야겠다고 생각했습니다. 방법은 아래 네 가지 컴포넌트를 기반으로 관리하는 것입니다.
Components
- Row : width 100% 인 컨테이너, height를 입력받고 하위요소가 가로로 배치
- Col : height 100% 인 컨테이너, width를 입력받고 하위 요소가 세로로 배치
- Center : width, hegiht 100%인 컨테이너, 요소가 정 중앙에 배치
- Spacing : width 또는 height를 입력받습니다. 나머지 한 값은 100%가 됩니다.
이제 Container 와 Absolute가 아니라, 위 네 가지 요소를 블럭처럼 쌓아서 요소들의 위치를 정할 수 있습니다. 최근에 Flutter 공부를 하고 있는데, Flutter가 위와 같은 방식으로 요소를 배치하는 것을 알고 적용해 봤습니다. 위 내용을 실제로 구현하면 아래와 같이 됩니다.
Row.tsx
import styled, { css, CSSProperties } from "styled-components";
interface RowProps {
height: number;
mainAxisAlignment?:
| "center"
| "start"
| "end"
| "flex-start"
| "flex-end"
| "left"
| "right";
crossAxisAlignment?:
| "normal"
| "flex-start"
| "flex-end"
| "center"
| "start"
| "end"
| "self-start"
| "self-end"
| "baseline"
| "stretch";
children: JSX.Element | JSX.Element[];
style?: CSSProperties;
}
export const Row = ({
height,
children,
mainAxisAlignment = "start",
crossAxisAlignment = "normal",
style = defaultStyle,
}: RowProps) => {
return (
<Container
height={height}
mainAxisAlignment={mainAxisAlignment}
crossAxisAlignment={crossAxisAlignment}
style={style}
>
{children}
</Container>
);
};
const defaultStyle: CSSProperties = {};
const Container = styled.div<RowProps>`
height: ${({ height }) => height}px;
display: flex;
flex-direction: row;
justify-content: ${({ mainAxisAlignment }) => mainAxisAlignment};
align-items: ${({ crossAxisAlignment }) => crossAxisAlignment};
width: 100%;
`;
Col.tsx
import styled, { CSSProperties } from "styled-components";
interface ColProps {
width: number;
mainAxisAlignment?:
| "center"
| "start"
| "end"
| "flex-start"
| "flex-end"
| "left"
| "right";
crossAxisAlignment?:
| "normal"
| "flex-start"
| "flex-end"
| "center"
| "start"
| "end"
| "self-start"
| "self-end"
| "baseline"
| "stretch";
children: JSX.Element | JSX.Element[];
style?: CSSProperties;
}
export const Col = ({
width,
children,
mainAxisAlignment = "start",
crossAxisAlignment = "center",
style = defaultStyle,
}: ColProps) => {
return (
<Container
width={width}
mainAxisAlignment={mainAxisAlignment}
crossAxisAlignment={crossAxisAlignment}
style={style}
>
{children}
</Container>
);
};
const defaultStyle: CSSProperties = {};
const Container = styled.div<ColProps>`
width: ${({ width }) => width}px;
display: flex;
flex-direction: column;
justify-content: ${({ mainAxisAlignment }) => mainAxisAlignment};
align-items: ${({ crossAxisAlignment }) => crossAxisAlignment};
height: 100%;
`;
Center.tsx
import styled, { CSSProperties } from "styled-components";
interface CenterProps {
children: JSX.Element | JSX.Element[];
style?: CSSProperties;
}
export const Center = ({ children, style = defaultStyle }: CenterProps) => {
return <Container style={style}>{children}</Container>;
};
const defaultStyle: CSSProperties = {};
const Container = styled.div<CenterProps>`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
Spacing.tsx
import styled, { css } from 'styled-components';
interface SpacingProps {
width?: number;
height?: number;
}
export const Spacing = ({ width, height }: SpacingProps) => {
return <Container width={width} height={height}></Container>;
};
const Container = styled.div<SpacingProps>`
${({ width }) => {
if (width) {
return css`
width: ${width}px;
`;
} else {
return css`
width: 100%;
`;
}
}}
${({ height }) => {
if (height) {
return css`
height: ${height}px;
`;
} else {
return css`
height: 100%;
`;
}
}}
`;
이 4가지 조합이 있으면 position:absolute
나 margin
, padding
등을 되는대로 써가며 구현하는 것이 아니라, 체계적으로 위치를 관리할 수 있고, 코드도 한결 깔끔해 집니다. 이를 바탕으로 리팩토링한 Banner.tsx
를 봅시다.
Banner1.tsx : before
import { CSSProperties } from 'react';
import { Container } from '../../../common/container';
import { Img } from '../../../common/img';
import { Text } from '../../../common/text';
export const Banner1 = () => (
<>
<Container height="440px" style={Banner1BackgroundCSS}>
<Container width="1920px" position="relative">
<Img
width="1920px"
height="600px"
src={process.env.PUBLIC_URL + 'img/home/home1.png'}
style={Banner1ImgCSS}
/>
<Container width="1136px" position="absolute">
<Text className="ENHeadline-0 graywhite" style={Banner1Text1CSS}>
Home styling with ownroom
</Text>
<Text className="KRBody-1 graywhite" style={Banner1Text2CSS}>
누구나 나다운 집에 살 수 있도록
</Text>
<Text className="ENBody-1 graywhite" style={Banner1Text3CSS}>
Make your own room
</Text>
</Container>
</Container>
</Container>
</>
);
const Banner1BackgroundCSS: CSSProperties = {
backgroundColor: 'var(--brand-yellow-001)',
};
const Banner1ImgCSS: CSSProperties = {
position: 'absolute',
};
const Banner1Text1CSS: CSSProperties = {
position: 'absolute',
top: '58px',
left: 0,
};
const Banner1Text2CSS: CSSProperties = {
position: 'absolute',
top: '181px',
left: 0,
};
const Banner1Text3CSS: CSSProperties = {
position: 'absolute',
top: '217px',
left: 0,
fontStyle: 'normal',
};
Banner1.tsx : after
import { CSSProperties } from "react";
import { Col } from "../../../common/Col";
import { Row } from "../../../common/Row";
import { Spacing } from "../../../common/Spacing";
import { Text } from "../../../common/Text";
export const Banner1 = () => (
<>
<Row height={440} mainAxisAlignment={"center"} style={BackgroundColor}>
<Col width={1920} style={BackgroundImage}>
<Col width={1136} crossAxisAlignment={"start"}>
<Spacing height={58} />
<Text className="ENHeadline-0 graywhite">
Home styling with ownroom
</Text>
<Spacing height={58} />
<Text className="KRBody-1 graywhite">
누구나 나다운 집에 살 수 있도록
</Text>
<Text className="ENBody-1 graywhite">Make your own room</Text>
</Col>
</Col>
</Row>
</>
);
const BackgroundImage: CSSProperties = {
backgroundImage: `url(${process.env.PUBLIC_URL + "img/home/home1.png"})`,
backgroundSize: "cover",
};
const BackgroundColor: CSSProperties = {
backgroundColor: "var(--brand-yellow-001)",
};
어떠신가요? 차이가 느껴지시나요? 단순히 라인 수가 짧아졌을 뿐 아니라, 어느 부분에 CSSProperties
가 개입해야는지가 더욱 명확해졌습니다. 개입 지점이 명확해지면서 네이밍도 훨씬 간결해졌구요. 또, Container에게 주어져있던 여러 역할들을 쪼갬으로써 코드를 처음 보는 사람도 어느 정도 레이아웃이 머릿속으로 그려지도록 하는 효과가 있다고 생각합니다.
이런 방식으로 나머지 컴포넌트들도 리팩토링하면, 위치 부분을 체계화시킬 수 있고 앞으로 프로젝트를 진행할 때도 보일러 플레이트로 적용하면 좋겠다고 생각했습니다!
회고
이번 글을 쓰면서, 앞으로도 글을 많이 써야겠다고 느꼈습니다. 이 글도 여러 번 지우면서 쓴 글인데, 글을 쓰는 과정에서 자신의 논지가 정리되는 느낌이 들어 좋았습니다. 이전 글에서는 반응형 Container를 만들고 좋아했지만, 이제와서보니 그것이 문제가 있는 코드였습니다. 물론 지금 쓴 코드도 문제가 있을 수 있겠지만, 이렇게 생각을 정리해둔다면 나중에 다시 볼 때도 '과거의 나'를 이해해줄 수 있을 것이라는 생각이 들었습니다. 다음 편에서는 폴더 구조에 대해서 탐구해보겠습니다. 긴 글 읽어주셔서 감사드립니다. 😀
'Tech Note' 카테고리의 다른 글
잘하는 프론트엔드 개발자란? (0) | 2022.09.03 |
---|---|
디자이너의 요구사항에 맞춰 개발하기 (0) | 2022.02.05 |
댓글
이 글 공유하기
다른 글
-
잘하는 프론트엔드 개발자란?
잘하는 프론트엔드 개발자란?
2022.09.03 -
디자이너의 요구사항에 맞춰 개발하기
디자이너의 요구사항에 맞춰 개발하기
2022.02.05