🎨

클래스101의 디자인 시스템, One Product System

📌

이 글은 클래스101 프론트엔드 챕터의 주간 미팅에서 새로운 디자인 시스템을 소개하고, 앞으로의 개발 방향을 설명하기 위해 작성했습니다. 우리는 지금 Class101 UI라는 디자인 시스템을 사용해 제품을 개발하고 있습니다. 하지만 이 글에서 언급할 몇 가지 설계 단계의 문제들을 해결하고 서비스 리브랜딩에 대응해야 할 필요가 생겼고, One Product System(a.k.a OPS)이라는 새로운 디자인 시스템을 구현하게 되었습니다.

디자인 시스템을 도입한다는 것은 본질적으로는 디자인에 제한을 둔다는 것입니다. 디자인 시스템을 도입하면 컴포넌트를 입맛대로 변형할 수 없습니다. 예를 들어, Button 컴포넌트는 sm, md, lg, xl의 4가지 크기만 가질 수 있고, 그 외의 크기로는 사용할 수 없습니다. 텍스트 관련 컴포넌트도 마찬가지입니다. 제목이나 본문으로 사용할 수 있는 컴포넌트들이 정해져 있고, 각 상황에 맞는 컴포넌트만 사용해야 합니다.

이렇게 제약만 추가해서 얻을 수 있는 게 뭐가 있을까요? 짐작했겠지만 통일성과 생산성입니다. 둘 중에 개발 관점에서 더 중요하게 보는 쪽은 생산성입니다. 어떤 컴포넌트가 가질 수 있는 변형들을 미리 정의해 두고, 그 이외의 변형을 사용할 수 없게 한다면 이를 조합하여 만들어지는 컴포넌트의 개수가 제한됩니다. 개수가 제한되어 있다면 컴포넌트를 미리 만들어 둘 수 있고 이를 얼마든지 재활용할 수 있습니다. 생산성이 좋아질 수밖에 없습니다.

변형(variation) 관리

그래서 새로운 디자인 시스템을 만드는 데 가장 중요하게 생각한 부분이 바로 컴포넌트의 변형, 즉 베리에이션 관리입니다. 디자인 시스템 개발의 핵심이 여러 컴포넌트의 다양한 베리에이션을 깔끔하게 처리하는 것이기 때문이죠. OPS에서는 styled-system을 도입해 이 문제를 해결했습니다.

image

위의 Typography 시스템은 이렇게 구현할 수 있습니다.

import styled from '@emotion/styled';
import React from 'react';
import { variant } from 'styled-system';
import { TypographyProps } from '../../props';
import { Box } from '../Box';

export const textVariants = {
  heading1: {
    fontWeight: 'bold',
    fontSize: '1.75rem',
    lineHeight: '2.25rem',
  },
  heading2: {
    fontWeight: 'bold',
    fontSize: '1.625rem',
    lineHeight: '2.125rem',
  },
  heading3: {
    fontWeight: 'bold',
    fontSize: '1.25rem',
    lineHeight: '1.625rem',
  },
  heading4: {
    fontWeight: 'bold',
    fontSize: '1.125rem',
    lineHeight: '1.375rem',
  },
  body1: {
    fontSize: '1rem',
    lineHeight: '1.25rem',
  },
  body2: {
    fontSize: '0.875rem',
    lineHeight: '1.125rem',
  },
  body3: {
    fontSize: '0.8125rem',
    lineHeight: '1.125rem',
  },
  caption1: {
    fontSize: '0.75rem',
    lineHeight: '1rem',
  },
  caption2: {
    fontSize: '0.6875rem',
    lineHeight: '0.875rem',
  },
};

export type TextVariant = keyof typeof textVariants;

export type TextProps = {
  variant: TextVariant;
} & Pick<TypographyProps, 'fontWeight' | 'lineLimit'>;

export const Text: React.FC<BaseTextProps> = styled(Box)(
  variant({
    variants: textVariants,
  })
);

또는 두 종류의 변형을 함께 관리할 수도 있습니다.

image

위의 Button 시스템은 다음과 같이 구현할 수 있습니다.

import styled from '@emotion/styled';
import React, from 'react';
import { variant } from 'styled-system';
import { BaseButton, BaseButtonProps } from '../../../private/BaseButton';
import { textVariants } from '../../../private/BaseText';
import { skipForwardProps } from '../../../props';

