📚 서론

이번엔 React 에서 사용할 수 있는 다양한 CSS 기법들과 렌더링의 구조, 그리고 렌더링을 최적화할 수 있는 memo, useCallback, useMemo 훅에 대해 간략히 알아보자.

📘 CSS

📗 Inline Style

📕 직접 기술

return (
	<div style={{color: 'red'}}>
    	...
    </div>
)

📕 사전 정의 후 지정

const containerStyle = {
	width: "100%",
    ...
}
  
return (
	<div style={containerStyle}>
    	...
	</div>  
)

주의

  • 속성명은 카멜케이스
    • text-align => textAlign
  • 값은 문자열 혹은 수치
    • color: 'red', margin: 0

코드가 복잡해질수 있으니 인라인 스타일 사용을 지양하자

📗 CSS Module

.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 로 학습을 진행할 생각이다.

📕 CSS Module 사용법

주의 파일명.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 의 경우 해당 클래스의 스코프는 컴포넌트 안으로 한정되어 다른 컴포넌트에서 같은 이름으로 중첩되지 않는다.

📗 Styled JSX

컴포넌트 안에서 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 는 스타일을 적용한 컴포넌트를 정의할 수 있다. 이때 클래스를 지정하는 것이 아니라 스타일을 적용한 컴포넌트를 정의한다. 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>
    )
}

📗 Emotion

  • inline style, styled jsx, styled component 와 매우 유사한 형태로 작성할 수 있다.
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;
	...
`

📗 Tailwind CSS

보고 바로 bootstrap 같다는 느낌이 들었다. Tailwind CSS 는 유틸리티 우선 프레임워크이다. 이는 Tailwind CSS 가 className 에 설정할 수 있는 클래스명의 부품만 제공하고 개발자는 각각을 조합해 사용한다.

📕 Tailwind 설치 및 설정

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 사용방법

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 : 컴포넌트를 메모이제이션하여 부모 컴포넌트가 재렌더링 되어도 자녀 컴포넌트의 재렌더링을 방지

이게 무슨말일까? 재렌더링을 방지한다면 어떻게 이게 가능할까?

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

react.dev memo 예제

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 가 바뀔때는 재렌더링 되지 않는다.

📕 useCallback

함수 메모이제이션

만약 부모 컴포넌트에 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

  • 캐시하고 싶은 함수. 아무 인자를 가질 수 있고 그 어떤 값도 반환할 수 있다.
  • React 는 최초 렌더링 동안 이 함수를 반환한다.
  • React 는 마지막 렌더링 때까지 dependencies 가 변하지 않았다면 같은 함수를 반환한다. 그렇지 않으면 현재 렌더링중 전달한 함수를 제공하고 나중에 다시 사용하게끔 저장한다.

depdendencies

  • fn 에서 참조되고 있는 모든 반응형 값 ( state, props, 컴포넌트 바디안에 직접 선언된 변수나 함수 등과 같이 화면에 표시되는 동안 변할수 있는 값 )들의 리스트
  • dependencies 의 항목 수는 일정해야 하고 [dep1, dep2, dep3] 처럼 작성되어야 한다.

재렌더링 사이에 함수 정의를 캐싱하고 싶다면 컴포넌트의 최상단에 useCallback 을 호출해야 한다.

초기 렌더링시 useCallback 은 fn 함수를 반환하고 이어지는 렌더링 동안 dependencies 가 변하지 않았다면 마지막 렌더링시의 fn 함수를, 그렇지 않다면 이번 렌더링 동안의 fn 함수를 반환한다.

useCallback 이 맨 위에 정의되어 있으니 fn 는 렌더링과 관련없이 한 번만 생성된다.
그러나 dependencies 의 reactive value 가 변하면 이에 따라 재렌더링이 될때 새로 생성된다.

주의

  • useCallback 도 Hook 이니 컴포넌트 최상단에서 호출해야 하고 반복문이나 조건문 안에서 호출할 수 없다.

react.dev 의 useCallback 예제

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 으로 감싸야 한다.

📕 useMemo

변수 메모이제이션

const sum = useMemo(() => {
	return 1 + 3;
}, []);

변수 정의 로직이 복잡하거나 많은 수의 루프가 실행되는 경우에 사용하여 변수 설정에 의한 부하를 낮출 수 있다. 또한 의존 배열에 설정된 값을 참조하여 변수를 설정하는데 영향을 주는 외부값을 명시적으로 나타낼 수 있어 가독성에 좋다.

음.. 아직 이해가 잘 안된다. 바로 공식문서를 뜯어봤다.

useMemo 은 재렌더링 사이에 계산의 결과를 캐시해주는 React 훅이다.

const cachedValue = useMemo(calculateValue, dependencies)

calculateValue

  • 캐시하고 싶은 값을 계산하는 함수
  • 순수함수 (매 실행마다 동일값 반환) 이고, 인자를 갖지 말아야 하고 아무 타입의 값을 반환해야 한다.
  • 처음 렌더링시 React 는 이 함수를 호출한다. 그 다음 렌더링때 마지막 렌더링때까지 의존배열이 바뀌지 않았다면 React 는 같은 값을 반환한다. 그렇지 않고 바뀌었다면 calculateValue 를 호출하여 그 결과를 반환하고 값을 저장하여 이후에 다시 사용될 수 있게 한다.

dependencies

  • calculateValue 코드에서 참조하고 있는 모든 reactive value 의 리스트
  • 나머지 조건은 useCallback 과 동일

재랜더링 사이 계산을 캐시하고 싶다면 컴포넌트의 최상단에 useMemo 훅을 호출해야 한다.

react.dev 의 useMemo 예제

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

useMemo 가 갖는 calcualteValue 함수가 순수함수고 인자를 갖지 말아야 한다는 것은 처음 알게 되었다.

react.dev 의 useMemo 예제

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 는 사용되는 사례를 종종 봤으나 정확히 어떤 동작을 하는지는 몰랐었는데 자세하진 않더라도 렌더링 최적화의 측면에서 어떤 기능을 하는지 정도를 정리해볼 수 있었다.

최근에 좋은 글들을 보면서 블로그는 어떻게 해야 잘 쓸 수 있을까에 대한 고민을 해봤다. 이전에 세미나를 준비할때도 비슷하게 조언을 들었던 것 중에 설명할때는 상대방이 초등학생이라도 이해할 수 있게끔 쉽게 전달하고자 해야 한다는 게 기억에 남는다. 되도록이면 누구나 한번에 이해할 수 있게끔 풀어 설명한다면 나 역시도 더 깊은 이해를 할 수 있을것 같다.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글