프론트엔드 데브코스 5기 TIL 50 - Context API, 재사용가능한 컴포넌트(2)

김영현·2023년 12월 7일
1

TIL

목록 보기
59/129

Context API

Provider로 데이터를 공급해주면 Consumer로 데이터를 사용한다.
PropsDrilling을 피하려고 나온 상태 공유 API. 관리 API가 아니다

Todo를 통한 ContextAPI 실습!

ToDO를 만들면서 실습해보자!
강의도중 처음 배운 내용들 위주로 보았다.

not 선택자(css)

 &:not(:first-child)

not선택자. 위처럼 사용한다면 first-child를 제외한 태그들에 적용한다! 몰랐는데 굉장히 유용해보인다.

provider, context

const 내가지을이름Context = createContext();
const 내가지을이름Provider = ({children}) => {
...
	return 
  		(
        <내가지을이름Context.Provider value={{프로바이더 내부에서 선언한 상태, 변수, 함수들을 전달한다.}}>
          {children}
      	</내가지을이름Context.Provider>
        )
}

이렇게 사용하면된다!
더 구체적인 예시는ㅇㅏ래에..

import { createContext, useContext, useState } from "react";
import { v4 } from "uuid";

const TaskContext = createContext();

export const useTasks = () => useContext(TaskContext);

const TaskProvider = ({ children }) => {
  const [tasks, setTasks] = useState([]);
  const addTask = (content) => {
    setTasks([
      ...tasks,
      {
        id: v4(),
        content,
        isCompleted: false,
      },
    ]);
  };
  const updateTask = (id, isCompleted) => {
    setTasks(
      tasks.map((item) => (item.id === id ? { ...item, isCompleted } : item))
    );
  };

  const removeTask = (id) => {
    setTasks(tasks.filter((item) => item.id !== id));
  };
  return (
    <TaskContext.Provider value={{ tasks, addTask, updateTask, removeTask }}>
      {children}
    </TaskContext.Provider>
  );
};

export default TaskProvider;

이렇게 contextApi로 만든 상태를 사용하고싶다면

    <TaskProvider>
      <div>
        <NewTaskForm />
        <TaskList css={{ marginTop: 16 }} />
      </div>
    </TaskProvider>

컴포넌트 최상단에서 이렇게 감싸주면 된다. children프로퍼티로 들어가야하니까 ㅎㅎ
이후 사용할 컴포넌트에서

  const { updateTask, removeTask } = useTasks();

이렇게 가져와서 쓰면 동기화가 잘 된다.

styledcomponent내부에서 props사용

컴포넌트니까 결국 props를 받아서 사용할수있다.

text-decoration: ${({prop}) => prop && 'line-through'}

템플릿 리터럴처럼 ${}로 감싼뒤 사용한다. 괄호랑 브라켓이 좀 엮여있어서 살짝 헷갈리는구먼

그런데 문제가생겼다...조건부 스타일이 안들어감.
아래 해결방법.

checkbox의 value

input type=checkbox로 사용할때 value가 true,false처럼 불리언이 아니라 on으로만 나온다 ㅋㅋㅋ심지어 off도 안나옴.
그래서 e.target.checked를 가져와서 사용해야한다.
멍충~


재사용 가능한 컴포넌트 연습!

자 시작해보자이

Upload 컴포넌트

드래그&드롭을 이용하여 파일을 업로드하는 컴포넌트다. 유용하겠는걸?

이미지 업로드시 주의사항

참고로 파일업로드도 원래 <input type='file'/>을 사용하지만, 스타일링이 힘들어서 화면에서 안보이게하고 label이용함. 마치 토글처럼

또한 파일이 업로드되면 value에 파일이 들어온다.