const kindVariants = {
  primary: {
    fontWeight: '700',
    color: 'white',
    backgroundColor: 'primary.500',
    '&:hover': {
      backgroundColor: 'primary.700',
    },
    '&:active': {
      backgroundColor: 'primary.800',
    },
    '&:disabled': {
      color: 'primary.200',
      backgroundColor: 'primary.100',
    },
  },
  secondary: {
    fontWeight: '700',
    color: 'white',
    backgroundColor: 'black',
    '&:hover': {
      backgroundColor: 'gray.800',
    },
    '&:active': {
      backgroundColor: 'gray.700',
    },
    '&:disabled': {
      color: 'gray.400',
      backgroundColor: 'gray.100',
    },
  },
  tertiary: {
    fontWeight: '700',
    color: 'gray.700',
    backgroundColor: 'gray.100',
    '&:hover': {
      backgroundColor: 'gray.200',
    },
    '&:active': {
      backgroundColor: 'gray.300',
    },
    '&:disabled': {
      color: 'gray.400',
      backgroundColor: 'gray.100',
    },
  },
  outline: {
    color: 'black',
    backgroundColor: 'white',
    borderWidth: 1,
    borderStyle: 'solid',
    borderColor: 'gray.300',
    '&:hover': {
      backgroundColor: 'gray.200',
      borderColor: 'gray.400',
    },
    '&:active': {
      backgroundColor: 'gray.300',
      borderColor: 'gray.500',
    },
    '&:disabled': {
      color: 'gray.400',
      backgroundColor: 'white',
      borderColor: 'gray.300',
    },
  },
};

const sizeVariants = {
  sm: {
    height: 30,
    paddingY: 8,
    paddingX: 12,
    ...textVariants.caption2,
  },
  md: {
    height: 38,
    paddingY: 10,
    paddingX: 12,
    ...textVariants.body2,
  },
  lg: {
    height: 44,
    paddingY: 12,
    paddingX: 14,
    ...textVariants.body1,
  },
  xl: {
    height: 50,
    paddingY: 14,
    paddingX: 16,
    ...textVariants.heading4,
  },
};

type KindVariant = keyof typeof kindVariants;
type SizeVariant = keyof typeof sizeVariants;

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  Pick<BaseButtonProps, 'as'> & {
    kind: KindVariant;
    size: SizeVariant;
    full?: boolean;
  };

export const Button: React.FC<ButtonProps> = styled(
  (props: ButtonProps) => <BaseButton borderRadius={2} {...props} />,
  skipForwardProps(['size', 'kind'])
)(
  variant({
    prop: 'kind',
    variants: kindVariants,
  }),
  variant({
    prop: 'size',
    variants: sizeVariants,
  })
);

반응형 스타일

styled-system을 도입하여 반응형 스타일도 관리하기 편해졌습니다. 예를 들어, 아래 AuthLayout 컴포넌트의 우측 영역은 모바일에서 숨겨집니다. 어떻게 구현할 수 있을까요?

Desktop
Desktop
Mobile
Mobile

styled-system을 사용하면 반응형 스타일 속성으로 쉽게 처리할 수 있습니다.

const AuthLayout: React.FC<AuthLayoutProps> = ({ background, children }) => (
  <HStack as="main" height="100%" minHeight="100vh">
    <VStack as="section" alignment="center" flex={1} px={24} py={64}>
      <VStack width="100%" maxWidth={424} flex={1}>
        <Class101Logo size="medium" />
        <VStack mx={0} my="auto">
          {children}
        </VStack>
      </VStack>
    </VStack>
    <Position position="relative">
      <VStack flex={1.1} overflow="hidden" display={['none', 'block']}>
        <Position position="absolute">
          <Image width="100%" height="100%" objectFit="cover" src={background} />
        </Position>
      </VStack>
    </Position>
  </HStack>
);

display 속성에 none, block이 차례로 들어간 것을 볼 수 있습니다. 모바일 화면에서는 display: none을, 그보다 큰 화면에서는 display: block을 적용한다는 의미입니다.

더 자세히 알아볼까요? OPS의 theme 정의를 확인해보면 세 개의 중단점이 보입니다. 이 중단점들로 가능한 화면 구성은 ~ 413px, 414px ~ 767px, 768px ~ 1239px, 1240px ~ 입니다.

