프로젝트 초기단계. 빠르게 유저 반응을 보기 위해서 많은 스타트업에서는 빠르게 제품을 개발한다. 정리되지 않은 코드 위에 새로운 기능들이 얹혀지고, 또한 어떤 코드들은 더 이상 사용하지 않는 기능이 되어 한쪽 저편에 먼지처럼 자리하게 된다.
개발 인원이 늘어나면서 내가 만든 코드와 니가 만든 코드는 점점 뒤죽박죽이 되고 얽히고 얽힌다. 두달 전에 <CounterInput />
이라는 컴포넌트를 내가 만들었지만 오늘 <NumberCounter />
라는 같은 기능의 컴포넌트가 다른 사람의 손에 의해 다시 만들어진다. 아니면 미래의 나에 의해 다시 만들어지기도 한다.
🙅🏻♀️ 이건 뭔가 잘못된 것 같아!
Something-Driven-Development 라는 단어는 TDD밖에 몰랐었고 그 마저도 빠르게 제품을 개발하는 과정에서 프로젝트에 도입하자는 의견도 내기 어려웠다. 당시에는 Typescript를 도입하는 것 조차도 설득이 필요했다. 아무래도 Type 작성에 대한 추가 리소스가 필요할 수 밖에 없기 때문이다.
빠르게 제품을 개발해야 한다는 스타트업의 특성을 모르는것도 아니고 동의하지 않는 것도 아니다. 하지만 어떠한 시점이 지나면, 여러 개발자들에 의해 만들어진 마구마구 얽히고 섥힌 코드들은 감당하기 힘들 정도가 될 수도 있다. 조금만 건드려도 여기저기 버그들이 터져 나온다. 그 시점은 생각보다 빨리 올지도 모른다.
그 시점을 지나면 TDD나 Typescript등을 적용 했을 때 오히려 빠른 유지보수와 새로운 기능의 빠른 추가를 할 수 있음은 물론이고 신뢰 높은 안정적인 제품을 만들어준다.
이제 이 글의 주제인 문서 주도 개발에 대해서 이야기 해보고자 한다. (DDD는 Domain Driven Development의 약자로 더 알려져 있다. 그것과는 다른 얘기이다.)
UI를 개발할 때 문서 주도 개발은 Storybook Driven Development
또는 Style-Guide Driven Development
와도 같은 말이 될 수 있을 것 같다. 구글에 위 키워드로 검색해보면 관련 글들을 찾을 수 있다. 스타일가이드와 스토리북 또한 문서의 형태이기에 문서 주도 개발의 한 부분이라고 생각할 수 있다.
컴포넌트를 제작할 때 레고를 조립하는 것과 비교하곤 한다. 어린이용 레고는 설명서가 없어도 조립이 가능하지만, 스타워즈 우주선은 설명서 없이는 힘들다. 이 것과 마찬가지로 초기 단계의 어플리케이션은 코드가 많지 않고 복잡하지 않아서 문서 없이도 쉽게 파악이 가능하다. 하지만 수개월, 수년동안 쌓인 복잡한 로직의 애플리케이션에서는 찾고자 하는 코드를 찾기 조차 어려워 질 수 있다.
이 때 우리는 설명서가 필요하다.
스토리북을 잘 기록해 두었다면, 찾고자 하는 컴포넌트는 스토리북에서 비교적 쉽게 찾을 수 있고, 컴포넌트가 어떻게 생겼는지 잘 보여준다. 어떤 props를 받고 props에 따라서 어떤 모습으로 보여지는지에 대해서도 잘 보여준다.
API 문서도 마찬가지이다. 어떤 parameter를 받고 어떤 response를 보내주며, 어떠한 에러 상황들을 생각해 볼 수 있을지에 대해 잘 설명되어 있는 문서가 있다면, 여러 개발자들이 하나의 언어로 소통할 수 있게 된다. 심지어 기획자와 디자이너도 그렇다.
역시나 빠르게 제품을 개발하다 보면, 또는 급하게 버그를 수정하다 보면, 문서를 업데이트 하는 것을 잊어버리는 경우가 많다. 문서가 업데이트 되지 않으면 여러가지 문제가 발생하게 된다.
username
과 password
를 보내 달라고 되어있는데, 버전 업데이트 후 실제로는 username
대신 email
을 처리하고 있다면?size
props를 number
로 받도록 적혀있는데, 실제로는 5rem
등의 string
으로만 받고 있다면?당연히 예상하지 못한 에러들이 터져 나올 것이다. 이러한 일들이 잦아진다면, 일일히 API에 대해서 물어봐야 하고 개발자들간의 신뢰에도 문제가 생길지도 모른다.
생각해보라. 레고를 샀는데, 설명서와 부품이 서로 다른 상황을.
깨진 유리창 법칙과 비슷하게, 문서가 한 번 신뢰를 잃게 되면 이 문서는 업데이트 되지 않는 문서구나
라는 인식이 생겨서 아무도 신경쓰지 않게 되고 그렇게 잊혀지고 버려지게 될 것이다.
그래서 우리는 문서를 먼저 업데이트 해야한다.
실행 코드들을 먼저 작성한 뒤 테스트 코드를 작성해도 우리는 안정적인 앱을 만들 수 있다. 하지만 TDD를 통해 테스트 코드를 먼저 작성함으로써 우리는 테스트를 놓치는 실수를 줄일 수 있고 기능에 대해 더 깊게 고려할 수 있으며 과하게 설계하거나 불필요한 코드 작성을 피할 수 있다.
문서 주도 개발도 마찬가지이다. 기능을 먼저 개발 한 다음 문서를 작성해도 된다. 하지만 문서를 먼저 작성함으로써 더 깊게 기능에 대해 고려할 수 있고, 기능 개발 후 문서 작성을 미루게 되는 실수도 방지할 수 있다. 문서는 항상 최신 상태를 유지하게 된다.
부모 클래스가 업데이트 되면 상속받은 자식 클래스들도 함께 업데이트 되는 것 처럼, 문서와 코드는 깊게 연결 되어 있어야 한다.
이 것은 기획서에도 적용이 되고, Wireframe 에도 적용이 되고 디자인에도 적용이 되고 Style-Guide에도 적용이 되고 API 문서에도 적용이 된다.
새로운 기능이 추가 되면, 각 포지션의 전문가들은 아래와 같이 문서를 업데이트 해야 한다.
프론트엔드 개발자로써, 스토리북 주도 개발에 대해서 더 이야기해보고 싶다. 미래에 어떤 더 좋은 툴이 나오게 될지 모르지만. 나는 현재로써는 스토리북 주도 개발을 너무도 중요한 개발 습관이라고 생각하고 있다.
항상 최신상태로 유지 된 스토리북 문서는, 여러명의 프론트엔드 개발자간의 소통과 또한 디자이너와의 소통에서 높은 신뢰를 준다. 잘 관리되고 있는 신뢰도 높은 스토리북은 개발자들이 버려두지 않을 것이다.
스토리북에 존재하지 않은 컴포넌트는 존재하지 않아야 하며
스토리북에 존재하는 컴포넌트는 반드시 코드로 존재해야 한다.
나는 UI를 개발할 때에는 React 개발 서버를 켜지 않거나, 켜놓더라도 신경 쓰지 않는다. 오로지 스토리북만 보며 개발한다. 컴포넌트를 개발하기 시작하면, 개발중인 컴포넌트의 입장에서 생각하며, 어떤 이벤트를 발생할 수 있고, 어떤 props를 받아야 하는지 먼저 고민한다.
예를 들어, 이메일 로그인 폼 컴포넌트의 입장에서 생각해보자.
등의 요소들이 필요할 것 같다.
이메일과 패스워드는 props로 받아야 할까? 부모 컴포넌트에서는 이메일과 패스워드의 현재 값에 대해 알 필요가 없을 것 같다. 이메일과 패스워드에 대한 관심사는 현재 컴포넌트에서만 갖도록 하자. 불필요한 Re-render를 줄일 수 있다.
그렇다면 사용자에게 보여줄 메시지
와 제출 이벤트
만 props로 받으면 될 것 같다. 그리고 로딩
상태도 필요 하겠다. 이메일과 패스워드는 제출 이벤트에 넘겨 주도록 하자. (기능 구현은 현재 컴포넌트의 관심사가 아니기 때문에 값만 넘겨준다.)
코드를 아래와 같이 작성하고, 반드시 개발 서버가 아닌 스토리북에서 UI를 확인한다.
// EmailLoginForm.tsx
...
export interface EmailLoginFormProps {
message: string; // 사용자에게 보여줄 메시지
loading?: boolean;
onSubmit: (email: string, password: string) => void; // 제출 이벤트
}
const EmailLoginForm: React.FC<EmailLoginFormProps> = ({ message, onSubmit }) => {
const email = useInput('');
const password = useInput('');
const handleSubmit = useCallback(e => {
e.preventDefault();
onSubmit(email.value.trim(), password.value);
}, [email.value, password.value]);
return (
<form onSubmit={handleSubmit} >
<input {...email} type='email' ... />
<input {...password} type='password' ... />
<p>{message}</p>
</form>
)
}
...
// EmailLoginForm.stories.tsx
...
export default {
title: 'Organisms/EmailLoginForm',
component: EmailLoginForm,
} as Meta;
const Template: Story<EmailLoginFormProps> = (args) => <EmailLoginForm {...args} />
export const Default = Template.bind({});
Default.args = {
loading: false,
};
export const Message = Template.bind({});
Message.args = {
loading: false,
message: '유저에게 메시지를 보여줍니다.',
};
...
스토리북에서 UI가 깨지는 케이스를 작성하는 것은 상당히 중요하다고 생각한다. 예를 들어 <Button />
컴포넌트를 작성할때 우리는 버튼에 확인
, 제출
, 취소
등과 같은 짧고 간단한 글자만 들어 갈 것이라고 예상하고 컴포넌트를 작성하는 경우가 많을 것이다. 하지만 서비스가 성공하여, 다양한 언어를 지원하게 되었다면? (세계의 많은 언어들이 한글보다 길게 표현 된다.)
버튼 뿐 아니라, 유저가 유저 닉네임을 입력할 때 100자 이상 입력해 버렸다면? 서비스 정책상, 유저 닉네임에 글자 제한을 걸 수 없다면? 튀고 싶어하는 어떤 유저들은 100자 이상의 닉네임을 작성하게 될 것이다. 많이 봤다.. 이 경우 유저 닉네임이 표시 되는 UI 에서는 긴 유저 닉네임
에 대한 처리가 필요할 것이다. 이러한 케이스를 예상하지 못했다면, 글자가 다 튀어나가 버리거나 아주 요상하게 생긴 UI가 되어 버린다.
텍스트 데이터가 없는 경우에도 마찬가지이다. 아무것도 표시하지 않을지? 아니면 placeholder를 보여줄지에 대한 고민도 필요하다.
데이터가 undefined | null
로 들어 왔을 땐? 그냥 빈 문자열
이 왔을 땐? number
만 받을 수 있는 props에 string | undefined | null
이 들어 왔을 땐? ... typescript로 타입만 지정했다고 끝나는게 아니다. 서버에서 데이터 타입이 예상과 다르게 들어오면 애플리케이션이 whiteout 될 수 있다.
이러한 케이스들을 Storybook 단에서 미리 방지하자.
아래의 예시를 보자. 버튼의 경우는 부모의 width 이상으로 너비가 overflow 된 상태에서는 2줄까지 보여주고, 2줄이 넘어가면 말줄임표로 처리하는 방법으로 결정했다. (아래 예시에서는 부모의 width를 임의로 제한한 상태이다.)
물론 좋은 방법은 아닐 수 있다. 어떤 버튼인지 정보가 가려질 수 있기 때문이다. 말줄임표 처리는 UI가 깨지지 않기 위한 최종 보루이고, width: 100%
속성을 적용하여 텍스트가 길어질 것 같다면 버튼 텍스트는 최대한 모두 보여주는게 더 바람직하다고 생각한다.
카카오 로그인 버튼의 경우 텍스트가 없을 때 로고만 보여주도록 정의했다.
아래 Input 예시의 경우 라벨과 에러 메시지가 길어지는 경우를 방지했다.
라벨은 1줄 말줄임표로 처리해도 상관 없을 것 같지만, 에러 메시지의 말줄임표는 중요한 정보를 가릴 수 있기 때문에, width만 맞춰주고 y 방향으로는 제한 없이 길어지는게 더 좋은 방법일 수 있다.
아래는 Global Navigation Bar의 사례이다. GNB는 전체 앱을 통틀어 1개 밖에 없기 때문에 개발자가 따로 신경 쓸 수 있기 때문에, 메뉴가 길어진 경우는 스크롤 없이 그냥 overflow: hidden
시켜 버렸다. 화면에 비해 메뉴가 길어지면 Hamburger 버튼으로 처리하자는 결정이다.
물론 어떠한 레이아웃이냐에 따라 x방향으로 scroll 될 수 있게 해도 좋고, 줄바꿈이 되도 좋다.
이 글은 개발 하다가 제가 느낀 점을 기반으로 작성되었습니다. 그래서 틀린 부분이 있을 수 있고 더 나은 방법이 존재할 수 있습니다. 그저 한 명의 부족한 개발자의 생각 정도로 이해하고 글을 읽어주세요. 추가 의견이나 아이디어가 있으면 댓글로 함께 논의해보아요.
부족한 글 읽어주셔서 감사합니다.