보통 3가지로 나뉨. 인라인 스타일, CSS(SCSS)파일 빼서 두는 스타일 그리고 CSS in JS(emotion)
또한 상태의존 스타일은 인라인 스타일로 하는게 좋음.
=> 그러면 서타일 싯트로 빼서 쓰는게 가장 적은 비율이겠구먼?
일단 제일 많이 사용하는 emotion을 설치해보자!
일단 npm i @emotion/react로 설치해주고, 바벨 플러그인도 설치해준다. npm i -D @emotion/babel-plugin
이때 바벨 플러그인을 설정해주어야하는데, Creat-reat-app으로 만든 프로젝트는 이미 환경세팅이 다 되어있어서, 설정을 바꾸기 까다로움.
두가지방법이 존재함.
use strict구문처럼 컴파일러에게 지시를 내린다. /** @jsxImportSource @emotion/react */를 적어주면 된다.일일히 적는건 귀찮으니 크라꼬가 나아보인다!
참고로 크라꼬는 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로 실행하던걸 크라꼬로 바꿔주면 끝!
emotion의 스타일 방식은 두가지지로 나뉨.
styeld-compontnt, css-prop
스타일드컴포넌트는 알고있으니, css-prop를 알아보자.
const SomeComponent = ({ children }) => (
<div css={{
backgroundColor: 'hotpink',
'&:hover': {
color: 'lightgreen'
}
}>
Some hotpink text.
{children}
</div>
);
이렇게 css라는 이름의 prop으로 넘겨준다.
간단한걸? 참고로 문자열로 넘겨주는 방식도 있지만, 이렇게 객체타입으로 넘겨주는 걸 권장한다.
타입스크립트로 타입 체킹이 용이하대나? 일단 써봐야 알 것 같음.
함수의 연산된 값을 메모이제이션 하는 훅
컴포넌트는 함수로 구현됨. 그저 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]);
첫번째 인자는 반환값, 두번째 인자는 의존값이다. 의존값이 같다면 메모된값을 내려줌.
이렇게 큰 연산은 드물것같은데...잘못 사용하면 오히려 낭비일수도 있겠다
부모에게서 받아온 props가 변경되었을때만 리-렌더링
비슷한 메모이제이션 기법이지만, 하는 기법이다. 위에서 부모 상태가 바뀌면 자식도 리렌더링이 일어난다했는데, 그걸 방지해줌.
export default React.memo(ShowSum);
이렇게해주면...
App.js에서 상태변화가 일어나도 ShowSum.js는 리렌더링 되지 않기에 연산이 이루어지지 않는다.
함수 자체를 메모이제이션 함
아니, 함수는 연산이 오래걸릴뿐인데 왜 함수 자체를 메모이제이션 하는걸까?
//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);
커스텀훅이라 왠지 거창하고 어려워보이지만, 기존 훅을 조합하거나 자주 사용되는 로직을 따로 빼서 사용하는 거다. 그냥 중복코드를 함수로 빼낸다고 생각하면 됨.
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;
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을 잘 사용하는구나?
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;
이렇게 중복된 로직을 훅으로 따로 빼내면 보기 더 편해진다 ㅎㅎ
컴포넌트를 문서화 하고, 눈으로 바로 확인가능하고 상태별로 어떤 스타일을 줄지도 결정할수 있음!
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,
},
};
이렇게 하면 끝!
강의 버전과 현재 버전이 조금 달라서 쬐끔 이해하기 어려웠는데, 역시 공식문서를 보니 해결됐다.
지금까지 배운 내용을 토대로 로그인폼을 만들어본다. 스토리북도 사용하시는데, 시간이 좀 더 소요되지만 좋아보인다.
argTypes:{
onClick:{action:"onClick"}
}
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랑 헷갈리는 부분도 있긴한데 괜찮음ㅎㅎ 하다보면 명확해지겠지.
그리고 알고리즘 문제를 푸는게 굉장히 도움됐다. 커스텀 훅 같이 함수를 짤때 논리적으로 생각할수 있게됨...
뭐든 필요 없는 공부는 없다!