프론트엔드 데브코스 5기 TIL 48 - 리액트 스타일링, 빌트인 훅, 스토리북

김영현·2023년 12월 5일
0

TIL

목록 보기
57/129

리액트에서의 스타일

보통 3가지로 나뉨. 인라인 스타일, CSS(SCSS)파일 빼서 두는 스타일 그리고 CSS in JS(emotion)
또한 상태의존 스타일은 인라인 스타일로 하는게 좋음.
=> 그러면 서타일 싯트로 빼서 쓰는게 가장 적은 비율이겠구먼?

일단 제일 많이 사용하는 emotion을 설치해보자!

emotion

일단 npm i @emotion/react로 설치해주고, 바벨 플러그인도 설치해준다. npm i -D @emotion/babel-plugin
이때 바벨 플러그인을 설정해주어야하는데, Creat-reat-app으로 만든 프로젝트는 이미 환경세팅이 다 되어있어서, 설정을 바꾸기 까다로움.

emotion CRA에서 사용하도록 설정하기

두가지방법이 존재함.

  • Pragma: use strict구문처럼 컴파일러에게 지시를 내린다. /** @jsxImportSource @emotion/react */를 적어주면 된다.
  • Craco: 설정을 덮어쓸 수있따.

일일히 적는건 귀찮으니 크라꼬가 나아보인다!
참고로 크라꼬는 CreatReactAppConfigOverride의 줄임말임ㅋㅋ

//craco.config.js
module.exports = {
  babel: {
    presets: ["@emotion/babel-preset-css-prop"],
  },
};

적어주면 잘 덮어쓴다. 플러그인은 설치하자.
이렇게하고

  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "craco eject"
  },

react-scripts로 실행하던걸 크라꼬로 바꿔주면 끝!

styled-component, css-prop

emotion의 스타일 방식은 두가지지로 나뉨.
styeld-compontnt, css-prop

스타일드컴포넌트는 알고있으니, css-prop를 알아보자.