const baseTheme = {
  space: {
    0: 0,
    2: 2,
    4: 4,
    6: 6,
    8: 8,
    10: 10,
    12: 12,
    14: 14,
    16: 16,
    18: 18,
    20: 20,
    24: 24,
    28: 28,
    30: 30,
    36: 36,
    40: 40,
    60: 60,
  },
  breakpoints: ['414px', '768px', '1240px'],
};

반응형 스타일 속성은 이 중 가장 작은 화면부터 매칭됩니다. 예를 들어 위 예시처럼 display 속성에 'none', 'block']을 넣어 주면 해당 컴포넌트는 ~ 413px의 화면에서는 보이지 않고, 414px ~의 화면에서는 모두 보이게 됩니다.

OPS의 개발 방향

디자인 시스템을 만들어 뒀으니 쓰기만 하면 될 것 같은데, 이렇게 구현에 대한 이야기까지 하니 의아할 수도 있을 것 같습니다. 사실 이유는 간단합니다. 앞으로는 이 글을 읽고 있는 프론트엔드 개발자들이 모두 OPS를 개발할 예정이기 때문이죠!

앞으로 새로 개발하거나 개편하는 페이지가 있다면 OPS를 사용하여 개발하고, 시스템에 없는 컴포넌트는 직접 만들어서 PR을 보내주면 됩니다. 그러면 OPS를 담당하는 이 API 디자인과 구현을 리뷰해 주고, OPS 디자이너들의 최종 리뷰를 거쳐 시스템 컴포넌트에 추가됩니다. 새로운 컴포넌트의 개발 과정은 Jira 프로젝트에서 관리하고, 주간 회의에서 릴리즈 노트를 공유합니다.

디자인 시스템을 적극 도입하기 위한 정책은 간단합니다. 가능한 한 styled를 사용하지 마세요. 즉, 모든 컴포넌트를 시스템 컴포넌트의 조합으로만 만들기 위해 노력하세요. 스타일을 컴포넌트에 직접 넣어주지 않으려면 항상 적절한 시스템 컴포넌트 혹은 스타일 속성을 만들어야 합니다. 아래 예시와 OPS의 특징들을 살펴보며 이 가이드라인에 대해 자세히 알아보겠습니다.

디자인 시스템 사용하기

디자인 시스템을 도입하면 생산성이 좋아진다는 건 익히 들어서 알겠는데, 정확히 어떻게 달라질까요?

이를 알아보기 위해 다음 EmailLoginForm 컴포넌트를

  • 디자인 시스템을 사용하지 않고
  • 기존의 @class101/ui를 사용하여

각각 만들어 보겠습니다.

EmailLoginForm 컴포넌트
EmailLoginForm 컴포넌트

디자인 시스템 없이 만들기

CSS-in-JS 라이브러리인 emotion을 사용하여 만들면 이렇게 됩니다.

image
import styled from "@emotion/styled";

const Form = styled.form`
  display: flex;
  flex-direction: column;
`;

const FormGroup = styled.div`
  display: flex;
  flex-direction: column;
  margin: 0 0 16px;
`;

const Label = styled.label`
  font-size: 14px;
  line-height: 20px;
  color: #1a1a1a;
  letter-spacing: -0.15px;
  margin: 0;
`;

const Input = styled.input`
  box-sizing: border-box;

  height: 48px;
  margin: 2px 0 0;
  padding: 0 16px;

  border: solid 1px #e5e5e5;
  border-radius: 3px;

  font-size: 14px;
  line-height: 20px;
  letter-spacing: -0.15px;

  color: #1a1a1a;
  background-color: white;

  &:hover {
    border: solid 1px #d7d7d7;
  }

  &:focus {
    outline: none;
    border-color: #1a1a1a;
  }

  &::placeholder {
    color: #cacaca;
  }
`;

const ResetPasswordLink = styled.a`
  display: block;
  float: left;

  font-size: 11px;
  line-height: 16px;
  letter-spacing: normal;
  color: #cacaca;

  text-decoration: none;
`;

const SignUpLink = styled.a`
  display: block;
  float: right;

  font-size: 11px;
  line-height: 16px;
  letter-spacing: normal;
  color: #cacaca;

  text-decoration: none;
`;

