css 작업을 하다보면 반복되는 css 코드들이 많이 생긴다.
특히 flex 기반의 레이아웃을 만들다보면 display: flex;
flex-direction: ~~
등의 코드들이 반복되는 경우가 많은데, emotion
이나 styled-component
를 사용할 경우 이런 반복적인 코드를 컴포넌트 혹은 css 변수로 선언후 모듈화하여 여러군데에서 사용할수 있다.
레이아웃 컴포넌트를 작업하게 된 이유가 바로 이 때문이다. 반복되는 레이아웃을 공통된 컴포넌트로 만들어 사용할 수 있게 만들고 싶었다.
이번 작업은React
와 emotion
을 사용해 작업했다.
작업을 진행하면서 고려했던 점이 크게 3가지 있다.
아래는 위 3가지 니즈를 충족시키기 위해 작업한 과정과 결과물이다.
<Row></Row>
<Column></Column>
<Grid></Grid>
<Row tag="form" />
<Column tag="ul" />
<Grid tag="li"/>
<Row.form />
<Column.ul />
<Grid.li />
import { css } from "@emotion/react";
import { EmotionJSX } from "@emotion/react/types/jsx-namespace";
import styled from "@emotion/styled";
import React, { AllHTMLAttributes } from "react";
import { createStyled } from "createStyled";
import domElementList from "domElement";
type Props = {};
type LayoutBaseType = ({ ...rest }: React.ComponentProps<typeof StyledLayout>) => EmotionJSX.Element;
type LayoutTagsType = {
[tag in keyof JSX.IntrinsicElements]: LayoutBaseType;
};
interface CreateLayout extends LayoutBaseType, LayoutTagsType {}
const LayoutCSS = (props?: LayoutProps) => css``;
const StyledLayout = styled((props: { tag?: keyof JSX.IntrinsicElements } & LayoutProps & AllHTMLAttributes<HTMLElement>) => {
const { tag = "div", ...rest } = props;
return React.createElement(tag, rest);
})`
${(props) => LayoutCSS(props)}
`;
const LayoutBase: LayoutBaseType = ({ ...rest }: React.ComponentProps<typeof StyledLayout>) =>
createStyled(StyledLayout)({ ...rest });
const Layout = LayoutBase as CreateLayout;
domElementList.forEach((domElement) => {
Layout[domElement] = createStyled(StyledLayout, domElement)[domElement];
});
export default Layout;
import { EmotionJSX } from "@emotion/react/types/jsx-namespace";
import { StyledComponent } from "@emotion/styled";
import { AllHTMLAttributes } from "react";
type StyledBaseType<P> = StyledComponent<{ tag?: keyof JSX.IntrinsicElements } & P & AllHTMLAttributes<HTMLElement>>;
export function createStyled<P>(
StyledBase: StyledBaseType<P>
): ({ ...rest }: React.ComponentProps<StyledBaseType<P>>) => EmotionJSX.Element;
export function createStyled<P>(
StyledBase: StyledBaseType<P>,
tag: keyof JSX.IntrinsicElements
): {
[tag in keyof JSX.IntrinsicElements]: ({ ...rest }: React.ComponentProps<StyledBaseType<P>>) => EmotionJSX.Element;
};
export function createStyled<P>(StyledBase: StyledBaseType<P>, tag?: keyof JSX.IntrinsicElements) {
if (!tag) {
return ({ ...rest }: React.ComponentProps<typeof StyledBase>) => <StyledBase tag={"div"} {...rest} />;
} else {
return {
[tag]: ({ ...rest }: React.ComponentProps<typeof StyledBase>) => <StyledBase tag={tag} {...rest} />
};
}
}
<Layout />
<Layout.tag />
Layout
이라는 형태를 사용 방식에 따라 그 자체를 컴포넌트로 사용하거나, 혹은 컴파운드 형태로 사용하도록 작업하려고 했지만 어떻게 구현해야할지 갈피를 못잡고 있었다. 나 혼자 고민해서 만족스러운 결과가 나오지 않아서 styled-component
와 emotion/styled
의 내부 코드를 들여다봤다. styled
를 만들기 위한 고차함수가 있었고 이를 차용하기로 했다. 그래서 작업한 내용이 바로 createStyled
…! (두둥탁) createStyled
함수는 동일한 이름이지만 인자값에 따라 리턴값이 바뀌는 함수 오버로딩을 이용해 만든 함수이다.BaseStyle
하나만 받을경우 React 컴포넌트를 리턴하고, tag를 추가로 받을 경우 { [tag]: React컴포넌트 }
형태의 객체를 리턴하도록 작업했다.function createStyled<P>(StyledBase: StyledBaseType<P>, tag?: keyof JSX.IntrinsicElements) {
if (!tag) {
return ({ ...rest }: React.ComponentProps<typeof StyledBase>) => <StyledBase tag={"div"} {...rest} />;
} else {
return {
[tag]: ({ ...rest }: React.ComponentProps<typeof StyledBase>) => <StyledBase tag={tag} {...rest} />
};
}
}
createStyled
함수를 생성하고 이를 Row
, Column
, Grid
에 각각 적용했고 어느정도 원하는 결과나 도출됐다.const RowTags = createStyled(StyledRowBase, tag);
// return { [tag]: <RowBase />}
const Row = createStyled(StyledRowBase);
// return <RowBase />
하지만 RowTags
와 Row
를 하나의 코드로 사용하지 않고 있기때문에 100% 만족스럽지는 않았다. RowTags
와 Row
를 하나로 사용하기 위해 혼자서 꽤 오래 고민을 해봤지만, 역시 원하는 결과물이 나오지 않아서 이번에도 styled-component
와 emotion/styled
의 코드를 들여다봤다. Row
를 어떻게 구현하는가에만 생각을 쏟고 있었는데, 타입 다운캐스팅(as
)을 사용해서 타입을 강제해주니 해결됐다.const RowBase = (props) => createStyled(StyledRowBase)(props);
const Row = RowBase as typeof RowBase & {[tag in keyof JSX.IntrinsicElements]: typeof RowBase};
export default Row;
이제 구현은 완료됐지만, 빌드시 d.ts파일에 각 html 태그마다 타입이 하나씩 모두 생성되는 문제가 있었다.//Row.tsx
const RowBase = (props) => createStyled(StyledRowBase)(props);
const Row = RowBase as typeof RowBase & {[tag in keyof JSX.IntrinsicElements]: typeof RowBase};
export default Row;
위처럼 구현했을때 d.ts파일을 까보면 무시무시한게 나온다.//Row.d.ts
export {
a: () => typeof Row;
div: () => typeof Row;
ul: () => typeof Row;
li: () => typeof Row;
// ...이런게 수십개 있음 ㄷㄷ;;;
}
원인은 생각보다 단순했다. Row.tsx
에서 Row
의 타입을 지정할때 타입을 생성하지 않고 지정해줬기 때문에 저런 참사가 발생했던것 같다. 타입을 생성하고 생성한 타입을 지정하니 해결됐다.//Row.tsx
//기존
const RowBase = (props) => createStyled(StyledRowBase)(props);
const Row = RowBase as typeof RowBase & {[tag in keyof JSX.IntrinsicElements]: typeof RowBase};
export default Row;
//변경후
type RowBaseType = (props: propsType) => EmotionJSX.Element;
type RowTagsType = {
[tag in keyof JSX.IntrinsicElements]: RowBaseType;
};
interface CreateRow extends RowBaseType, RowTagsType {}
const RowBase = (props) => createStyled(StyledRowBase)(props);
const Row = RowBase as createRow;
export default Row;