파일을 드래그해서 놓을때, 브라우저 기본동작으로인해 파일이 새 창에 열린다. 따라서 전파와 기본 이벤트를 막아주어야함.

  const handleDragEnter = (e) => {
    if (!droppable) {
      return;
    }
    e.preventDefault();
    e.stopPropagation();

    if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
      setDragging(true);
    }
  };
  const handleDragLeave = (e) => {
    if (!droppable) {
      return;
    }
    e.preventDefault();
    e.stopPropagation();

    setDragging(false);
  };
  const handleDragOver = (e) => {
    if (!droppable) {
      return;
    }
    e.preventDefault();
    e.stopPropagation();
  };
  const handleFileDrop = (e) => {
    if (!droppable) {
      return;
    }
    e.preventDefault();
    e.stopPropagation();

    const { files } = e.dataTransfer;
    const changedFile = files[0];
    setFile(changedFile);
    onChange && onChange(changedFile);
    setDragging(false);
  };

자세히보면 각 이벤트 핸들러마다 전파와 기본이벤트를 막는는 로직이 보인다.

children을 함수로 떤져주기

그리고 children을 함수로 던져줄수도 있다는걸 첨 알았다.

//Upload.js 로직 일부
{typeof children === "function" ? children(file, dragging) : children}

//사용할때
    <Upload droppable>
      {(file, dragging) => (
        <div
          style={{
            width: 300,
            height: 100,
            border: "4px dashed #aaa",
            borderColor: dragging ? "black" : "#aaa",
          }}
        >
          {file ? file.name : "클릭하거나 드래그하시오"}
        </div>
      )}
    </Upload>

children에게 prop을 전달할때 이렇게도 전달할수 있구나?

Badge컴포넌트

알림이 쌓인 수를 보여주는 UI를 Badge라고함!


출처:https://www.telerik.com/kendo-react-ui/badge

이렇게 생긴 UI임.
스타일은 제거하고 코드 올려보면..

const Badge = ({
  children,
  count,
  maxCount,
  backgroundColor,
  textColor,
  dot = false,
  showZero,
  ...props
}) => {
  let badge = null;

  const colorStyle = {
    backgroundColor,
    color: textColor,
  };

  if (count) {
    badge = (
      <Super style={colorStyle}>
        {maxCount && count > maxCount ? `${maxCount}+` : count}
      </Super>
    );
  } else {
    if (count !== undefined) {
      badge = showZero ? <Super style={colorStyle}>0</Super> : null;
    } else if (dot) {
      badge = (
        <Super className="dot" style={colorStyle}>
          0
        </Super>
      );
    }
  }

  return (
    <BadgeContainer {...props}>
      {children}
      {count > 0 || (count === 0 && showZero) ? badge : null}
    </BadgeContainer>
  );
};

이렇게 되어있다.
이정도 조건문은 괜찮은 걸까? 항상 볼때마다 고민이 많다. 아무래도 모르는게 많기 때문이겠지...
주말에 멘토님이 추천해주신 SOLID원칙을 다시 정독해봐야겠다.

Icon 컴포넌트

https://feathericons.com/ 에서 제공해주는 무료 svg아이콘을 한번 래핑해서 사용해보겠다. Vue강의시간에서 fonts.google을 래핑한것과 유사하구만

웹팩 버전이슈(Buffer 모듈)

노드의 Buffer모듈을 통하여 svg아이콘을 img에 넣으려했으나,

이렇게 오류가 남. 찾아보니 Webpack5버전부터 기존에 제공하던 node.js의 자동 폴리필을 제공하지 않는다. 번들사이즈를 줄이려는 노력이겠지?

아무튼 없다는 소리니까 일단 설치한뒤...필요한곳에서 import해서 사용하니 해결됐다.

import { Buffer } from "buffer";

const Icon = ({
  name,
  size = 16,
  strokeWidth = 2,
  color = "#222",
  rotate,
  ...props
}) => {
  const iconStyle = {
    "stroke-width": strokeWidth,
    storke: color,
    width: size,
    height: size,
  };
  const icon = require("feather-icons").icons[name];
  const svg = icon ? icon.toSvg(iconStyle) : "";
  const base64 = Buffer.from(svg, "utf8").toString("base64");

  return (
    <span {...props}>
      <img
        alt={name}
        style={{ transform: rotate && `rotate(${rotate}deg)` }}
        src={`data:image/svg+xml;base64,${base64}`}
      />
    </span>
  );
};
export default Icon;