const SomeComponent = ({ children }) => (
  <div css={{
      backgroundColor: 'hotpink',
      '&:hover': {
        color: 'lightgreen'
      }
    }>
    Some hotpink text.
    {children}
  </div>
);

이렇게 css라는 이름의 prop으로 넘겨준다.
간단한걸? 참고로 문자열로 넘겨주는 방식도 있지만, 이렇게 객체타입으로 넘겨주는 걸 권장한다.
타입스크립트로 타입 체킹이 용이하대나? 일단 써봐야 알 것 같음.


메모이제이션 훅

useMemo

함수의 연산된 값을 메모이제이션 하는 훅

컴포넌트는 함수로 구현됨. 그저 JSX를 반환하는 함수일뿐. 리액트 내부에서 컴포넌트 함수를 실행하여 렌더링함.
이때 내부에 구현된 함수들이나 변수들이 다시 실행되는 과정을 거치게 됨. => 리소스 낭비 발생.

특히 연산의 속도가 느린 컴포넌트라면 불쾌한 사용자경험 발생!

//App.js
  const [content, setContent] = useState("버튼");
  return (
    <div>
      <button onClick={() => setContent(content + ":")}>{content}</button>
      <ShowSum n={400000000} />
    </div>
  );
//ShowSum.js
const ShowSum = ({ n }) => {
  const sum = (n) => {
    let temp = 0;
    for (let i = 1; i <= n; i++) {
      temp += i;
    }
    console.log(temp);
    return temp;
  };
  const result = sum(n);
  return <div>{result}</div>;
};

이렇게 큰 값이 들어오는 연산은 시간이 오래걸림.
이때 부모의 상태가 변경되었지만, 상태가 변경되지 않은 자식이 리렌더링 되는 사이드이펙트 발생!

이럴때 useMemo를 사용하여 최적화를 해준다.

const result = useMemo(() => sum(n), [n]);

첫번째 인자는 반환값, 두번째 인자는 의존값이다. 의존값이 같다면 메모된값을 내려줌.

이렇게 큰 연산은 드물것같은데...잘못 사용하면 오히려 낭비일수도 있겠다

React.memo

부모에게서 받아온 props가 변경되었을때만 리-렌더링

비슷한 메모이제이션 기법이지만, 하는 기법이다. 위에서 부모 상태가 바뀌면 자식도 리렌더링이 일어난다했는데, 그걸 방지해줌.

export default React.memo(ShowSum);

이렇게해주면...

App.js에서 상태변화가 일어나도 ShowSum.js는 리렌더링 되지 않기에 연산이 이루어지지 않는다.

useCallback

함수 자체를 메모이제이션 함
아니, 함수는 연산이 오래걸릴뿐인데 왜 함수 자체를 메모이제이션 하는걸까?

//App.js
  const onSave = () => console.log("저장합니다");
  const [val, setVal] = useState(0);
  return (
    <div>
      <button onClick={() => setVal(val + 1)}>{val}</button>
      <Editor onSave={onSave} />
    </div>
  );
//Editor.js
const Editor = ({ onSave }) => {
  console.log("editor렌더링됨");
  return <div onClick={onSave}>Editor</div>;
};

분명 함수를 그냥 내려줬을 뿐인데, 버튼을 누를시 자식 컴포넌트 리 렌더링이 일어난다. 이때 React.memo를 사용해서 프롭스를 메모이제이션해도 역시나 리렌더링이 일어난다.

=>App.js가 리렌더링(함수 재실행)될때마다 onSave함수도 새로 생긴다. 함수는 일급 객체(참조형)이다. 따라서 이전에 있던 함수와 메모리 위치가 다르기때문에 다른 함수다...!! 이를 방지하기위하여 useCallback을 사용하는 것이다.

참고로 같은 내용의 함수가 들어올때 리렌더링을 방지하려면React.memo와 같이 사용해야 제 기능을 누릴수있다.

const Editor = ({ onSave }) => {
  console.log("editor렌더링됨");
  return <div onClick={onSave}>Editor</div>;
};

export default React.memo(Editor);

Custom Hook

커스텀훅이라 왠지 거창하고 어려워보이지만, 기존 훅을 조합하거나 자주 사용되는 로직을 따로 빼서 사용하는 거다. 그냥 중복코드를 함수로 빼낸다고 생각하면 됨.

useToggle

import { useCallback, useState } from "react";

const useToggle = (initialState = false) => {
  const [state, setState] = useState(initialState);
  const toggle = useCallback(() => setState((state) => !state), []);
  return [state, toggle];
};

export default useToggle;

useHove

import { useCallback, useEffect, useRef, useState } from "react";

const useHover = () => {
  const [state, setState] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = useCallback(() => setState(true), []);
  const handleMouseOut = useCallback(() => setState(false), []);

  useEffect(() => {
    const el = ref.current;
    if (el) {
      el.addEventListener("mouseover", handleMouseOver);
      el.addEventListener("mouseout", handleMouseOut);
      return () => {
        el.removeEventListener("mouseover", handleMouseOver);
        el.removeEventListener("mouseout", handleMouseOut);
      };
    }
  }, [ref, handleMouseOut, handleMouseOver]);
  return [ref, state];
};

export default useHover;

useCallback을 잘 사용하는구나?

useKeyPress

import { useCallback, useEffect, useRef, useState } from "react";

const useKeyPress = (targetKey) => {
  const [keyPressed, setKeyPreseed] = useState(false);
  const ref = useRef(null);
  const handleKeyDown = useCallback(
    ({ key }) => {
      if (key === targetKey) {
        setKeyPreseed(true);
      }
    },
    [targetKey]
  );
  const handleKeyUp = useCallback(
    ({ key }) => {
      if (key === targetKey) {
        setKeyPreseed(false);
      }
    },
    [targetKey]
  );
  useEffect(() => {
    const el = ref.current;
    if (el) {
      el.addEventListener("keydown", handleKeyDown);
      el.addEventListener("keyup", handleKeyUp);
      return () => {
        el.removeEventListener("keydown", handleKeyDown);
        el.removeEventListener("keyup", handleKeyUp);
      };
    }
  }, [ref, handleKeyDown, handleKeyUp]);
  return [ref, keyPressed];
};

export default useKeyPress;

이렇게 중복된 로직을 훅으로 따로 빼내면 보기 더 편해진다 ㅎㅎ


StoryBook

컴포넌트를 문서화 하고, 눈으로 바로 확인가능하고 상태별로 어떤 스타일을 줄지도 결정할수 있음!

npx storybook@latest init로 설치하면...
src/stories라는 폴더가 생성되고 그 안에 파일들이 잘 생성되있음ㅎㅎ
npm run storybook으로 실행해보면...
이런 화면이 나온다(벨로그 이미지 업로드 오류로인하여 다음에 첨부예정)

컴포넌트를 하나 만들어서 스토리북에 넣어보자.

//Box.js (컴포넌트)
const Box = ({ width = 100, height = 100, backgroundColor = "red" }) => {
  const style = {
    width,
    height,
    backgroundColor,
  };
  return <div style={style}></div>;
};

export default Box;

//stories/Box.stories.js
import Box from "../components/Box";

export default {
  title: "Example/Box",
  component: Box,
  //프롭스를 정해준다.
  argTypes: {
    width: { control: "number" },
    height: { control: "number" },
    backgroundColor: { control: "color" },
  },
};

//박스의 형태들을 export로 내보내준다.
export const Primary = {
  args: {
    primary: true,
  },
};

이렇게 하면 끝!
강의 버전과 현재 버전이 조금 달라서 쬐끔 이해하기 어려웠는데, 역시 공식문서를 보니 해결됐다.


로그인 폼 예제

지금까지 배운 내용을 토대로 로그인폼을 만들어본다. 스토리북도 사용하시는데, 시간이 좀 더 소요되지만 좋아보인다.

  1. 스토리북 액션주기
argTypes:{
	onClick:{action:"onClick"}
}
  1. useForm커스텀훅
import { useState } from "react";

const useForm = ({ initialValues, onSubmit, validate }) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = async (e) => {
    try {
      e.preventDefault();
      setIsLoading(true);
      const newErrors = validate(values);
      if (!Object.keys(newErrors).length) {
        await onSubmit();
      } else {
        setErrors(newErrors);
      }
    } catch (e) {
      console.log(e);
    } finally {
      setIsLoading(false);
    }
  };
  return { values, errors, isLoading, handleChange, handleSubmit };
};

코드 전부를 첨부하기엔 좀 길어서...중요 부분만 뽑아옴!


느낀점

바닐라 JS로 컴포넌트 => Vue => React순으로 배웠더니, React의 구조가 눈에 잘 들어온다.
살짝 Vue랑 헷갈리는 부분도 있긴한데 괜찮음ㅎㅎ 하다보면 명확해지겠지.
그리고 알고리즘 문제를 푸는게 굉장히 도움됐다. 커스텀 훅 같이 함수를 짤때 논리적으로 생각할수 있게됨...
뭐든 필요 없는 공부는 없다!

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

0개의 댓글