React Testing Library

카테고리 React

📌 React Testing Library

리액트에서 TDD 방식의 개발을 하기 위해서 테스팅 라이브러리를 사용해보자.

🎯 목적

1. 버그 캐치

테스트를 통해 예상치 못한 여러가지 버그들을 사전에 확인하기 용이하다.

2. 애플리케이션 신뢰도 향상

어떠한 근거로 이 애플리케이션이 제대로 동작하는지 근거를 뒷받침하고 이 근거에 대한 신뢰도를 높일 수 있다.

3. 질문 및 답변 시간 축소

누군가 어떤 컴포넌트가 어떻게 동작하는지 질문했을 때 그에대한 답변으로 이 테스트를 보여주기만 하면된다. 그럼으로 시간을 절약하고 유지보수성을 높일 수 있다.

4. 문서 역할

테스트라는 문서를 제공함으로서 2,3번의 장점을 가능하도록 한다.

💼 사용방법

우선 해당 라이브러리를 설치해준다.

1
2
3
4
5
npm install --save-dev @testing-library/react

npm install --save-dev @testing-library/dom

npm install --save-dev @testing-library/user-event

리액트 테스팅 라이브리러DOM 테스트 라이브러리, 사용자 행동 테스트 라이브러리를 설치하여 테스트 개발을 해보자.

🦖 Component 테스트

✏️ 테스트 코드 작성하는 방법

  1. 테스트가 필요한 컴포넌트 렌더링

  2. 컴포넌트의 요소 탐색

  3. 요소와의 상호작용

  4. 어설션 테스트 결과와 기대 값이 일치하는 지 확인

우선 컴포넌트를 생성해주자.

📌 Tip 컴포넌트 생성

컴포넌트를 쉽게 생성하기 위해 yamoo9님이 제공해준 Tool을 사용해보도록하자.

1
npx degit yamoo9/create-react-component create-react-component
1
2
3
4
5
6
7
8
// package.json
{
...
"scripts" :
...,
"rc": "node create-react-component create",
"rd": "node create-react-component delete"
}
1
2
npm run rc -- 컴포넌트_이름 // 컴포넌트 생성
npm run rd -- 컴포넌트_이름 // 컴포넌트 제거

ESLint 에서 테스팅 라이브러리를 사용하게되면 오류를 띄워주는데 이에 대한 Lint 경고를 꺼두자.

1
2
3
4
5
// package.json
{
"eslintConfig":{...},
"testing-library/no-debugging-utils": "off"
}

예제

1. 컴포넌트가 렌더링 확인