const SubmitButton = styled.button`
  width: 100%;
  height: 48px;
  padding: 0;
  margin: 16px 0;

  flex: initial;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: center;

  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.2px;

  color: white;
  background-color: #ff5600;

  border-radius: 3px;
  border: 0;
  outline: none;
`;

const EmailLoginForm = () => (
  <Form>
    <FormGroup>
      <Label htmlFor="email">이메일</Label>
      <Input type="email" name="email" placeholder="example@gmail.com" />
    </FormGroup>
    <FormGroup>
      <Label htmlFor="password">비밀번호</Label>
      <Input type="password" name="password" placeholder="********" />
    </FormGroup>
    <div>
      <ResetPasswordLink href="/reset-password">
        비밀번호를 잊으셨나요?
      </ResetPasswordLink>
      <SignUpLink href="/sign-up">회원 가입하기</SignUpLink>
    </div>
    <SubmitButton type="submit">로그인</SubmitButton>
  </Form>
);

export default EmailLoginForm;

모든 컴포넌트에 각각 스타일을 지정해 준 것을 확인할 수 있습니다.

@class101/ui 사용하여 만들기

이 컴포넌트를 우리의 이전 디자인 시스템을 사용하여 다음과 같이 작성할 수 있습니다.

image
import styled from "styled-components";
import {
  Button,
  ButtonColor,
  ButtonSize,
  Colors,
  FormGroup,
  Input,
  TextStyles
} from "@class101/ui";

const Form = styled.form`
  display: flex;
  flex-direction: column;
`;

const StyledFormGroup = styled(FormGroup)`
  margin-bottom: 12px;
`;

const StyledInput = styled(Input)`
  margin-top: 2px;
`;

const ResetPasswordLink = styled.a`
  ${TextStyles.caption1};
  color: ${Colors.gray500};
  display: block;
  float: left;
  text-decoration: none;
`;

const SignUpLink = styled.a`
  ${TextStyles.caption1};
  color: ${Colors.gray500};
  display: block;
  float: right;
  text-decoration: none;
`;

const StyledButton = styled(Button)`
  margin: 16px 0;
`;

const EmailLoginForm = () => (
  <Form>
    <StyledFormGroup label="이메일" labelColor={Colors.gray900}>
      <StyledInput type="email" name="email" placeholder="example@gmail.com" />
    </StyledFormGroup>
    <StyledFormGroup label="비밀번호" labelColor={Colors.gray900}>
      <StyledInput type="password" name="password" placeholder="********" />
    </StyledFormGroup>
    <div>
      <ResetPasswordLink href="/reset-password">
        비밀번호를 잊으셨나요?
      </ResetPasswordLink>
      <SignUpLink href="/sign-up">회원 가입하기</SignUpLink>
    </div>
    <StyledButton
      type="submit"
      color={ButtonColor.ORANGE}
      size={ButtonSize.LARGE}
    >
      로그인
    </StyledButton>
  </Form>
);

export default EmailLoginForm;

단순히 줄 수로만 봐도 많은 코드가 줄어들었습니다! 하지만 아직 더 개선할 수 있을 것 같습니다. 앞서 디자인 시스템을 적극 도입하기 위해서는 가능한 한 styled를 사용하지 말자고 이야기했는데, 여전히 styled는 사라지지 않았습니다. StyledInput, StyledFormGroup 등을 보면 주로 컴포넌트를 배치하기 위한 용도로 시스템 컴포넌트에 별도의 스타일이 추가된 것을 볼 수 있습니다.

OPS를 사용하여 만들기

같은 컴포넌트를 OPS를 사용하여 만들면 아래와 같은 모습이 됩니다.

import {
  Button,
  Caption,
  HStack,
  Link,
  TextField,
  VStack,
} from '@class101/oui';

const EmailLoginForm = () => (
  <form>
    <VStack spacing={16} mb={4}>
      <TextField
        name="email"
        type="email"
        label="이메일"
        placeholder="example@naver.com"
      />
      <TextField
        name="password"
        type="password"
        label="비밀번호"
        placeholder="********"
      />
    </VStack>
    <HStack spacing="between" mb={4} my={8}>
      <Link to="/reset-password">
        <Caption level={2} color="gray.600">
          비밀번호를 잊으셨나요?
        </Caption>
      </Link>
      <Link to="/sign-up">
        <Caption level={2} color="gray.600">
          회원 가입하기
        </Caption>
      </Link>
    </HStack>
    <Button kind="primary" size="lg" full>
      로그인
    </Button>
  </form>
);

