혼자 적어보는 노트

프로그래머스 데브코스 TIL - Day 53 본문

스터디

프로그래머스 데브코스 TIL - Day 53

jinist 2022. 6. 2. 04:09

 

✅ 오늘의 학습

📌 React (6)

 

- 컴포넌트 연습하기

Flux / Breadcrumb / Tab

- 사용자 정의 Hook 연습하기

useScroll / useKey / useClickAway

 


Flux 컴포넌트

12그리드 방식으로 culumn을 나누어서 레이아웃을 잡는 것을 도와주는 컴포넌트.

 

col 컴포넌트에서 span과 offset값을 받아서 아래와 같이 style을 지정하여

12그리드를 구현할 수 있다.

const StyledCol = styled.div`
  max-width: 100%;
  box-sizing: border-box;

  width: ${({ span }) => span && `${(span / 12) * 100}%`};
  margin-left: ${({ offset }) => offset && `${(offset / 12) * 100}%`};
`;

 

<Row gutter={[8, 8]}>
  <Col span={4}><Box /></Col>
  <Col span={2}><Box /></Col>
  <Col span={2}><Box /></Col>
  <Col span={2}><Box /></Col>
</Row>

 

위 코드처럼 Row컴포넌트 안에 Col컴포넌트를 자식요소로 담아서 구현을 하는 방식인데

gutter값을 전달해 줄 때 context API를 사용했다는 점이 흥미로웠다,

 

Flux/FluxProvider.js

import { createContext, useContext } from "react";

const FluxContent = createContext();

export const useFlux = () => useContext(FluxContent);

const FluxProvider = ({ children, gutter = 0 }) => {
  return <FluxContent.Provider value={{ gutter }}>{children}</FluxContent.Provider>;
};

export default FluxProvider;

Provider를 생성하고 gutter값을 props로 받을 수 있게 지정하고

value를 통해 값을 내보낼 수 있게 설정한다.

 

[Flux/Row.js]

  return (
    <FluxProvider gutter={gutter}>
      <StyledRow {...props} align={align} justify={justify} style={{ ...props.style, ...gutterStyle }}>
        {children}
      </StyledRow>
    </FluxProvider>
  );

Row컴포넌트 내부의 요소들을 Provider로 감싸고 Row의 props로 받은 gutter를

Provider의 props를 통해 전달하게됨으로써 gutter값을 하위 컴포넌트에서도 공유가 가능해진다!

 

const Col = ({ children, span, offset, ...props }) => {
  const { gutter } = useFlux();
  
  // 생략
  );

즉, Col컴포넌트에서도 gutter값을 바로 사용할 수 있게 되니 해당 값에 맞게 스타일 지정이 가능했다.

context API를 전역 상태를 관리하는 용도로만 생각했지 이런식으로 사용할 생각을 못했다..

그리고 이번 사례를 보며 Context API의 장점이 확실하게 와닿았다.

 

Tab 컴포넌트

Tab컴포넌트는 만들면서 조금 헷갈려서 흐름을 따라가며 적어보기로 했다.

 

Tab 컴포넌트는 아래와 같은 형태로 Tab 내부의 Tab.Item컴포넌트로 구성되며,

Title은 props로 넘겨주고 content는 children으로 넘기는 형태로 작성했다.

<Tab>
  <Tab.Item title="item1" index="item1">
    Content 1
  </Tab.Item>
  <Tab.Item title="item2" index="item2">
    Content 2
  </Tab.Item>
  <Tab.Item title="item3" index="item3">
    Content 3
  </Tab.Item>
</Tab>

 

[Tab/index.js]

import styled from "@emotion/styled";
import React, { useMemo, useState } from "react";
import TabItem from "./TabItem";

const TebItemContainer = styled.div`
  border-bottom: 1px solid #ddd;
`;

const childrenToArray = (children, types) => {
 // 생략
};

const Tab = ({ children, active, ...props }) => {
  const [currentActive, setCurrentActive] = useState(() => {
    if (active) {
      return active;
    } else {
      const index = childrenToArray(children, "Tab.Item")[0].props.index;
      // children의 index찾기

      return index;
    }
  });

  const items = useMemo(() => {
    return childrenToArray(children, "Tab.Item").map((element) => {
      return React.cloneElement(element, {
        ...element.props,
        key: element.props.index,
        active: element.props.index === currentActive,
        onClick: () => {
          setCurrentActive(element.props.index);
        },
      });
    });
  }, [children, currentActive]);


  // 선택된 content 구하기
  const activeItem = useMemo(() => {
    return items.find((element) => currentActive === element.props.index);
  }, [currentActive, items]);

  return (
    <div {...props}>
      <TebItemContainer>{items}</TebItemContainer>
      <div>{activeItem.props.children}</div>
    </div>
  );
};

Tab.Item = TabItem;

export default Tab;

 

Tab부분의 title 구현

각 Item들에게 props로 전달할 데이터를 추가해주기 위해 Tab컴포넌트에서 가공을 진행한다.

1. key값에 props로 받은 index를 전달
2. 선택된 item의 index 값과 item들의 props.index값을 비교해서
같은 엘리먼트만 active값을 true로 전달
3. onClick 이벤트 전달

그렇게 되면 TabItem 컴포넌트 내부에 적어놓은대로 title만 출력이 되고
active를 true로 받은 컴포넌트만 text를 bold로 처리할 수 있다.

 