1
2
3
4
// ToggleButton.jsx
export function ToggleButton({ onText, offText, on }) {
return <div>{on ? onText : offText}</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ToggleButton.test.jsx
import { render, screen } from "@testing-library/react";
import { ToggleButton } from "./ToggleButton";

describe.only("ToggleButton Test Start!", () => {
test("컴포넌트가 정상적으로 렌더링 되었습니다.", () => {
render(<ToggleButton onText="1" offText="0" />);

const offTextElement = screen.getByText("0");
const onTextElement = screen.queryByText("1");

expect(offTextElement).toBeInTheDocument();
expect(onTextElement).not.toBeInTheDocument();
});
});
  • getByText() : 가상으로 그려진 문서에 존재하는 것만 가져올 수 있다. 만약 존재 하지 않는 다면 오류를 발생시킨다.
  • queryByText() : 존재하지 않으면 오류를 발생시키지 않고 null 값으로 가져온다.

2. 활성화 상태 여부에 따라 텍스트 표시

1
2
3
4
// ToggleButton.jsx
export function ToggleButton({ onText, offText, on }) {
return <button type="button">{on ? onText : offText}</button>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ToggleButton.test.jsx
describe("ToggleButton 컴포넌트", () => {
test("활성 상태 여부에 따라 활성(ON)/비활성(OFF) 텍스트가 표시됩니다.", () => {
let onText = "ON";
let offText = "OFF";

render(<ToggleButton onText={onText} offText={offText} />);

let elements = screen.queryAllByRole("button");
let firstElement = elements[0];
expect(firstElement).toHaveTextContent(offText);

screen.debug();

cleanup();

render(<ToggleButton onText={onText} offText={offText} on />);

elements = screen.queryAllByRole("button");
firstElement = elements[0];

screen.debug();

expect(firstElement).toHaveTextContent(onText);
});
});
  • 만약 on 일때의 text와 off일 때의 text를 찾고 싶다면 queryAllText() 를 사용해준다,
  • cleanup()을 해줘야지만 firstElement를 확인할 때, 앞에 그려진 것을 지우고 새로 그려진 것을 비교해줄 수 있다.

3. onToggle 속성(prop)에 연결된 함수 실행 확인 & 활성 상태 컴포넌트는 ToggleButton--on 클래스 이름 포함 확인

1
2
3
4
5
6
7
8
9
10
11
12
// ToggleButton.js
export function ToggleButton({ onText, offText, on, onToggle }) {
return (
<button
type="button"
className={`ToggleButton ${on ? "ToggleButton--on" : ""}`.trim()}
onClick={onToggle}
>
{on ? onText : offText}
</button>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ToggleButton.test.js
describe("ToggleButton 컴포넌트", () => {
test("`onToggle` 속성(prop)에 연결된 함수가 정상적으로 실행됩니다.", () => {
let expected = "triggering toggle event";
let received = "";

render(
<ToggleButton
onToggle={() => {
received = expected;
}}
/>
);

expect(received).not.toBe(expected);

const element = screen.queryByRole("button");

fireEvent.click(element); // click button element

expect(received).toBe(expected);
});

test(`활성 상태의 컴포넌트는 'ToggleButton--on' 클래스 이름을 포함한다.`, () => {
let expected = "ToggleButton--on";
render(<ToggleButton on />);

const element = screen.getByRole("button");
expect(element).toHaveClass(expected);
});
});
  • fireEvent() : 이벤트를 발생시켜주는 메서드이다.

  • queryByRole() : 해당 요소의 역할을 확인하는데 사용된다.

    ex) button 태그에 type을 “button”으로 명시적으로 작성하였는지…

🏓 소감

오늘 수업에서 상태를 가지지 않는 컴포넌트의 다양한 테스트 방법에 대해 실습을 진행하였다. 리액트를 TDD 방식으로 개발을 진행하게 된다면 앞서 말한 애플리케이션의 신뢰도 향상할 수 있고 테스트 문서를 생성하여 유지보수를 용이하게 할 수 있다는 생각이 들었다.

아직 jest에 익숙하지 않아 낯설고 어렵지만, 우테코에서도 jest를 사용하고 있고 앞으로 자주 사용해보면서 테스트 주도 개발에 대해 몸을 익히도록 해야겠다.

댓글 공유

📌 JSX가 뭐길래?

JSX는 리액트 컴포넌트 사용을 HTML 처럼 사용하기 위해 생겨났다. JSX는 XML 처럼 생긴 문법 표현식이다.

1
2
3
4
5
6
const name = "loco";

const element = <h1>안녕하세요~! {name}</h1>;

// rendered
<h1>안녕하세요~! loco</h1>;

하지만 우리는 이렇게 편리한 JSX를 바로 사용할 수 없다. 왜냐하면 브라우저는 HTML, CSS, Javascript 파일만 읽을 수 있으므로 JSX 문법은 읽을 수 없다.

하지만 이렇게 편리한데 사용하고 싶으니 babel이라는 컴파일러를 사용한다.

🌈 babel이 뭐길래?

Babel은 자바스크립트 컴파일러로 아래의 역할을 하여 브라우저 호환 문제를 해결한다.

  • 구문 변환
  • 대상 환경에 누락된 폴리필 기능
  • 소스 코드 변환

babel을 사용하면 최신 문법으로 작성한 코드를 원하는 브라우저 환경에 잘 동작하도록 알아서 코드를 변환한 새로운 파일을 생성해준다.

또한, 애플리케이션이 방대해짐에 따라 한곳에서 파일을 관리하기가 어려워 모듈로써 파일을 구분하기 시작했다.

그렇게 파일을 구분을 하긴 했는데 그 많은 파일을 선후 관계를 따져가면서 HTML 파일에 등록시키고 최적화 하는 작업은 매우 번거로운 작업이다.

이러한 배경이 있기에 webpack이라는 모듈 번들러가 등장하였다.

🌏 webpack이 뭐길래?

앞서 설명했듯이 webpack은 모듈 번들러이다. 웹 브라우저 환경은 CBD, Module 프로그래밍에 적합하지 않다.

왜냐하면 브라우저 호환성때문이다. 모던 브라우저가 모듈을 사용할 수 있긴 하지만 모든 브라우저에서 가능한 것은 아니다. 또한, 브라우저는 애플리케이션을 build하는 환경을 기본적으로 제공하지 않기에 애플리케이션을 build하기 위한 환경을 개발자가 직접 설정해줘야 한다.

☄️ webpack을 사용하는 이유

1. 모듈 번들링

애플리케이션이 방대해짐에 따라 복잡성이 증가하여 코드가 혼란스러워지고 뒤섞이게 되어 개발이 어려워지는 문제가 발생하였다.

이를 해결하기 위해 코드 베이스를 모듈로 관리하고 번들링 해줘야한다.

2. 트리 쉐이킹(Tree Shaking)

애플리케이션에서 사용되지 않는 코드는 최적화 과정을 통해 제거되어 꼭 필요한 코드만 번들에 포함되도록 한다.

3. 코드 분할 (Code Spliting)

번들링으로 한곳에 묶기는 하였는데 이게 또 너무 크다보니 로드시간이 길어지는 문제가 발생하였다. 이를 해결하기 위해 코드 분할을 하여 런타임 중에 나눠진 파일들을 동적으로 불러오도록 하였다.

4. 코드 최적화와 소스맵(Source Map)

주석, 공백, 긴 함수명 등을 모두 축소 또는 제거하여 파일의 크기를 크게 줄일 수 있다. 이렇게 최적화된 코드는 사람이 읽기 힘들기에 디버깅하기가 어렵다.

이를 위해 코드를 추적하는 소스맵(Source Map)을 사용하여 버그를 추적하기가 용이하게 되었다.

결과적으로 편리한 JSX를 사용하기 위해서는 babel이라는 컴파일러로 변환은 하여 브라우저가 읽을 수 있는 파일로 변환해주어야 하며, 브라우저는 애플리케이션을 build 해주는 환경을 기본 제공해주지 않고 수많은 모듈 파일들을 번들링해주는 도구로서 webpack을 사용한다.

🏓 소감

수업을 하면서 개발을 하기 위한 Tool이 왜 이렇게나 많은지, 하나를 배우는 데에도 시간이 오래걸려 쉽게 접근하지 못한 부분이었다.

그동안 CRA(Create React App)이라는 명령어를 통해 리액트를 쉽게 사용할 수 있는데, 이는 기본적인 설정을 지원하고 프로젝트에 맞게 설정을 변경할 수가 없다. 그렇기 때문에 config 파일들의 설정이 필요하다는 것을 알게되었다.

앞으로 더 훌륭한 개발자가 되기 위해서는 기본 설정에만 의존하지 않고 내가 진행하려는 목적에 맞는 세팅을 알아둘 필요가 있어 열심히 배워보려고한다.

댓글 공유

요구사항

결과물

  • 버튼 컴포넌트를 만들어 재사용 가능하게 만들어라
  • 아이콘 컴포넌트를 만들어 재사용 가능하게 만들어라
  • 컴포넌트를 분리하여라

해결

1. src/main.js 파일에서 렌더링 준비를 한다.

1
2
3
4
5
6
7
8
const container = document.querySelector('root');
const root = React.createRoot(container);

root.render(
<React.StrictMode>
<h1>Get Ready?</h1>
</React.StrictMode>
)
  • 업로드 버튼 컴포넌트의 type, content를 지정해주어 컴포넌트를 재사용할 수 있다.
  • 사용자가 컴포넌트를 사용하려면 type과 content를 입력하여 아이콘과 내용을 적어주면 된다.

2. 버튼 컴포넌트 생성

1
2
3
4
5
6
7
8
9
10
11
const Button = props => {
const {children, type} = props;

return (
<button className="upload-button" disabled={type === 'disabled' ? "disabled" : ''}>
{children}<Icon type={type} />
</button>
)
}

export default Button
  • 버튼 컴포넌트에게 전달받은 props의 type값을 그대로 Icon 컴포넌트에게 전달해주었다.
  • props 객체로 전달받은 type값에 따라 조건부로 disable 속성을 등록/해제 해주었다.

3. 아이콘 컴포넌트 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const icons = {
"idle" : "M1.80202 3.65685L5.27085 0.292893C5.67355 -0.0976316 6.32645 -0.0976317 6.72915 0.292893L10.198 3.65685C10.6007 4.04738 10.6007 4.68054 10.198 5.07107C9.79528 5.46159 9.14238 5.46159 8.73968 5.07107L7.03117 3.41421L7.03117 11C7.03117 11.5523 6.5695 12 6 12C5.4305 12 4.96883 11.5523 4.96883 11L4.96883 3.41421L3.26032 5.07107C2.85762 5.46159 2.20472 5.46159 1.80202 5.07107C1.39933 4.68054 1.39933 4.04738 1.80202 3.65685Z",
"pending": "M9.96461 3.87118C9.61452 3.2192 9.10905 2.66362 8.49296 2.25365C7.87687 1.84368 7.16918 1.59198 6.43257 1.52084C5.69597 1.4497 4.95319 1.56132 4.27003 1.84582C3.58688 2.13031 2.98442 2.5789 2.51604 3.15184C2.04766 3.72478 1.7278 4.40439 1.58481 5.13047C1.44181 5.85656 1.48009 6.6067 1.69626 7.31445C1.91242 8.02221 2.29979 8.66573 2.82407 9.18802C3.34834 9.7103 3.99333 10.0952 4.7019 10.3087L5.1124 8.94617C4.6279 8.8002 4.18687 8.53699 3.82839 8.17987C3.46991 7.82275 3.20503 7.38273 3.05723 6.89879C2.90942 6.41484 2.88325 5.90192 2.98102 5.40544C3.0788 4.90897 3.2975 4.44427 3.61777 4.05251C3.93804 3.66075 4.34998 3.35402 4.8171 3.15949C5.28422 2.96496 5.79211 2.88864 6.29578 2.93728C6.79945 2.98592 7.28335 3.15803 7.70461 3.43835C8.12588 3.71868 8.47151 4.09857 8.71088 4.54438L9.96461 3.87118Z",
"resolved": "M11.6321 0.725152C12.0601 1.07426 12.124 1.70419 11.7749 2.13214L4.43311 11.1321C4.24683 11.3605 3.96931 11.4951 3.67466 11.4999C3.38 11.5047 3.09822 11.3794 2.90454 11.1573L0.246308 8.10888C-0.116669 7.69263 -0.0734819 7.06094 0.34277 6.69796C0.759022 6.33499 1.39071 6.37817 1.75369 6.79443L3.63274 8.94927L10.2251 0.867921C10.5742 0.439968 11.2042 0.376047 11.6321 0.725152Z",
"rejected": "M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12ZM3.40106 3.25359C3.79159 2.86306 4.42475 2.86306 4.81528 3.25359L6.01883 4.45714L7.22238 3.25359C7.61291 2.86306 8.24607 2.86306 8.6366 3.25359C9.02712 3.64411 9.02712 4.27728 8.6366 4.6678L7.43304 5.87135L8.6366 7.07491C9.02712 7.46543 9.02712 8.0986 8.6366 8.48912C8.24607 8.87964 7.61291 8.87964 7.22238 8.48912L6.01883 7.28557L4.81528 8.48912C4.42475 8.87964 3.79159 8.87964 3.40106 8.48912C3.01054 8.0986 3.01054 7.46543 3.40106 7.07491L4.60462 5.87135L3.40106 4.6678C3.01054 4.27728 3.01054 3.64411 3.40106 3.25359Z",
"disabled": "M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM9.65221 6.00001C9.65221 8.01707 8.01707 9.65221 6.00001 9.65221C5.37082 9.65221 4.77878 9.4931 4.26195 9.21293L9.21292 4.26194C9.4931 4.77878 9.65221 5.37081 9.65221 6.00001ZM2.83053 7.81592L7.81591 2.83053C7.28103 2.52342 6.66103 2.34781 6.00001 2.34781C3.98296 2.34781 2.34781 3.98296 2.34781 6.00001C2.34781 6.66103 2.52342 7.28104 2.83053 7.81592Z"
}

const Icon = props => {
const {type} = props;

return (
<svg className={`button-icon ${type === 'pending' ? "spinner_V8m1" : ""}`} width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d={icons[type]} fill={type === 'disabled' ? "#ADAEB6" : "#525577"} />
</svg>
)
}
  • props 객체로 전달받은 type 값을 가지고 상황에 맞는 아이콘을 보여주고 그에 따라 스타일링도 해주었다.
  • icons 객체에 props 객체로 받은 타입에 대한 값을 저장하여 관리하였다.

4. 컴포넌트 분리

이 모든 코드가 한 파일에 있기에 이를 컴포넌트 단위와 역할별로 구분해주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./main.js
import UploadButton from './components/Button.js'

const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);

root.render(
<React.StrictMode>
<UploadButton type="idle">업로드</UploadButton>
<UploadButton type="pending">업로드 중</UploadButton>
<UploadButton type="resolved">완료</UploadButton>
<UploadButton type="rejected">실패</UploadButton>
<UploadButton type="disabled">업로드</UploadButton>
</React.StrictMode>
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ./components/UploadButton.js
import Icon from './Icon.js'

const Button = props => {
const {children, type} = props;

return (
<button className="upload-button" disabled={type === 'disabled' ? "disabled" : ''}>
{children}<Icon type={type} />
</button>
)
}

export default Button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ./components/Icon.js
const icons = {
...
}

const Icon = props => {
const {type} = props;

return (
<svg className={`button-icon ${type === 'pending' ? "spinner_V8m1" : ""}`} width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d={icons[type]} fill={type === 'disabled' ? "#ADAEB6" : "#525577"} />
</svg>
)
}


export default Icon
  • 모듈이 하나의 개체로 이루어져 있으므로 기본 내보내기를 해주었다.
  • 기본 내보내기를 해주었으므로 import시 이름을 변경하여 가져올 수 있다.

💪 맞닥뜨린 문제

1. 리액트 import 오류

import오류

1
2
3
4
5
6
7
8
9
10
import {createRoot, StrictMode} from './react.js';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
<StrictMode>
<h1>Hi~!</h1>
</StrictMode>
)

위와 같이 import를 해오고 싶었지만 CDN으로 React를 사용하는 것이기 때문에 react.js를 불러올 수 없다.

그러므로 ReactDOM, React로 객체처럼 사용해야만 했다.

🏓 소감

간단한 업로드 버튼 컴포넌트를 구현하는 과제를 해보면서 리액트와 좀 더 친숙해지는 계기가 된 것 같다. figma 시안이 워낙 꼼꼼하게 잘 나와있어서 스타일링 하는데는 큰 어려움 없이 해서 좋았다.

처음 딱 시작하려는 순간부터 오류를 맞이해서 당황하였지만 문제도 잘 해결했고 SVG 파일에 애니메이션과 스타일을 입혀보는 것을 직접 해보니 더 기억에 오래 남을 것 같다.

댓글 공유

📌 React란?

화면에 렌더링되는 UI를 컴포넌트 단위로 쪼개 사용할 수 있는 Javascript 라이브러리리 혹은 프레임워크이다.
라이브러리인가? 프레임워크인가?는 크게 중요하지 않다. 우리가 중점적으로 생각해야할 부분은 리액트는 Progressive(점진적)이라는 것이다.

특징

  1. 리액트는 점진적이므로 애플리케이션을 모두 리액트로 구성할 필요가 없다.

  2. 리액트는 Javascript 중점 라이브러리이다. 그러므로 자바스크립트를 잘하면 재밌을 것이다.

  3. 리액트는 별도의 설치가 필요없이 컴포넌트를 즉시 사용해볼 수 있다.

  4. 선언형 프로그래밍으로 명령형 프로그래밍보다 코드를 이해하기가 쉽다.

  5. 리액트를 배우면 웹, 리액트 네이티브를 배우면 모바일 등 한가지를 배워 확장성이 넓다.

시작하기 전

1. React API 라이브러리를 사용하여 UI를 구상하는 VitualDOM을 생성한다.

1
2
// CDN
<script src="//unpkg.com/react@17/umd/react.development.js" crossorigin></script>
  • 만약 버전을 변경하고 싶다면 @version을 써준다.

  • 만약 IE를 고려 해야한다면 17 버전을 사용하고 그렇지 않다면 18 버전 사용하자.

2. ReactDOM API 라이브러리를 사용하여 VirtualDOM을 RealDOM에 렌더링하여 UI를 구현한다.

1
<script src="//unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
  • React API와 버젼을 일치시켜 줘야한다.

💡 React v17의 render()와 v18의 createRoot() 차이

1
2
3
4
5
6
7
// version 17
const app = React.createElement('div');

ReactDOM.render(app,
document.getElementById('root'),
() => {console.log('rendered!')}
);
  • 렌더링할 React 요소인 app을 인자로 전달하고 이를 렌더링할 container인 root container를 render()에 전달한다.

리액트 버전 18부터 render() 대신 createRoot()를 사용한다.

1
createRoot(container[, options])
1
2
3
4
5
6
// version 18
const root = createRoot(document.getElementById('root'));

const element = <h1>Hello, world</h1>; // JSX

root.render(element);

문서의 RealDOM 요소 노드인 container를 React Root로 만들어 반환하고, React Root를 render() 메서드를 사용하여 React 요소를 RealDOM에 렌더링한다.

실습

1. React 요소 노드와 Real DOM 요소 노드 차이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Real
const headlineNode = document.createElement('h1');
headlineNode.classList.add('headline');
headlineNode.setAttribute('land','en');
headlineNode.insertAdjacentHTML('beforeend', `Add <strong>React</strong> to a Website`);

// React
const headlineVnode = React.createElement(
'h1',
{
className : 'headline',
lang: 'en'
},
'Add ',
React.createElement('strong', null, 'React'),
'to a Website'
)

React.createElement

  • RealDOM 요소 노드는 브라우저가 렌더링하면서 생성한 것이고 이와 달리 React 요소 노드는 Object(객체)로 그 형태가 다르게 생겼다.

  • 위 사진을 보면 props라는 객체에 children, className, lang 등이 담겨있는 것을 알 수 있다. props에 대해서는 이후에 알아보자.

❗️ StrictMode 오류 해결

ECMAScript에서 ‘use strict’ 사용하여 문법적 오류를 미리 알려주었듯이 React에서도 이러한 오류를 미리 알려준다.

이에 대한 경고를 해결하기 위해서는 StrictMode 컴포넌트를 사용해야한다.

1
2
3
4
5
React.createElement(
React.StrictMode,
null,
React.createElement(App, { children: [headline, reactLogo] })
)

사용법은 간단하다. 생성하려는 React 요소를 React.StrictMode의 자식 요소로 전달해주면 된다.

2. React Component vs React Element

자바스크립트에서는 재사용을 위해 함수를 사용하고 리액트에서는 재사용을 위해 컴포넌트를 사용한다. 컴포넌트 생성 방법은 함수를 생성하듯이 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
function SvgPath(props) {
return React.createElement('path',{
d: props.d,
fill: 'currentColor'
});
}

const App = props => React.createElement('div',
{ className: 'App' },
...props.children
);
  • 리액트 컴포넌트는 매개변수로 props를 받는다.

  • props 객체를 통해 다른 컴포넌트에게 본인의 HTML attributes, 자바스크립트 값(객체, 배열, 함수 등)을 전달해줄 수 있다.

  • 함수선언식, 함수표현식 둘 다 가능하다.

하지만 createElement()를 사용하여 컴포넌트를 생성하는 것은 매우 번거로운 일이다. 그 대안으로 생겨난 것이 바로 JSX이다.

JSX란?

JSX는 리액트 컴포넌트 사용을 HTML 처럼 사용하기 위해 생겨났다. JSX는 XML 처럼 생긴 문법 표현식이다.

1
2
3
4
5
6
7
8
9
10
const name = 'loco';

const element = (
<h1>
안녕하세요~! {name}
</h1>
);

// rendered
<h1>안녕하세요~! loco</h1>

자동 세미콜론 삽입이 되는 것을 방지하기 위해 괄호로 감싸는 것을 추천한다.

특징

  1. 선언형, HTML과 비슷한 구조, {}를 사용한 데이터 바인딩이 편리하다.

  2. 브라우저엔진에 의해 해석되지 못하므로 babel이 컴파일을 해줘야만 한다.

  3. HTML 보단 JS에 가까우므로 camelCase 명명규칙을 따른다.

  4. JSX는 렌더링하기 전에 이스케이프하므로 애플리케이션에서 명시적으로 작성되지 않는 내용은 script에 주입되지 않아 XSS 공격으로부터 안전하다.

❗️ React에서 babel 추가 설정

React바벨추가설정

  1. 이전에 React를 사용하기 전에 설정해두었던 babel 사양에서는 React를 컴파일 해줄 수 없다.

  2. 컴포넌트를 모듈 파일로 구분할 때에 babel은 input으로 지정된 파일만 컴파일 해주고 input 파일에서 import한 파일까지 컴파일해주지 않는다.

위 2가지 문제 해결을 위해서는 플러그인을 설치 해줘야한다.

1
npm i -D @babel/preset-react
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// babel.config.js
module.exports = {
comments: false,
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
},
],
'@babel/preset-react',
],
};

  • 설치가 끝났다면 babel.config.js 파일에 해당 플러그인을 사용할 것이라고 등록을 해줘야 정상 동작한다.

  • 옵션값이 있을 경우 []로 감싸서 넣어주고 그렇지 않은 경우 문자열로만 추가한다.

