레이아웃 컴포넌트 작업기

민순기·2023년 4월 20일
1

작업 내용

작업 이유

css 작업을 하다보면 반복되는 css 코드들이 많이 생긴다.
특히 flex 기반의 레이아웃을 만들다보면 display: flex; flex-direction: ~~ 등의 코드들이 반복되는 경우가 많은데, emotion이나 styled-component를 사용할 경우 이런 반복적인 코드를 컴포넌트 혹은 css 변수로 선언후 모듈화하여 여러군데에서 사용할수 있다.
레이아웃 컴포넌트를 작업하게 된 이유가 바로 이 때문이다. 반복되는 레이아웃을 공통된 컴포넌트로 만들어 사용할 수 있게 만들고 싶었다.

이번 작업은Reactemotion을 사용해 작업했다.

작업을 진행하면서 고려했던 점이 크게 3가지 있다.

  1. 네이밍이 명확해야 한다.
  2. 모든 html 태그가 호환되도록 해야한다.
  3. 고차 컴포넌트에서 만들어진 컴포넌트를 인자로 받을 수 있게 해야 한다.

아래는 위 3가지 니즈를 충족시키기 위해 작업한 과정과 결과물이다.

컴포넌트

  • Row
  • Column
  • Grid

사용 방법

<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;
  • createStyled
    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 />
    <Layout.tag />
    Layout 이라는 형태를 사용 방식에 따라 그 자체를 컴포넌트로 사용하거나, 혹은 컴파운드 형태로 사용하도록 작업하려고 했지만 어떻게 구현해야할지 갈피를 못잡고 있었다. 나 혼자 고민해서 만족스러운 결과가 나오지 않아서 styled-componentemotion/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 />
    하지만 RowTagsRow 를 하나의 코드로 사용하지 않고 있기때문에 100% 만족스럽지는 않았다. RowTagsRow를 하나로 사용하기 위해 혼자서 꽤 오래 고민을 해봤지만, 역시 원하는 결과물이 나오지 않아서 이번에도 styled-componentemotion/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 태그마다 타입이 하나씩 모두 생성되는 문제가 있었다.
  • d.ts 파일 관리
    //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;
profile
2년차 FE 개발자 민순기입니다.

0개의 댓글