Tab의 content 구현

tab.Item의 children인 tab의 content부분은
가공이 완료된 items를 조회해서 currentActive값과 일치하는
element.props.index를 찾아서 반환하여 출력한다.

 

[Tab/TabItem.js]

const TabItemWrapper = styled.div`
  background-color: ${({ active }) => (active ? "#ddf" : "eee")};
`;

const TabItem = ({ children, title, index, active, onClick, ...props }) => {
  return (
    <TabItemWrapper active={active} onClick={onClick} {...props}>
      <Text strong={active}>{title}</Text>
    </TabItemWrapper>
  );
};

TabItem.defaultProps = {
  __TYPE: "Tab.Item",
};

TabItem.propTypes = {
  __TYPE: PropTypes.oneOf(["Tab.Item"]),
};

export default TabItem;

배열의 index로만 비교해도 정상적으로 결과가 작동되나

강의에서는 props를 통해 index에 string으로 값을 전달하고 그 값을 통해 비교를 했다.

아마도 안정성을 위해 고유한 값을 넣어 준 것 같다.

 


 

사용자 정의 Hook

useScroll

엘리먼트를 지정하면 해당 요소의 현재 스크롤의 좌표를 반환해주는 hook

const [ref, coord] = useScroll();
  return (
    <>
      <Box ref={ref}>
        <Inner></Inner>
      </Box>
      {coord.x}, {coord.y}
    </>
  );

 

useScoll 최적화

1. passive옵션 true설정

element.addEventListener("scroll", handleScroll, { passive: true });

 

2. requestAnimationFrame 사용

scroll 이벤트의 경우 초당 많은 이벤트를 발생시키기 때문에

useRefState라는 hook을 추가로 만들어서 requestAnimationFrame을 사용하여 최적화 해주었다.

 

[useRefState.js]

const useRefState = (initialState) => {
  const frame = useRef(0);
  const [state, setState] = useState(initialState);

  const setRefState = useCallback((value) => {
    cancelAnimationFrame(frame.current);

    frame.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);

  return [state, setRefState];
};

export default useRefState;

 

requestAnimationFrame() 이란?

브라우저에게 수행하려는 애니메이션을 요청하고
다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 한다.

브라우저는 초당 60fps로 렌더링을 하는데 이 60fps를 맞추기 위해 RAF를 사용할 수 있다.

비동기로 실행되며 Animation Frame이라는 queue에서 처리되기 때문에 (Macro)Task queue보다  먼저 처리된다.

 

* 보통 canvas에서 많이 사용되는데 scroll의 최적화로도 사용한다.

 

window.requestAnimationFrame(callback);

requestAnimationFrame는 리페인트 이전에 실행할 콜백을 인자로 받고
고유한 요청 id 인 long 정수 값을 반환한다.

 

이 id값을 활용하면 cancleAnimationFrame을 통해 요청을 취소할 수 있다.

 

 

useClickAway

지정한 요소 외에 클릭을 할 경우 handler함수를 실행 시켜 주는 hook

const events = ['mousedown', 'touchstart'];

const useClickAway = (handler) => {
  const ref = useRef(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) {
      return;
    }

    const handleEvent = (e) => {
      !element.contains(e.target) && handler(e);
    };

    for (const eventName of events) {
      document.addEventListener(eventName, handleEvent);
    }

    return () => {
      for (const eventName of events) {
        document.removeEventListener(eventName, handleEvent);
      }
    };
  }, [ref, handler]);

  return ref;
};

이렇게 구현을 해도 정상적으로 동작 하지만

위 코드대로라면 handler가 변경이 될 때도 내부의 event를 지우고 새로 생성하게된다.

 

개선 방법

 const saveHandler = useRef(handler);

  useEffect(() => {
    saveHandler.current = handler;
  }, [handler]);

 

useRef는 값이 변경되어도 다시 렌더링을 하지 않기 때문에 이 점을 활용하여

인자로 받은 handler를 useRef에 담아주고 handler가 변경될 때마다

saveHander.current에 있는 handler만 교체하도록 변경해준다.

 

  useEffect(() => {
   // ..생략

    const handleEvent = (e) => {
   	  // !element.contains(e.target) && handler(e);
      !element.contains(e.target) && saveHandler.current(e);
    };

   // ..생략
    };
  }, [ref, saveHandler]);

적용을 위해 handleEvent에 saveHandler.current를 실행하도록 변경하고

하단의 의존성 배열에 handler가 아닌 saveHander로 바꾸어주면

이벤트가 중복적으로 삭제/생성 되는 것을 방지할 수 있다.

 


✍ 느낀 점 

강의를 들으며 컴포넌트를 따라서 만들다가 몇개는 강의를 재생하기 전에 멈춰놓고

먼저 만들어 보았는데, 일회용으로 사용하는 것이라면 기능 구현에만 생각을 했겠지만 (이전의 나..)

이렇게 컴포넌트 단위로 생각을 하면서 재사용성을 위해 고려해야할 점들이 꽤 있었고

생각하는 것 자체가 훈련이 되는 느낌이었다.

그리고 hooks를 만드는 것을 보면서 단순 기능 구현뿐만 아니라 최적화까지 고려를 하는 부분들까지

다루어 주셔서 많은 도움이 되었고 새로 접하게 되는 부분들이 많아 정말 유익했다.👍

Comments