JSX로 컴포넌트 생성하기

이제 JSX를 사용할 준비가 끝났으니 JSX를 사용하여 컴포넌트를 생성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const HeadLine = () => (
<h1 className="headline" lang="en">
Add <strong>React</strong> to a Website
</h1>
);

const App = () => {
<div className="App">
<HeadLine></HeadLine>
</div>
}

const container = document.getElementById('react-root');
const ReactDOMRoot = ReactDOM.createRoot(container);

ReactDOMRoot.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
  • React.createElement()를 사용하여 React Element를 생성하여 컴포넌트를 생성하는 방법보다 훨씬 편리하다는 것을 알 수 있다.

🏓 소감

리액트 수업을 듣고 배운것을 차근차근 익혀가면서 정리해보았다. props에 대한 개념도 확실히 잡히고, JSX를 왜 쓰게 되었는지, 안쓰면 무엇인 문제점이고 불편한지를 깨달을 수 있는 시간이여서 좋았다.

공식문서를 읽어볼 때도 영어로 된 것을 자동번역하여 읽지 않고 원문을 보면서 읽으니 시간이 조금 더디지만 그렇기 때문에 기억에 더 오래 남을 것이라고 생각하여 영어로 읽는 연습을 하고있다.

댓글 공유

🥨  리액트