export default EmailLoginForm;

스택 컴포넌트

OPS에서는 컴포넌트 배치 문제를 해결하기 위해 그 역할을 하는 컴포넌트를 만들었습니다. HStack, VStack, ResponsiveStack 그리고 Center 컴포넌트입니다. SwiftUI의 HStack, VStack과 흡사합니다.

Stack 컴포넌트들은 간격 관련 속성들(padding, margin)과 레이아웃 관련 속성들(width, height, display, overflow, ...)을 받을 수 있습니다.

HStack 컴포넌트는 자식 컴포넌트들을 가로로 정렬시키는 컴포넌트입니다.

  • spacing 속성에 숫자를 넣어 자식 컴포넌트 간 간격을 지정하거나 around, between 으로 정렬시킬 수 있습니다.
  • alignment 속성에 'stretch' | 'top' | 'bottom' | 'center'를 넣어 세로 정렬을 지정할 수도 있습니다.
image
image

방향만 다르고 비슷한 역할을 하는 VStack 컴포넌트, 이 둘을 합쳐 가운데 정렬을 시킬 수 있는 Center 컴포넌트, 그리고 반응형으로 Stack 컴포넌트의 방향이 달라져야 하는 경우를 위한 ResponsiveStack 컴포넌트도 있습니다.

스타일 속성 만들기

styled-system의 특징 중 하나는 CSS 속성을 React 컴포넌트의 속성으로 만들어서 바로 적용할 수 있게 해 준다는 것입니다. 아래 Box 컴포넌트를 보겠습니다.

import styled from '@emotion/styled'
import { typography, space, color } from 'styled-system'

const Box = styled('div')(
  typography,
  space,
  color
)

typography 시스템을 적용하면 fontSize, fontWeight 등의 속성을 받을 수 있고, space 시스템을 적용하면 padding, margin 등의 속성을 받을 수 있고, color 시스템을 적용하면 backgroundColor, color 등의 속성을 받을 수 있습니다. 위에서 만든 Box 컴포넌트는 다음과 같이 사용합니다.

<Box
  fontSize={4}
  fontWeight='bold'
  padding={3}
  marginBottom={[4, 5]}
  color='white'
  bg='primary'>
  Hello
</Box>

marginBottom 속성에서 확인할 수 있듯, styled-system으로 만들어진 속성은 반응형 스타일까지 대응할 수 있습니다.

그렇다면 조금 더 복잡한 스타일은 어떨까요? 예를 들어, VStack 컴포넌트에서 스크롤 바를 숨기기 위해서는 아래와 같은 CSS를 적용해야 합니다.

const Container = styled(VStack)`
	scrollbar-width: none;
  -ms-overflow-style: none;
  &::-webkit-scrollbar {
    display: none;
  }
`;

각 스타일 규칙을 전부 scrollbarWidth, msOverflowStyle, webkitScrollbarDisplay로 만들고 싶지는 않을 겁니다. 이 규칙들을 묶어 하나의 속성으로 만들 수 있습니다.

import type { LayoutProps as SystemLayoutProps } from 'styled-system';
import { compose, layout as systemLayout, system } from 'styled-system';
import type { ResponsiveValue } from './types';

export type LayoutProps = SystemLayoutProps & {
  hideScroll?: ResponsiveValue<boolean>;
};

const customLayout = system({
  hideScroll: (value: boolean) =>
    value
      ? {
          scrollbarWidth: 'none',
          msOverflowStyle: 'none',
          '&::-webkit-scrollbar': {
            display: 'none',
          },
        }
      : {},
});

export const layout = compose(systemLayout, customLayout);

이제 이 layout 시스템을 적용한 컴포넌트에서는 hideScroll 속성을 사용할 수 있습니다!

Box 기반으로 개발하기

OPS에는 이렇게 만들어진 속성들을 모두 받을 수 있는 Box 컴포넌트가 있습니다.

const Box = styled('div', {
  shouldForwardProp,
})(
  action,
  animator,
  background,
  border,
  color,
  flexbox,
  input,
  image,
  input,
  layout,
  position,
  space,
  shadow,
  typography,
  transition
);

