티스토리 뷰

반응형

안녕하세요!

 

오늘도 역시 토스 강의를 보고 와.. 이렇게도 할 수 있구나를 느껴버린 나머지.. 

 

직접 해보는게 어떨까 해서 따라하게 되었습니다.

 

근데 이게 모든 소스를 보여주신건 아니라(내가 해석을 못한걸지도..) 제 주관이 섞여 좀 구조가 바뀌었는데 토스 구조처럼 어떻게 짜는지 혹시 아시는 분 있으시다면 공유 부탁드립니다..

 

코드 리뷰는 언제든지 환영입니다 :D


Effective Components ?

지금의 회사에서 저의 코드를 수정해줄 리더분이 없으셨어서.. 제가 리드해가며 새롭게 리액트로 리팩토링을 하는 작업에서 나는 '설계'부터 잘못했구나 라고 느끼게 된 계기들이 엄청 많았습니다.

 

우선 제가 만든 컴포넌트들은 재사용에 불리한 구조를 가지고있는 경우가 대부분이였습니다. 재사용에 불리하단 것 중 단점으로는 크게 사용하기에 어려운 컴포넌트들이 대부분이였습니다.

 

그렇다보니 어거지로 이거 추가, 저거 추가 하다보니 내부 소스가 더러워지기도 하죠..

 

나아가 추상화도 제대로 되어있지 않은 컴포넌트가 대부분이였습니다.

 

그래서 이번에 토스 유튜브를 보면서.. Headless 기반의 추상화 컴포넌트를 한번 구상해서 개발해보자 라는 생각을 하게 되었고, 이렇게 블로그에 포스팅을 하고있습니다.

 

주니어 개발자라.. 많이 부족한데, 많은 지적을 통해 보완하도록 하겠습니다.


DropDown Select 컴포넌트 만들기

우선 실질적으로 사용이 가능한 Select 컴포넌트를 구상하려고 합니다. Select컴포넌트는 하나의 큰 동작을 수행할 것입니다.

 

그 큰 동작은 Dropdown 컴포넌트를 보이게, 안보이게 하는 내용입니다.

 

// Select.tsx

/** @jsxImportSource @emotion/react */
import React, { useState } from "react";
import { Dropdown } from "./Dropdown";

export interface Props {
    label?: string;
    trigger: React.ReactNode;
    value: string;
    onClick: (value: string) => void;
    options: string[];
}

export const Select = ({label, trigger, value, onClick, options}: Props) => {
    const [isOpen, setIsOpen] = useState(false);
    
    const changeOpen = () => {
    	setIsOpen(item => !item);
    }

    return (
        <Dropdown label={label} value={value} >
            <Dropdown.Trigger onClick={changeOpen} as={trigger}/>
            {isOpen && <Dropdown.Menu>
                {options.map((option, index) => (
                    <Dropdown.Item onClick={onClick} key={index}>{option}</Dropdown.Item>
                ))}
            </Dropdown.Menu>}
        </Dropdown>
    )
}

 

우선 위 코드에서 저는 isOpen의 useState와 changeOpen을 숨길 생각입니다. 굳이 Select.tsx가 알 필요가 없으니까요. 따로 커스텀 훅으로 관리를 하고 싶습니다.

 

// useOpen.ts

import React, { useState } from "react";

export const useOpen = () => {
    const [data, setData] = useState(false);

    const open = () => {
        setData(() => true);
    }

    const close = () => {
        setData(() => false);
    }

    const change = () => {
        setData(item => !item);
    }

    return {
        data, open, close, change
    }
}

 

이렇게 우리는 data와 change만 받으면 될 것 같습니다. 그럼 그대로 리팩토링을 진행해 보면

 

// Select.tsx

/** @jsxImportSource @emotion/react */
import React, { useState } from "react";
import { useOpen } from "../../hoc/useOpen";
import { Dropdown } from "./Dropdown";

export interface Props {
    label?: string;
    trigger: React.ReactNode;
    value: string;
    onClick: (value: string) => void;
    options: string[];
}

export const Select = ({label, trigger, value, onClick, options}: Props) => {
    const {data, change} = useOpen();

    return (
        <Dropdown label={label} value={value} >
            <Dropdown.Trigger onClick={change} as={trigger}/>
            {data && <Dropdown.Menu>
                {options.map((option, index) => (
                    <Dropdown.Item onClick={onClick} key={index}>{option}</Dropdown.Item>
                ))}
            </Dropdown.Menu>}
        </Dropdown>
    )
}

 

으로 바꿀 수 있습니다.. 이렇게 바꾸는게 나은거 맞겠죠..? 누가 내 코드좀 리뷰해줘.. 사수없는 회사는 지옥이야~

 

그럼 이제.. Dropdown 컴포넌트를 만들러 가볼까요..

 

// Dropdown.tsx

/** @jsxImportSource @emotion/react */
import React from "react";
import { Direction } from "../components/Flex";

interface DropProps {
    label?: string;
    value: string;
    children: React.ReactNode;
}

export const Dropdown = ({label, value, children}: DropProps ) => {

    return (
        <Direction
            css={{
                padding: '10px',
            }}
        >
            <div>{label}</div>
            {children}
        </Direction>
    )
}

Dropdown.Trigger = ({as, onClick}: {as: React.ReactNode; onClick: () => void}) => {
    return (
        <div onClick={onClick}>
            {as}
        </div>
    )
}

Dropdown.Menu = ({children}: {children: React.ReactNode}) => {
    return (
        <div>
            {children}
        </div>
    )
}

Dropdown.Item = ({children, onClick}: {
    children: string, onClick: (value: string) => void
}) => {
    return (
        <div css={{
            cursor: 'pointer'
        }} onClick={() => onClick(children)}>
            {children}
        </div>
    )
}

 

모든 로직은 Dropdown.tsx에 담아두었습니다. 이렇게 설정한다면 우리는 Select.tsx만 불러서 label, trigger, value, onClick, opions 만 불러서 사용할 수 있을 것입니다.

 

사용방식도 기존과 동일한 구조로 되어있으니 사용하는데 어려움이 없을거라 생각합니다!

 

그럼 한번 사용해 보도록 하겠습니다.

 

// Home.tsx

/** @jsxImportSource @emotion/react */
import React, { useState } from "react";
import { Select } from "./Select";

const InputButton = ({value}: {value: string}) => {
    return (
        <div css={{
            cursor: 'pointer',
            width: '100%'
        }}>{value || '드롭다운'}</div>
    )
}

export const Home = () => {
    const [selected, change] = useState('');

    return (
            <div>
                <Select
                    label="리액트 프레임워크"
                    trigger={<InputButton value={selected} />}
                    value={selected}
                    onClick={change}
                    options={["Next.js", "Remix", "Gatsby", "Relay"]}
                />
            </div>
    )
}

 

우리가 흔히 사용하는 select와 비슷하지 않나요? 원래는 onChange를 해야하지만.. 난 토스의 코드 방식대로 구현을 못하겠어서.. 바꿨다는건 안비밀..

 

쨋던 저는 이렇게 구성해봤습니다. 그럼 이제 유연하게 사용할 수 있을거라 생각합니다.

 

이렇게 하나하나 배워가는거지뭐..... 나도 대기업 가고싶네.. ㅠ

반응형