- spacing system prop은 아래와 같이 사용 가능하다.
// margin-top: 8px; margin-bottom: 8px; padding-left: 4px; padding-right: 4px;
<Button my={2} px={1}>버튼</Button>
// margin-left: 4px; padding-bottom: 4px;
<Input ml={1} pb={1} />
'spacing 1'은 곧 '4px'을 의미하며, 디자이너와의 협의를 통해 '홀수 px'은 허용하지 않기로 했다.
8px, 12px, 16px 등 4의 배수를 spacing으로 많이 사용하고 있기 때문에 1 === 4px 로 계산하였다.
먼저 prop으로 들어온 number를 '짝수 px' 로 바꾸는 함수를 구현했다.
const STANDARD = 4;
/**
* input/output 예시
* 1. spacing(1) === 4px
* 2. spacing(2.5) === 10px
* 3. spacing(1.75) === 8px (4 * 1.75 = 7이지만 홀수 픽셀을 허용하지 않고 가장 가까운 짝수로 반올림)
* 4. spacing(1.45) === 6px (4 * 1.45 = 5.8이지만 홀수 픽셀을 허용하지 않고 가장 가까운 짝수로 반올림)
*/
export const spacing = (operand: number): number => {
if (Number.isInteger(operand * 2)) {
// 0.5의 배수일 때
return STANDARD * operand;
} else {
// 0.5의 배수가 아닌 소수일 때
const integerPart = Math.floor(operand);
const fractionalPart = operand - integerPart;
const operandRoundedByOneHalf =
fractionalPart < 0.75 && fractionalPart > 0.25 ? integerPart + 0.5 : Math.round(operand);
return STANDARD * operandRoundedByOneHalf;
}
};
- 이제 margin spacing을 구현하고자 한다.
'margin의 type', 컴포넌트가 받을 'margin props', 실제로 css 스타일을 입힐 'margin style' 3가지를 각각 구현해야한다.
// margin type
const marginSpacingOptions = ['mt', 'mb', 'ml', 'mr', 'my', 'mx'] as const;
export type MarginSpacing = {
[key in typeof marginSpacingOptions[number]]?: number;
};
// margin props
export const marginSpacingProps = (props: MarginSpacing): MarginSpacing =>
marginSpacingOptions.reduce(
(prev, curr) => ({
...prev,
[curr]: props[curr],
}),
{},
);
// margin style
export const marginSpacingStyle = (props: MarginSpacing): SerializedStyles => {
const { mx, my } = props;
const { mt = my, mb = my, ml = mx, mr = mx } = props;
const margins = Object.entries({ top: mt, bottom: mb, left: ml, right: mr });
return css`
${margins
.filter(isNumberValue)
.map(([position, value]) => `margin-${position}: ${spacing(value)}px;`)
.join('')}
`;
};
- 2번에서 만든 spacing type, props, style을 컴포넌트에 적용하면 되는데, Button 컴포넌트를 기준으로 설명하고자 한다.
먼저 Button Component Props에 MarginSpacing type을 extends한다.
// Button.tsx
import { MarginSpacing } from ...;
export type ButtonProps = MarginSpacing & ...
그 후, marginSpacingProps를 이용하여 styled button에 margin 관련 prop을 넘긴다.
// Button.tsx
import styled from '@emotion/styled';
export const BaseButton = styled.button`
...
`;
...
export const Button = (props: ButtonProps): JSX.Element => {
return (
<BaseButton
...
{...marginSpacingProps(props)}
...
>
{props.children}
</BaseButton>
);
};
마지막으로 marginSpacingStyle을 이용하여 styled 안에서 css style을 부여한다.
// Button.tsx
export const BaseButton = styled.button`
...
${marginSpacingStyle};
...
`;
- 이제 Button 컴포넌트에서 mt/mb/ml/mr/mx/my prop을 사용할 수 있다!
같은 방법으로 padding spacing props도 구현한다.
// Padding
const paddingSpacingOptions = ['pt', 'pb', 'pl', 'pr', 'py', 'px'] as const;
export type PaddingSpacing = {
[key in typeof paddingSpacingOptions[number]]?: number;
};
export const paddingSpacingProps = (props: PaddingSpacing): PaddingSpacing =>
paddingSpacingOptions.reduce(
(prev, curr) => ({
...prev,
[curr]: props[curr],
}),
{},
);
export const paddingSpacingStyle = (props: PaddingSpacing): SerializedStyles => {
const { px, py } = props;
const { pt = py, pb = py, pl = px, pr = px } = props;
const paddings = Object.entries({ top: pt, bottom: pb, left: pl, right: pr });
return css`
${paddings
.filter(isNumberValue)
.map(([position, value]) => `padding-${position}: ${spacing(value)}px;`)
.join('')}
`;
};
- 그리고 margin과 padding을 합친 box spacing props도 구현한다.
box spacing만 적용하면 margin과 padding props을 둘 다 사용할 수 있다.
// Box(Margin + Padding)
export type BoxSpacing = MarginSpacing & PaddingSpacing;
export const boxSpacingProps = (props: BoxSpacing): BoxSpacing => ({
...marginSpacingProps(props),
...paddingSpacingProps(props),
});
export const boxSpacingStyle = (props: BoxSpacing): SerializedStyles => css`
${marginSpacingStyle(props)};
${paddingSpacingStyle(props)};
`;
- 이제 원하는 컴포넌트 어디서든 margin, padding props를 사용할 수 있다!