Why?

우리가 CBD 기반의 라이브러리를 직접 사용해보았을 때, 전역상태의 개념 부재, 지역 상태 관리의 어려움 등의 문제를 겪었던 경험이 있다. 리액트는 이러한 어려움을 줄여준다.

👀  props vs state

  • props는 컴포넌트로 전달된 입력 데이터를 객체화한 것
    • 읽기 전용이므로 수정해서는 안된다.
  • state는 컴포넌트의 내부적인 상태 데이터를 객체화한 것 ⇒ 컴포넌트 상태 데이터 변경되면 렌더링 발생
    • 비공개 요소이며, 컴포넌트에 의해 완전히 제어된다.

👹  JSX

⇒ 리액트는 컴포넌트라는 느슨하게 연결된 유닛으로 관심사를 분리한다.

  • React element(객체)를 생성한다. (Babel은 JSX를 React.createElement() 호출로 컴파일한다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);

// 위와 동일
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

// JSX는 아래와 같은 element 객체를 생성하고, 이를 통해 화면에 렌더링을 한다.
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
  • 표현식이다.

⇒ if, for 문에 사용될 수 있다.

  • JSX는 HTML보다 JS에 가깝기 때문에 어트리뷰트 이름 대신 camelCase로 명명한다.

ex) class ⇒ className, tabindex ⇒ tabIndex

  • 모든 값을 렌더링하기 전에 이스케이프하므로 악성 사용자가 XSS 공격을 하는 것을 방지할 수 있다.