물론 이 강력한 컴포넌트는 OPS 내부에서만 사용할 수 있습니다. OPS의 컴포넌트들은 Box 컴포넌트를 기반으로 만들게 됩니다. 아래 Chip 컴포넌트의 디자인과 구현을 살펴봅시다.

image
import React, { useMemo } from 'react';
import { variant } from 'styled-system';
import styled from '@emotion/styled';
import { skipForwardProps } from '../../props';
import { kindVariants as textKindVariants } from '../../private/BaseText';
import { Box } from '../../private/Box';
import { Icon } from '../Media/Icon';

const kindVariants = {
  fill: {
    backgroundColor: 'gray.100',
    '&:hover': {
      backgroundColor: 'gray.200',
    },
  },
  outline: {
    backgroundColor: 'white',
    borderWidth: 1,
    borderStyle: 'solid',
    borderColor: 'gray.300',
    '&:hover': {
      backgroundColor: 'gray.200',
    },
  },
} as const;

const sizeVariants = {
  sm: {
    paddingX: 12,
    paddingY: 6,
    ...textKindVariants.body3,
  },
  md: {
    paddingX: 16,
    paddingY: 10,
    ...textKindVariants.body2,
  },
} as const;

type KindVariant = keyof typeof kindVariants;
type SizeVariant = keyof typeof sizeVariants;

export type ChipProps = {
  label: string;
  kind?: KindVariant;
  size?: SizeVariant;
  action?: 'delete' | 'add';
  onAction?: () => void;
};

export const Chip: React.FC<ChipProps> = styled((props: ChipProps) => {
  const { label, action, onAction, ...props } = props;

  const ActionIcon = useMemo(() => {
    switch (action) {
      case 'delete':
        return Icon.Thin.Close;
      case 'add':
        return Icon.Thin.Add;
      default:
        return () => null;
    }
  }, [action]);

  return (
    <Box {...props} display="inline-flex" alignItems="center" borderRadius={24}>
      {label}
      {action && (
        <Box
          as="button"
          ml={4}
          p={0}
          lineHeight={0}
          background="none"
          border="none"
          cursor="pointer"
          onClick={onAction}
        >
          <ActionIcon size={14} fill="black" />
        </Box>
      )}
    </Box>
  );
}, skipForwardProps(['kind', 'size']))(
  variant({
    prop: 'kind',
    variants: kindVariants,
  }),
  variant({
    prop: 'size',
    variants: sizeVariants,
  })
);

Chip.defaultProps = {
  kind: 'fill',
  size: 'sm',
};

사실 Chip 컴포넌트는 이 글을 쓰는 도중에 만들어진 따끈따끈한 컴포넌트입니다. Box 컴포넌트를 기반으로 컴포넌트를 구성하거나 스타일을 주고, 가능한 변형을 kindVariants, sizeVariants로 관리하기 쉽게 작성한 것을 확인할 수 있습니다.

Beyond Web Design System

지금까지 우리가 어떤 생각으로 새로운 디자인 시스템을 설계했는지, 그리고 이 디자인 시스템의 특징과 개발 방법에 대해 알아봤습니다. One Product System이라는 이름에서도 드러나듯 우리의 목표는 이 시스템으로 웹과 앱을 포함한 클래스101의 모든 제품을 더 나은 품질을 유지하며 더 적은 노력으로 개발할 수 있게 하는 것입니다.

새 디자인 시스템 도입과 개발 방향에 대해 언급한 게 불과 일주일 전인데, 문서 작성이 채 끝나기도 전에 몇 개의 컴포넌트가 새로 추가되고 또 개발 중에 있습니다. 성능과 사용 편의성 중 어떤 부분에 중점을 두어야 할지, 어떻게 디자인해야 사용하는 입장에서 더 파악하기 쉬울지, 또 어떻게 더 잘 협업할 수 있을지 머리를 맞대고 고민하는 동료들의 모습을 보며 설레는 마음으로 글을 맺습니다. 우리의 미래가 너무 기대됩니다!

이런 고민을 함께하며 성장하는 서비스에 기여하고 싶다면 아래 링크를 통해 지원서를 보내주세요. 피플팀과의 티타임 및 채용 문의(recruit@101.inc)도 언제든 환영입니다. 😉