이번엔 React 에서 사용할 수 있는 다양한 CSS 기법들과 렌더링의 구조, 그리고 렌더링을 최적화할 수 있는 memo, useCallback, useMemo 훅에 대해 간략히 알아보자.
return (
<div style={{color: 'red'}}>
...
</div>
)
const containerStyle = {
width: "100%",
...
}
return (
<div style={containerStyle}>
...
</div>
)
주의
- 속성명은 카멜케이스
- text-align => textAlign
- 값은 문자열 혹은 수치
- color: 'red', margin: 0
코드가 복잡해질수 있으니 인라인 스타일 사용을 지양하자
.css 나 .scss 파일을 정의하는 방법이다. CSS Module 을 사용하면 스타일 중복 적용을 방지할 수 있다.
// CssModule.jsx
export const CssModule = () => {
return (
<div>
...
</div>
)
}
scss 설치
// sass 글로벌 설치
npm install sass -g
// sass 현재 버전 조회
npm show sass version
// scss 폴더에 변화가 생기면 소스맵이 생성되지 않는 조건으로 스스로 컴파일하도록 설정
sass --watch --no-source-map scss:css
혹은
npm install node-sass
- npm install node-sass
- node-sass 는 sass(scss) 파일을 css 로 변환하는 노드 node.js 기반 패키지이다. 더 많은 기능을 원할시 node-sass 를 사용하자. 단, 노드 버전에 의존적이므로 노드 버전을 올리면 오류가 발생할 확률이 높다. 혹은 dart sass 를 사용하는 방법도 고려할 수 있다.
- npm install sass
- node-sass 와 비교해 기능적으로 더 단순하다. sass 기능중 libsass 만 지원하고 node.js 바인딩이 아닌 다른 구현체를 사용한다.
일단 npm install sass 로 학습을 진행할 생각이다.
주의
파일명.module.scss 라는 명칭으로 파일을 생성할 것
// CssModules.module.scss
.container {
width: 100%;
margin: 8px;
...
}
.title {
margin: 0;
...
}
.button {
border: none;
...
&:hover {
color: #fff;
cursor: pointer;
}
}
//CssModuels.jsx
import classes from './CssModules.module.scss';
export const CssModule = () => {
return (
<div className={classes.container}>
...
)
}
CSS Module 의 경우 해당 클래스의 스코프는 컴포넌트 안으로 한정되어 다른 컴포넌트에서 같은 이름으로 중첩되지 않는다.
컴포넌트 안에서 style 태그를 사용해 CSS 를 기술한다. 이때 style 태그에는 jsx 표기를 사용해야 한다.
Styled JSX 표기법에서는 SCSS 표기법은 사용할 수 없다.
npm install styled-jsx
// StyledJsx.jsx
export const StyledJsx = () => {
return (
<div className="container">
<p className="title">Styled JSX 테스트</p>
</div>
<style jsx>{`
.container {
...
}
.title {
...
}
`}</style>
)
}
Styled Component 는 스타일을 적용한 컴포넌트를 정의할 수 있다. 이때 클래스를 지정하는 것이 아니라 스타일을 적용한 컴포넌트를 정의한다. Styled Component 는 SCSS 의 표기법을 그대로 이용한다.
npm install styled-components
// StyledComponent.jsx
import styled from 'styled-components';
const SContainer = styled.div`
...
`;
const STitle = styled.p`
...
`;
// props 를 이용시
const BoxOne = styled.div`
background-color: ${(props) => props.backgroundColor};
`
// 컴포넌트 상속받아 사용시
const BoxTwo = styled(BoxOne)`
border: 1px solid red;
`
// 기존 태그를 이름만 바꿔서 사용시 => as 키워드 사용
const Btn = styled.div`
color: white;
padding: 10px 15px;
`
// attribute 를 넣고 싶을때, tag 옵션 넣는 방법
const Input = styled.input.attrs({ required: true })``;
// 중첩시 자식 컴포넌트에 특정 컴포넌트가 올때 적용할 스타일 지정가능
/* sass 처럼 중첩구조 가능 */
const BoxThree = styled.div`
${Input} {
background-color: yellow;
}
p {
&:hover {} // sass 처럼 & 기호 사용 가능
}
`;
export const StyledComponent = () => {
return (
<SContainer>
<STitle>Styled Component 테스트</STitle>
<BoxOne backgroundColor={"navy"}></BoxOne>
<BowTwo />
<Btn as={"button"} />
<Input />
<BoxThree />
</SContainer>
)
}
npm install @emotion/react @emotion/styled
// Emotion.jsx
import { jsx, css } from '@emotion/react';
import styled from '@emotion/styled';
// scss 와 동일하게 작성 가능하다.
export const Emotion = () => {
// 컴포넌트 파일 안에 CSS 작성시
const containerStyle = css`
padding: 8px;
...
`;
// 자바스크립트 객체 안에 스타일 작성시
const titleStyle = css({
margin: 0,
...
});
return (
<div css={containerStyle}>
<p css={titleStyle}>Styled JSX 테스트</p>
<SButton>버튼</SButton>
</div>
)
}
// styled-components 사용시
const SButton = styled.button`
border: none;
...
`
보고 바로 bootstrap 같다는 느낌이 들었다. Tailwind CSS 는 유틸리티 우선 프레임워크이다. 이는 Tailwind CSS 가 className 에 설정할 수 있는 클래스명의 부품만 제공하고 개발자는 각각을 조합해 사용한다.
PostCSS 를 사용시
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Creat React App 을 사용시 PostCSS 를 덮어 쓸 수 없으니 CRACO ( Create React App Configuration Overide) 를 사용한다.
npm install @craco/craco
// TailwindCss.jsx
export const TailwindCss = () => {
return (
<div>
<p>Tailwind CSS 배워보자!</p>
</div>
)
}
// package.json
{
// "start": "react-scripts start"
// "build": "react-scripts build"
// "test": "react-scripts test"
"start": "craco start"
"build": "craco build"
"test": "craco test"
}
프로젝트 루트에 craco.config.js, tailwind.config.js 작성
// craco.config.js
module.exports = {
style: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}
}
}
npx tailwindcss init
// tailwind.config.js
module.exports = {
purge: [], // 파일 안에서 사용하지 않는 스타일이 있을때 삭제하는 옵션
darkMode: false, // 'media' or 'class'
theme: {
extend: {}, // 디자인 시스템 정의
},
variants: {
extend: {}, // 특정 css 클래스 변형 확장
},
plugins: [], // tailwind css 플러그인 추가
}
darkMode
- false: 어두운 테마 비활성화
- media: 미디어 쿼리를 사용해 시스템 어두운 테마 설정에 따라 자동으로 테마 변경
- class: 클래스를 추가하여 수동으로 어두운 테마 활성화
index.css 상단에 추가
@tailwind base;
@tailwind components;
@tailwind utilities;
Tailwind CSS 사용시 각 태그의 className 속성에 직접 정의한 클래스명을 설정해서 Tailwind CSS 가 제공하는 클래스명을 지정하면 된다.
export const TailwindCss = () => {
return (
<div className="border border-gray p-2">
<p className="m-0 text-gray-400">Tailwind CSS 배워보자!</p>
</div>
)
}
개인적으로는 태그 하나하나에 클래스명이 너무 길어져서 구체적인 설정을 하기 어려울 것 같았다. 또한 bootstrap 역시 사용하기 좀 불편한 느낌이었는데 비슷한 것 같아서 자주 사용하게 될 것 같지 않았다.
State 가 업데이트된 컴포넌트
state 는 컴포넌트 상태를 나타내는 변수라 업데이트될 때 재렌더링되지 않으면 화면 표시를 올바르게 저장할 수 없다.
Props 가 변경된 컴포넌트
리액트 컴포넌트는 props 를 인수로 받고 이에 따라 렌더링 내용을 결정하니 props 값이 바뀔 때는 재렌더링 해서 출력 내용을 변경해야 한다.
재렌더링된 컴포넌트 아래의 모든 컴포넌트
특정 컴포넌트가 재렌더링 되었다면 이 컴포넌트의 자손 컴포넌트들은 모두 재렌더링된다. 이외의 컴포넌트는 표시가 변하는게 아니니 재렌더링하지 않아도 문제가 없다.
리액트에서 컴포넌트, 변수, 함수 등을 재렌더링할때 제어가 필요하면 메모이제이션 memoization 을 수행한다.
memoization
: 이전 처리 결과를 저장해둬서 처리 속도를 높이는 기술
컴포넌트 메모이제이션
memo
: 컴포넌트를 메모이제이션하여 부모 컴포넌트가 재렌더링 되어도 자녀 컴포넌트의 재렌더링을 방지
이게 무슨말일까? 재렌더링을 방지한다면 어떻게 이게 가능할까?
memo
는 props 가 변하지 않았다면 컴포넌트의 재렌더링을 건너뛴다. 이러면 조금 이해갈 될 것 같다.
const Component = memo(() => {});
memo
는 props 에 변경이 있을 때만 재렌더링 된다.
// Memoization.jsx
export const Memoization = memo(() => {
return (
<div>
...
</div>
)
});
이렇게 컴포넌트를 memo
로 감싸서 불필요한 재렌더링을 방지할 수 있다는데 그럼 모든 컴포넌트를 다 memo
로 감싸야 하는것일까?
아니다.
모든 컴포넌트를 memo
로 감싸는 것은 오히려 성능 저하가 발생할 수도 있다. memo
는 어떤 컴포넌트가 재렌더링 될 때 해당 컴포넌트가 받는 props 의 변화를 감지하여 변화가 없다면 재렌더링을 건너뛰도록 하나 모든 컴포넌트를 memo
로 감싸면 오히려 흐름도 복잡하고 관리하기 어려워진다. 대신 성능 최적화가 필요한 큼 컴포넌트에만 memo
를 사용하자.
const MemoizedComponent = memo(Component, arePropsEqual?)
어떤 컴포넌트의 memoized 된 버전을 갖고 싶다면 컴포넌트를 memo 로 감싸자.
Component
: memoize 하고싶은 컴포넌트, memo
는 이 컴포넌트를 변경하지 않으나 memoized 된 새로운 컴포넌트를 반환한다.
선택적인 arePropsEqual
: 컴포넌트의 이전 props 와 새 props 를 두 인자로 갖는 함수, 이 함수는 두 props 가 동일할때 true 를 반환해야 하고 그렇지 않다면 false 를 반환해야 한다. 기본적으로 이 함수를 명시하지 않고 React 가 Object.is
를 통해 각 prop 를 비교한다.
Object.is
static 메서드는 두 값이 같은 값인지 결정한다.
console.log(Object.is('1', 1));
// Expected output: false
console.log(Object.is(NaN, NaN));
// Expected output: true
console.log(Object.is(-0, 0));
// Expected output: false
const obj = {};
console.log(Object.is(obj, {}));
// Expected output: false
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
});
위의 예제에서 Greeting 은 name 이 바뀔때 재렌더링되나 address 가 바뀔때는 재렌더링 되지 않는다.
함수 메모이제이션
만약 부모 컴포넌트에 state 와 setState 가 있고 이때 자식 컴포넌트에 props 로 state 를 바꾸는 setState 가 포함되어 있는 이벤트 트리거 함수를 props 로 전달한다고 쳐보자.
이 경우 위에서 memo
로 컴포넌트를 감쌌던 것으로는 자식 컴포넌트의 재렌더링을 막을 수 없다.
함수를 props 에 전달할때 컴포넌트를 메모이제이션해도 재렌더링되는 까닭은 함수가 다시 생성되기 때문이다.
const onClick = () => {
setNum(0);
};
이런 함수는 재렌더링되면 새로운 함수가 생성된다.
즉, 이런 이야기다.
- 컴포넌트는 state, props, 부모 컴포넌트 변경시 자손 컴포넌트들을 재렌더링한다.
- props 에 같은 함수를 전달하는 것 같았지만 부모 컴포넌트가 재렌더링될때마다 새로운 함수가 생성되었고 매번 다른 함수를 props 에 전달했으니 props 가 바뀌어서 자손 컴포넌트들도 재렌더링이 된 것이다.
그럼 어떻게 해야 할까?
이때 사용되는게 useCallback, 함수 메모이제이션 기능이다.
const onClickButton = useCallback(() => {
...
}, []);
useCallback
은 재렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 리엑트 훅이다.
const cachedFn = useCallback(fn, dependencies)
fn
dependencies
가 변하지 않았다면 같은 함수를 반환한다. 그렇지 않으면 현재 렌더링중 전달한 함수를 제공하고 나중에 다시 사용하게끔 저장한다. depdendencies
fn
에서 참조되고 있는 모든 반응형 값 ( state, props, 컴포넌트 바디안에 직접 선언된 변수나 함수 등과 같이 화면에 표시되는 동안 변할수 있는 값 )들의 리스트dependencies
의 항목 수는 일정해야 하고 [dep1, dep2, dep3] 처럼 작성되어야 한다. 재렌더링 사이에 함수 정의를 캐싱하고 싶다면 컴포넌트의 최상단에
useCallback
을 호출해야 한다.
초기 렌더링시 useCallback 은 fn
함수를 반환하고 이어지는 렌더링 동안 dependencies
가 변하지 않았다면 마지막 렌더링시의 fn
함수를, 그렇지 않다면 이번 렌더링 동안의 fn
함수를 반환한다.
useCallback 이 맨 위에 정의되어 있으니
fn
는 렌더링과 관련없이 한 번만 생성된다.
그러나dependencies
의 reactive value 가 변하면 이에 따라 재렌더링이 될때 새로 생성된다.
주의
- useCallback 도
Hook
이니 컴포넌트 최상단에서 호출해야 하고 반복문이나 조건문 안에서 호출할 수 없다.
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
함수를 재렌더링 사이에 캐시하고 싶다면 함수의 정의를 useCallback
훅으로 감싸자.
자바스크립트에서 function () {}
이나 () => {}
는 항상 다른 함수를 생성한다. 비슷하게 {}
객체 리터럴도 항상 새 객체를 생성한다. 이는 props 로 함수를 전달시 memo
로는 최적화를 할 수가 없다는 것을 의미한다.
memo
는 reactive value 에 변화가 있을때에만 재렌더링하는데 함수는 항상 변화가 있으니까!
그래서 props 로 setState 같은게 담긴 함수를 전달할때 최적화를 하려면 해당 함수를 useCallback 으로 감싸야 한다.
변수 메모이제이션
const sum = useMemo(() => {
return 1 + 3;
}, []);
변수 정의 로직이 복잡하거나 많은 수의 루프가 실행되는 경우에 사용하여 변수 설정에 의한 부하를 낮출 수 있다. 또한 의존 배열에 설정된 값을 참조하여 변수를 설정하는데 영향을 주는 외부값을 명시적으로 나타낼 수 있어 가독성에 좋다.
음.. 아직 이해가 잘 안된다. 바로 공식문서를 뜯어봤다.
useMemo
은 재렌더링 사이에 계산의 결과를 캐시해주는 React 훅이다.
const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
calculateValue
를 호출하여 그 결과를 반환하고 값을 저장하여 이후에 다시 사용될 수 있게 한다.dependencies
calculateValue
코드에서 참조하고 있는 모든 reactive value 의 리스트재랜더링 사이 계산을 캐시하고 싶다면 컴포넌트의 최상단에 useMemo
훅을 호출해야 한다.
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
useMemo 가 갖는 calcualteValue 함수가 순수함수고 인자를 갖지 말아야 한다는 것은 처음 알게 되었다.
import { useMemo } from 'react';
import { filterTodos } from './utils.js'
export default function TodoList({ todos, theme, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
return (
<div className={theme}>
<p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>
{todo.completed ?
<s>{todo.text}</s> :
todo.text
}
</li>
))}
</ul>
</div>
);
}
출처 : 벨로퍼트와 함께하는 모덴 리엑트 6. 조건부 렌더링 댓글의 예시
javascript에서는 null,false,undefined를 렌더링 하면 아무것도 나타나지 않는다.
삼항연산자를 이용한 조건부 렌더링
<div style={{ color }}>
{ isSpecial ? <b>*</b> : null }
안녕하세요 {name}
</div>
논리곱
<div style={{ color }}>
{isSpecial && <b>*</b>}
안녕하세요 {name}
</div>
논리곱은 왼쪽에 오는 값이 Truthy 일때 오른쪽 값을 그대로 반환하고 Falsy 라면 왼쪽 값을 그대로 반환한다. Falsy 라면 null, false, undefined 에 해당할테니 아무것도 안나탈 것이고 내가 원하던 대로 특정 조건이 참일때만 렌더링하고 싶은 JSX 노드를 표시할 수 있다.
docs
react.dev - Hooks
book
모던 자바스크립트로 배우는 리엑트 입문
blog
벨로퍼트와 함께하는 모던 리액트
nana_log - sass
CSS 기법에 어떻게 이렇게 다양한게 있는지 오늘또 새로운 지식을 알게 되었다. 그저 CSS Module, Styled Component, Sass 만 알고 있었는데 다른 방법들도 장점이 있어 사용하게 될 것 같다.
memo, useMemo, useCallback 는 사용되는 사례를 종종 봤으나 정확히 어떤 동작을 하는지는 몰랐었는데 자세하진 않더라도 렌더링 최적화의 측면에서 어떤 기능을 하는지 정도를 정리해볼 수 있었다.
최근에 좋은 글들을 보면서 블로그는 어떻게 해야 잘 쓸 수 있을까에 대한 고민을 해봤다. 이전에 세미나를 준비할때도 비슷하게 조언을 들었던 것 중에 설명할때는 상대방이 초등학생이라도 이해할 수 있게끔 쉽게 전달하고자 해야 한다는 게 기억에 남는다. 되도록이면 누구나 한번에 이해할 수 있게끔 풀어 설명한다면 나 역시도 더 깊은 이해를 할 수 있을것 같다.