🖥️  component vs element

element

DOM element와 달리 일반 객체이다. React DOM은 React 엘리먼트와 일치하도록 DOM을 업데이트 한다. 즉, 가상돔을 구성하고 리얼돔을 이와 일치하도록 한다.

React 엘리먼트를 렌더링하기 위해서 1. ReactDOM.createRoot()에 DOM 엘리먼트 전달 2. React 엘리먼트를 root.render() 에게 전달

1
2
3
4
5
const root = ReactDOM.createRoot(
document.getElementById('root')
);
const element = <h1>Hello, world</h1>;
root.render(element);
  • 불변객체로, 생성한 이후 해당 엘리먼트의 자식이나 속성을 변경할 수 없다.
  • 특정 시점의 UI를 보여준다고 생각
  • 이를 업데이트 하기 위해서는 새로운 엘리먼트 생성하고 이를 root.render()에게 전달하는 방법뿐

component

element들이 모여서 컴포넌트를 이룬다. 엘리먼트는 컴포넌트의 구성요소이다.

  • 컴포넌트를 통해 UI를 재사용 가능한 여러 조각으로 나눔 ⇒ CBD
  • 컴포넌트 정의 방법 ⇒ 함수 생성 (props라는 입력 데이터를 전달받는다)
1
2
3
4
5
6
7
8
9
10
11
// 함수형 컴포넌트
function Welcome(props) {
return <h1>Hello~! Welcome {props.name}</h1>;
}