그런데, 아이콘 태그를 <i>태그로 감싸는 부분이 의아했다. 글씨를 기울이는 태그였는데, 아이콘을 나타내는 태그기도 한것인가?
검색해봄
=> 다른 용도지만 잘 안쓰여서 찾아서 수정하기 편하고 icon의 첫글자와 같아서 사용한다.

시맨틱 태그로 그냥 나와주면 안되나?ㅋㅋㅋ

Avatar 컴포넌트

사용자의 프로필 사진을 나타내는데 쓰임. 특별히 모르는 부분과 이해 안가는 부분이 아예 없고 어제 작성했던 Image태그를 활용하는 부분이라 넘어간다.

다만 리액트에서 제공하는 React.cloneElement같은 메서드들의 쓰임새를 잘 기억해둘 필요가 있겠다.

React.isvalidelement()

자식을 렌더링해줄때 이 메서드를 사용한다. 그런데, 이게 왜 필요할까? 어차피 개발자는 필요한 컴포넌트만 자식으로 넣어서 사용할텐데 말이다.


공식문서에 나와있는 용법이다

아하 그러니까 React.cloneElement를 사용할때 예외사항(정말 의도치 않은 jSX외 요소)가 들어올때 예외처리를 하기위함이구나!
잘못하면 앱이 뻗거나 렌더링이 중단될테니 말이다.

안전이 최고!

Slider 컴포넌트


출처:https://design.procore.com/slider

이런 컴포넌트를 만든다.

const Slider = ({
  min = 0,
  max = 100,
  step = 0.1,
  defaultValue,
  onChange,
  ...props
}) => {
  const [dragging, setDragging] = useState(false);
  const [value, setValue] = useState(defaultValue ? defaultValue : min);
  const sliderRef = useRef(null);

  const handleMouseDown = useCallback(() => setDragging(true), []);
  const handleMouseUp = useCallback(() => setDragging(false), []);
  useEffect(() => {
    const handleMouseMove = (e) => {
      if (!dragging) {
        return;
      }
      const handleOffset = e.pageX - sliderRef.current.offsetLeft;
      const { offsetWidth: sliderWidth } = sliderRef.current;
      const track = handleOffset / sliderWidth;
      let newValue;
      if (track < 0) {
        newValue = min;
      } else if (track > 1) {
        newValue = max;
      } else {
        newValue = Math.round((min + (max - min) * track) / step) * step;
      }
      setValue(newValue);
      onChange && onChange(newValue);
    };
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [value, min, max, dragging, sliderRef, handleMouseUp, onChange, step]);

  const percentage = ((value - min) / (max - min)) * 100;

  return (
    <SliderContainer {...props} ref={sliderRef}>
      <Rail />
      <Track style={{ width: `${percentage}%` }} />
      <Handle
        onMouseDown={handleMouseDown}
        style={{ left: `${percentage}%` }}
      />
    </SliderContainer>
  );
};

특이한 부분은 없지만, 값을 잘 활용해서 핸들을 움직이는게 인상적이었다.
사용자 친화적 경험을 위해 최소-최대값을 설정하여 마우스가 넘어가면 최소-최대값을 넘겨준것도 좋았다.
이를 위해 useEffect에서 도큐먼트에 이벤트를 걸어주었다. 이후 return으로 청소해준다.


느낀점

타자가 일단 빨라야한다...ㅋㅋㅋ강사님들 보면 영타가 정말 빠르시다. 의식적으로 오타 없이 빨리치려고 노력해봐야겠다.
그리고 계속 컴포넌트를 배우는데...다 외우기엔 무리가 있다. 이해만 하고 넘어가자! 블랙박스학습법이다!
그렇게 어려운건 없어서 재밌었다 ㅎㅎ

profile
모르는 것을 모른다고 하기

0개의 댓글