// 클래스형 컴포넌트
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
  • React 컴포넌트는 자신의 props를 다룰 때, 반드시 순수함수 처럼 동작해야한다.
    • 프론트 개발을 하면서 순수함수로 개발을 한다는 것은 불가능한 일이기에 **state(상태)**가 등장하였다.

🧬  state와 컴포넌트 생명주기

생명주기 메서드를 사용하여 컴포넌트가 마운트되거나 언마운트될 때 일부 코드를 작동할 수 있다.

마운팅

컴포넌트가 처음 DOM에 렌더링 될 때마다 어떤 행동을 하도록 설정해주는 것을 “마운팅”이라고 한다.

언마운팅

컴포넌트가 생성된 DOM에서 삭제될 때마다 어떤 행동을 하도록 설정해주는 것을 “언마운팅”이라고 한다.

🎴 setState 유의사항

  • this.props, this.state가 비동기적으로 업데이트 될 수 있어 state를 계산할 때, 해당 값에 의존하면 안된다.
1
2
3
4
5
6
7
8
9
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
  • state가 소유하고 설정한 컴포넌트에만 state를 전달할 수 있다. ⇒ props를 통해 하향식(단방향식)으로만
1
2
3
4
5
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못합니다.

🌃  이벤트 처리

  • camelCase 사용
  • 문자열이 아닌 {함수}로 전달
  • e.preventDefault() 명시적으로 작성
1
2
3
4
5
6
7
8
9
10
11
12
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}

return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}

♨️  JSX 콜백안에서 this 바인딩

Javascript 클래스 메서드는 기본적으로 바인딩되어 있지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};

// 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
  • onClick={this.handleClick}과 같이 뒤에 ()를 사용하지 않고 메서드를 참조할 경우, 해당 메서드를 바인딩 해야 합니다.
  • 만약 constructor에서 bind해주기 싫다면 onClick={this.handleClick.bind(this)} 를 해줘야한다.

이벤트 핸들러에 인자 전달

1
2
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
  • 위줄은 화살표함수, 아랫줄은 bind를 사용하였다.
  • React 이벤트를 나타내는 e 인자가 ID뒤의 두번째 인자로 전달된다.
  • bind 사용시 e 인자가 자동으로 전달된다.

🎁  조건부 렌더링

  • props로 전달되는 조건에 따라 렌더링 해주는 무상태 컴포넌트 렌더링
1
2
3
4
5
6
7
8
9
10
11
12
// 무상태
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
// Try changing to isLoggedIn={true}:
root.render(<Greeting isLoggedIn={false} />);

상태를 가지지 않아 함수형 컴포넌트로 생성하였다. props로 전달되는 값에 따라 조건부 렌더링 한다.

  • state로 관리하는 유상태 컴포넌트 렌더링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}

handleLoginClick() {
this.setState({isLoggedIn: true});
}

handleLogoutClick() {
this.setState({isLoggedIn: false});
}

render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}

return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<LoginControl />);
  • 컴포넌트가 상태를 가지고 있고 상태를 변경하는 메서드도 지니고 있다. (캡슐화)
  • 상태가 바뀌면 바뀐 상태에 따라 button 엘리먼트의 사용자 정의 컴포넌트가 다르게 할당된다.
  • 상태가 바뀌면 바뀐 상태를 무상태 컴포넌트에게 props로 전달해주고 있다.

🔑  리스트와 Key

리액트에서 반복문은 주로 map 고차함수를 통해 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NumberList extends React.Component {
constructor(props) {
super(props);
this.state = {
numbers: [1, 2, 3, 4, 5],
};
}

render() {
const listItems = this.state.numbers.map((number) => (
<li key={number.toString()}>{number}</li>
));
return <ul>{listItems}</ul>;
}
}
  • 만약 li에 key props를 주지 않는다면 오류가 발생할 것이다.

Key란?

React가 어떤 항목을 식별하기 위한 문자열이다. 주로 ID를 Key로 사용하지만 ID가 없다면 최후의 수단으로 index를 사용한다.

  • key를 index로 사용하는 것은 권장하지 않는다.
  • key는 주변 배열의 context에서 의미가 있다. map 함수 내부 엘레먼트에 key 넣자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function ListItem(props) {
const value = props.value;
return <li>{value}</li>; // 여기에 key가 있는 것이 아니다.
}

class NumberList extends React.Component {
constructor(props) {
super(props);
this.state = {
numbers: [1, 2, 3, 4, 5, 6, 7],
};
}

render() {
const listItems = this.state.numbers.map((number) => (
// numbers 배열이 있는 맥락에 key가 있어야 한다.
<ListItem key={number.toString()} value={number} />
));
return <ul>{listItems}</ul>;
}
}
  • key는 배열의 형제 요소 사이끼리는 고유해야하지만 두 개의 다른 배열을 만들 때는 동일한 key 사용해도된다.
  • 만약 컴포넌트에서 key와 동일한 값이 필요하다면 이는 key가 아닌 다른 이름의 props로 명시적으로 전달해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}

const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];

const content = posts.map((post) =>
<Post
key={post.id} // X
id={post.id} // O
title={post.title} />
);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);
  • 위 예시에서 Post 컴포넌트는 props.id는 읽을 수 있지만, props.key는 읽을 수 없다.

👔  Form Element

React 에서는 사용자의 입력값을 state로 관리하여 React state가 신뢰 가능한 단일 출처(Single Source of Truth)가 된다. 이렇게 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트”라고 한다.

<input type="text"><textarea> 및 <select> 모두 매우 비슷하게 동작한다.

textarea

HTML에서는 텍스트를 자식노드로 정의하지만, React에서는 textarea 태그에 value 어트리뷰트를 사용한다.

1
<textarea value={this.state.value} onChange={this.handleChange} />

select

React에서는 select 태그에서 value 어트리뷰트를 통해 selected를 구현한다.

1
2
3
4
5
6
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
  • multiple옵션 허용 시 value 어트리뷰트에 배열 전달 가능
1
<select multiple={true} value={['B', 'C']}>

📈  State 끌어올리기

보통의 경우 state는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가한다. 그러고 나서 다른 컴포넌트도 그 값이 필요하게 되면 그 값을 그들의 가장 가까운 공통 조상으로 끌어올리고 공통 조상이 자식 컴포넌트들에게 props를 통해 전달해주면 된다. 이 때, 공통 조상의 state를 변경하는 함수도 props에 담아 같이 전달한다.

  • 하향식 데이터 흐름을 활용
  • 어떤 값이 props 또는 state로부터 계산될 수 있다면, 아마도 그 값을 state에 두어서는 안 됩니다.

ex) celsiusValue와 fahrenheitValue를 둘 다 저장하는 대신, 단지 최근에 변경된 temperature와 scale만 저장하면 됩니다. 다른 입력 필드의 값은 항상 그 값들에 기반해서 render() 메서드 안에서 계산될 수 있습니다.

댓글 공유

loco9939

author.bio


author.job