LifeCycle(라이프사이클)

카테고리 React

📌 LifeCycle이란?

라이프 사이클은 함수 컴포넌트와 달리 클래스 컴포넌트에서만 라이프 사이클이 제공된다. 컴포넌트 생성, 마운트, 업데이트, 언마운트 등 특정 시점에서 실행되는 콜백함수를 말한다.

👑 LifeCycle 필요성

라이프 사이클은 React의 성능을 최적화하고 React가 컨트롤할 수 없는 Side Effect(사이드이펙트)를 처리하기 위함이다.

대표적인 사이드 이펙트는 다음과 같다.

  1. 네트워크 통신 (비동기 통신 요청 및 응답)
  2. DOM 컨트롤 (RealDOM 접근 및 조작)
  3. 구독/취소 (이벤트 핸들링 등)
  4. 오류 감지 (ErrorBoundary 컴포넌트 등)

✏️ 대표적인 LifeCycle 메서드

  • constructor()
    • 마운트 되기 전에 실행된다.
  • render()
    • 컴포넌트 렌더링 시점에 호출
  • componentDidMount()
    • DOM에 마운트 된 이후 실행된다.
  • componentDidUpdate()
    • 최초 렌더링 시 실행 ❌
    • 업데이트 이후 실행
  • componentWillunmount()
    • 컴포넌트 소멸하기 직전에 실행

LifeCycle

🌈 Render 단계

  • render 단계는 virtualDOM을 조작하는 단계이다.
    • 성능 최적화를 위한 행동은 render 이전에 해줘야한다.
    • render가 끝났다고 해서 실제DOM에 바로 반영되는 것이 아니다.
  • 리액트 업데이트가 되는 경우는 다음 3가지 상황이다.
    1. 새로운 props가 전달되었을 때,
    2. 해당 컴포넌트의 state가 변경되었을 때,
    3. 강제로 변경시켰을 때만 업데이트가 발생한다.

🔨 Commit 단계

commit 단계는 RealDOM이므로 명령형이 가능하다.

  • DOM을 사용하여 componentDidMount, DidUpdate, WillUnmount 에서 명령형 코드 작성이 가능하다.
  • 사이드 이펙트 관련 함수는 여기서 수행해야한다.

댓글 공유

🐍 setState

카테고리 React, Hooks

📌 setState

setState() 메서드는 컴포넌트의 state 객체에 대한 업데이트를 실행합니다. state가 변경되면, 컴포넌트는 리렌더링됩니다.

1
setState(updater[, callback])

updater에 컴포넌트의 state 변경사항을 인자로 넘겨주고 React에게 해당 컴포넌트를 재렌더링해야한다고 요청을 보내는 메서드이다.

  • setState() 호출은 비동기적으로 이뤄진다.
  • 성능 향상을 위해 즉각적으로 재렌더링을 발생시키지 않는다.
    • 여러 변경 사항을 일괄적으로 갱신하거나 나중에 미룰 수 있다. 때문에 setState() 호출하자마자 this.state에 접근하는 것은 문제가 될 수 있다.
    • 대신 componentDidUpdate() 또는 setState(updater, callback)처럼 2번째 인자로 콜백함수를 전달하는 방법을 사용한다.
1
2
3
4
5
6
7
8
9
10
this.state = {
message: "",
};

this.setState(
{ message: "상태 변경" },
() => console.log("callback: ", this.state.message) // '상태 변경'
);

console.log(this.state.message); // ''
  • 아직 렌더링이 발생하지 않았기 때문에 this.state.message의 값이 갱신되지 않았다.

🎒 2022.11.12 추가

setState() 첫번째 인수로 콜백함수 올 때

1
2
3
4
5
6
7
8
9
10
11
12
13
export class BinaryCalcurator extends Component {
state = {
buttonStates: Array(this.props.numberOfButtons).fill(false),
};

handleToggleButtonContent = (index) => {
this.setState(({ buttonStates }) => ({
buttonStates: buttonStates.map((buttonState, idx) =>
idx === index ? !buttonState : buttonState
),
}));
};
}
  • 콜백함수는 이전 상태를 받아서 상태 변경을 요청한다.

댓글 공유

🍪 Cookie

카테고리 CS

📌 Cookie란?

서버가 클라이언트에게 전송하는 데이터이다. 서버가 HTTP Response Header의 Set-Cookie에 내용을 넣어 전달하면 브라우저가 이 내용을 자체적으로 브라우저에 저장한다.

1
2
3
4
5
6
<!-- Reponse Header -->
HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookies: <cookie-name>=<cookie-value>

// Page Content...
  • 쿠키는 key,value를 쌍으로 브라우저에 문자열로 저장된다.
  • 주로 만료 기간을 설정하거나 자동으로 서버로 전송되어야할 작은 용량의 정보를 담는다.
    • Ex) n일 동안 보지 않기, 비로그인 장바구니 기능
  • 쿠키는 주로 인증을 위해 사용된다.
  • 쿠키는 세션쿠키와 퍼머넌트 쿠키가 있다.

세션 쿠키는 클라이언트가 셧다운 되면 사라지는 쿠키이다. 왜냐하면 Expires, Max-Age를 지정해주지 않았기 때문이다.

일부 브라우저는 Session Resorting을 사용하여 세션쿠키를 영구적으로 만들 수 있다.

  • Expires, Max-Age 설정으로 만료 기한을 정해주지 않은 쿠키

클라이언트가 브라우저를 열고 닫거나 새로고침을 하여도 쿠키의 데이터가 보존되는 쿠키이다. Set-Cookie에 몇가지 조건을 추가하여 Permanent Cookie를 설정할 수 있다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT;
  • 만료시간은 클라이언트 시간을 따른다. (서버시간과 상이할 수 있다.)

✏️ 쿠키 접근

1
2
3
4
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// logs "yummy_cookie=choco; tasty_cookie=strawberry"

🔨 쿠키 보안

🙅‍♂️ 민감한 정보는 HTTP 쿠키 내에 저장하면 안된다.

XSS 공격으로부터 사용자의 쿠키 정보가 유출되는 것을 방지하기 위해 등장하였다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT; HttpOnly
  • document.cookie API로 접근할 수 없다.

HTTPS 전송을 보증해주는 역할을 하는 쿠키이다. Secure로 설정된 쿠키는 안전한 전송 프로토콜에만 쿠키 정보를 서버로 보낸다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT; Secure; HttpOnly

👿 Cross-site 요청 위조(CSRF) 공격

현재 사용자가 은행 사이트에 로그인 되어있다. 송금과 같은 은행 업무를 보고 로그아웃 하지 않고 새 탭을 열어 어떤 사이트를 열었다. 그런데 이 사이트에는 해커에게 송금을 요청하는 form이 있고 이 form은 자동으로 제출되도록 설정되었다.

form이 해당 사이트를 거쳐 은행 사이트로 전송될 때 인증 쿠키도 함께 전송된다. 은행은 사용자로 착각하여 해커에게 돈을 송금하는 일이 발생한다.

⇒ 이런 공격을 크로스 사이트 요청 위조 라고 부른다.

🚪 방어 방법

  • XSRF 토큰이라는 특수 필드 넣어줘야한다.
  • 민감한 동작에 필수로 요구되는 확인 절차가 항상 수행하도록 한다.
  • 민감한 동작에 사용되는 쿠키는 짧은 수명만 갖도록 한다.

댓글 공유

HTTP 요청 메서드 GET vs POST

카테고리 CS

📌 GET

GET 방식은 전송 URL에 입력 데이터를 쿼리 스트링으로 보내는 방식
ex) http://jsonplaceholder.typicode.com/posts?userId=1&id=1

  • 전송 URL 바로 뒤에 ?를 통해 데이터의 시작을 알려주고, Key-value 형태의 데이터 추가한다.
  • 캐시가 가능하다.
  • 1개 이상의 전송 데이터는 &로 구분한다.
  • URL에 전송 데이터가 모두 노출되어 보안에 문제가 있어 민감한 정보는 사용하지 않는다. 전송할 수 있는 데이터의 한계가 있다. (최대 255자)
  • REST API에서 GET 메소드는 모든 또는 특정 리소스의 조회를 요청한다.
    GET 요청 보내면 ~ 이거이거 조회할게요~

📌 POST

POST 방식은 사용자의 요청을 Request Body에 담아 보내는 방식
ex) http://jsonplaceholder.typicode.com/posts

  • URL에 전송 데이터가 모두 노출되지 않지만 GET에 비해 속도가 느리다.
  • 캐시가 불가능하다.
  • 데이터 길이와 타입의 제한이 없다.
  • REST API에서 POST 메소드는 특정 리소스의 생성을 요청한다.
    POST 요청 보내면 ~ 이거 생성해주세요~

댓글 공유

HTTP의 역사와 HTTPS

카테고리 CS

📌 HTTP

서버/클라이언트 모델을 따라 데이터를 주고받기 위한 프로토콜이다.

일반적으로 프로토콜을 이용하여 HTML 파일을 주고받을 수 있는 공간을 의미한다.

  • 텍스트 교환으로 보안에 취약하다.

역사

초기 웹 페이지는 단순한 서버-클라이언트의 구조를 따랐다. 처음부터 TCP / IP 위에서 구현되도록 설계되었다. 지금의 TCP는 무겁고 느리다고 까이지만, 이 당시에는 연결 지향적 특성 때문에 안정적이고 신뢰성있는 통신을 제공하여 평이 좋았다.

초기 HTTP 구조는 매우 간단했다. 요청 메서드도 GET 한가지였고, 헤더나 상태 코드도 없었고 응답은 무조건 HTML 파일 그 자체였다.

하지만 웹이 인기를 끌다 보니 기존 HTTP 사양만으로는 사용자들의 요구사항을 충족시킬 수 없었고 명시적으로 약속된 사양이 없어 여러 브라우저들 간의 혼란이 있어서 HTTP 기본 구조를 그대로 유지하면서 HTTP를 표준화하기 위해 HTTP WG 조직이 탄생하고 이 때 HTTP/1.0이 탄생하게되었다.

📌 HTTP/1.0

  • HTTP Header에 버전정보 명시되어있다.
  • 요청 메서드 GET, POST, HEAD 3가지
  • 상태 코드 추가 되어 클라이언트 측에서 요청 결과에 따라 동작하도록 설계 가능
  • Content-type의 도움으로 HTML 이외의 파일도 전송할 수 있게되었다.
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 요청 -->
GET /mypage.html HTTP/1.0
User-Agent: ...

<!-- 응답 -->
200 OK
...
Content-Type: text/html
<HTML>
A page with on image
<img src="/myimage.jpg" />
</HTML>
  • HTTP/1.0의 간단한 요청과 응답 예시이다.

📌 HTTP/1.1

HTTP/1.0의 문제

  • HTTP/1.0에서는 요청에 따른 응답이 수신되면 TCP 연결 바로 종료
    • 웹 페이지가 복잡해짐에 따라 이러한 TCP 핸드쉐이크 과정을 새로 거쳐야 하여 속도가 느려지는 문제가 있었다.
  • 1개의 요청을 보내면 1개의 응답을 받고 1번의 연결이 종료된다. 만약 10번의 요청을 보내면 TCP 연결을 10번 해야하므로 서버에 부담이 된다.

❗️ 문제 해결책

pipe

  • 연결 상태 유지(Persistent connection) : 기본적으로 한번 수립한 연결을 재사용 할 수 있도록 설정
    • 지정한 시간동안 연결을 끊지 않는 방식
    • 연결 유지 시간 길어지면 서버 부하 🔺
  • Pipelining(파이프라이닝) : 클라이언트가 여러 요청 연달아 보내야할 때, 먼저 보낸 요청의 응답을 기다리는 것이 아니라 발생한 요청은 일단 전송하는 방식
    • 클라이언트에서 여러 요청을 순차적으로 보내면 서버는 받은 순서에 따라 응답을 제공한다.

📌 HTTP/2

❓ HTTP/1.1의 문제

  • 매 요청마다 헤더를 중복해서 전송해야만 하는데 이것이 굉장한 낭비였다.
  • 서버가 항상 요청받은 순서대로 응답해야하므로 HOLB(Head-of-Line Blocking) 방식이었다.
  • 하나의 연결 내에서 응답 다중화(multiplexing)을 할 수 없어 요청이 순차적으로 처리되어야 했는데, 서버가 응답 작성 중간에 문제가 생기면 후속 요청들이 전송되지 못하고 지연되는 문제가 있었다.

❗️ 문제 해결책

spdy

  • SPDY 프로토콜 기반으로 동작 ⇒ 웹 페이지 로드 시간 단축
    • 이진 프로토콜 : 단순 텍스트를 전송하는 것보다 컴퓨터 입장에서 훨씬 더 효율적으로 데이터 전송 가능

mutiplexing

  • 응답 다중화 지원 : 하나의 TCP 연결에서 여러 요청을 동시에 처리할 수 있다. 바이너리 프레이밍으로 TCP 연결을 스트림, 메시지, 프레임으로 세분화하였다. 이는 요청 별 순서를 반드시 지켜야 했던 HTTP/1.1과 대조적이다.
    • 스트림 : 요청과 응답이 양방향으로 오가는 연결 단위
    • 메시지 : 하나의 요청과 응답을 구성하는 단위
    • 프레임 : 메시지를 구성하는 최소 단위

mutiplexing2

  • 위 사진에서 하나의 TCP 연결에서 3개의 스트림이 존재한다.
  • 5번 스트림은 클라이언트 측에서 서버로 데이터 전송하고, 1,3번 스트림은 서버 측에서 클라이언트로 데이터를 전송중이다.
  • 1번 프레임 사이에 3번 스트림의 프레임이 끼워져있다.

이로써 HTTP/1.1에서 발생한 HOLB 문제 해결할 수 있었다.

headerCompressing

  • 헤더 필드 압축 지원 : 달라진 부분만 다시 전송하는 코딩 기법 사용

📌 HTTP/3

❗️ TCP로 인한 문제점

앞서 헤더 필드 압축을 통해 HTTP/1.1에서 발생한 HOLB 문제를 해결했다고 하였지만 이와 별개로 TCP로 인해 발생하는 HOLB 문제는 해결할 수 없다는 한계가 있었다.

  • 신뢰성 지향하기에 데이터 손실 발생 시 데이터를 재전송하는데 순서대로 처리해야하기 때문에 HOLB 문제를 해결할 수 없다.
  • 혼잡제어를 수행하기 때문에 전송 속도를 낮은 상태에서 높이는 방식으로 속도 제어를 하는데 이는 네트워크 상황이 좋을 때 불필요한 지연을 발생시킨다.

문제를 해결하기 위해 TCP/IP기반이 아닌 UDP기반인 QUIC 프로토콜 위에서 동작하는 HTTP/3가 나왔다.

⇒ UDP는 신뢰성을 보장하지 않는데 QUIC 신뢰성 기능을 직접 구현하여 신뢰성 기능이 제공되는 UDP 기반의 프로토콜이다.

👍 HTTP/3 장점

RTT

  • 0-RTT 기능 : 연결 정보를 캐싱하여 재사용하여 다음 연결때는 바로 연결이 가능

UDP

  • 연결 다중화 지원 : 각 스트림이 독립적으로 동작한다.
    • HTTP/2 방식의 경우 TCP 특성상 데이터 손실이 발생할 경우 데이터 복구를 우선 처리하여 HOLB가 발생한다.
    • HTTP/3는 데이터 손실이 발생해도 연결 내 스트림이 독립적으로 동작하기 때문에 다른 스트림에 영향을 주지 않는다.
  • 연결 별 고유 UUID 지원
    • TCP 기반의 통신은 wifi 환경에서 셀룰러 환경으로 이동하는경우 IP주소가 변경되어 연결 재수립 과정을 거쳐야하지만, QUIC는 UUID(Connection ID) 연결 ID 기반으로 식별하여 연결을 유지할 수 있다.

📌 HTTP Message

HTTP 메시지는 서버와 클라이언트 간에 데이터가 교환되는 방식이다. 메시지 타입에는 요청(Request), 응답(Response)이 있다.

request와response

요청과 응답의 구조는 유사하다.

  1. 실행 되어야 할 요청 또는 요청의 성공, 실패가 기록된 한줄

  2. 요청에 대한 설명, 메시지 본문에 대한 설명이 포함된 HTTP Header 세트(optional)

  3. 요청에 대한 모든 메타 정보가 전송되었음을 알리는 빈 줄(blank line)

  4. 요청과 관련된 내용이 옵션으로 들어가거나, 응답과 관련된 문서가 들어간다. 본문의 존재 유무는 첫 줄과 HTTP Header에 명시된다.

✏️ HTTP Request

요청은 클라이언트가 서버로 전달해서 서버의 액션이 일어나게끔 하는 메시지이다.

1. Start Line

  1. HTTP 메서드

GET,POST 등의 서버가 수행해야 할 동작을 나타낸다.

  1. 요청 타겟 : URL 또는 (프로토콜,포트, 도메인)

요청 타겟 포맷은 HTTP 메서드에 따라 달라진다.

  • Origin 형식 : 끝에 ‘?’와 쿼리 문자열이 붙는 절대경로이다. 가장 일반적인 형식으로 GET, POST, HEAD, OPTIONS 메서드와 함께 사용한다.
1
2
3
4
POST / HTTP 1.1
GET /background.png HTTP/1.0
HEAD /test.html?query=alibaba HTTP/1.1
OPTIONS /anypage.html HTTP/1.0
  • absolute 형식 : 완전한 URL 형식으로, 프록시에 연결하는 경우 대부분 GET과 사용한다.
1
GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1
  1. 마지막으로 HTTP 버전이 들어간다.

2. Header

대소문자 구분없는 문자열 다음에 콜론(:)이 붙고 그 뒤에 오는 값은 Header에 따라 달라진다.

header

  • HTTP Method
  • HTTP Version
  • Host
  • Content-Type

3. Body(본문)

모든 요청에 본문이 들어가지 않는다. GET, HEAD, DELETE, OPTIONS 처럼 리소스를 가져오는 요청은 본문이 필요 없다.

보통 POST 요청으로 서버에 데이터를 업데이트 하기 위해 본문이 필요하다.

본문은 두가지 종류로 나뉜다.

  • 단일-리소스 본문(single-resource bodies) : Header 2개(Content-Type, Content-Length)로 정의된 단일 파일
  • 다중-리소스 본문(muliple-resource bodies) : 파트마다 다른 정보를 지닌다.

✏️ HTTP Response

응답은 요청에 대한 서버의 답변이다.

1. Start Line

  1. 프로토콜 버전 (보통 HTTP/1.1)

  2. 상태 코드 : 요청의 성공 여부를 나타낸다. 200, 404 또는 302

  3. 상태 텍스트 : 상태 코드에 대한 설명글

1
HTTP/1.1 404 Not Found.

2. Header

요청 Header와 동일한 구조이다.

resHeader

  • HTTP Version
  • Status
  • Content-Type
  • Set-Cookie

3. Body

모든 응답에 Body가 들어가지는 않는다. 201, 204 상태 코드를 가진 응답은 Body가 없다.

📌 HTTPS

기존 프로토콜에서 데이터가 쉽게 도난당하는 것을 방지하기 위해 SSL(Secure Socket Layer) 또는 TLS(Transport Layer Security) 프로토콜 을 사용하여 브라우저와 서버를 암호화하여 연결해준다.

또한 데이터가 전송중에 손상되거나 수정되는 것을 방지한다.

  • 구글같은 포털에서 SEO 가산점을 준다.

✏️ SSL(Secure Socket Layer) 프로토콜

HTTPS 프로토콜이 텍스트를 암호화 할 수 있도록 도와주는 프로토콜이다. 암호화 원리는 공개키 암호화 방식이다.

SSL 프로토콜 통제권을 IETF로 넘기면서 TLS로 바뀐 상태인데 현재는 두가지를 혼용해서 사용하고 있다.

공개키 암호화 방식

공개키 암호화 방식은 공개키와 개인키를 사용하는 방식이다.

  • 공개키는 모두에게 공개해서 사용하는 키이다.
  • 개인키는 서버에서만 가지고 있는 키이다.

예시

  1. A 서버가 HTTPS 적용하기 위해 공개키와 개인키를 만든다.
  2. CA 기업을 선택하고 해당 CA 기업에게 공개키 관리를 부탁하며 계약한다.
  3. CA 기업은 기업명, A 서버 공개키, 공개키 암호화 방법을 담은 인증서를 만들고 해당 인증서를 CA 기업의 개인키로 암호화해서 A 서버에게 제공한다.
  4. A 서버는 CA 기업의 개인키로 암호화된 인증서를 가지고서 클라이언트로부터 HTTPS이 아닌 요청이 오면 암호화된 인증서를 건네준다.
  5. 클라이언트는 예를 들어 main.html 을 서버에 요청했다고 하자. HTTPS 요청이 아니기 때문에 CA기업의 개인키로 암호화된 인증서를 받게된다.
    • CA 기업의 공개키는 모두에게 공개되므로 클라이언트는 인증서를 공개키로 복호화하여 A 서버 공개키를 얻었다.
  6. 클라이언트가 A 서버에 요청을 보낼 때 텍스트를 공개키로 암호화하며 HTTPS 요청을 보낸다.
  7. 서버는 개인키로 암호화된 텍스트를 복호화하여 데이터를 해석하고 응답을 개인키로 암호화하여 클라이언트에게 보낸다.
  8. 클라이언트는 다시 공개키로 응답을 해석하여 해당 서버가 인증된 서버임을 알 수 있다.

📚 참고

댓글 공유

🛤 Class Component vs Function Component

과거의 리액트는 상태를 가지는 컴포넌트는 클래스형으로, 상태를 가지지 않는 컴포넌트는 함수형으로 작성하였다. 왜 그랬을까?

class&function-components

Container 컴포넌트는 상태를 가지는데, 함수형 컴포넌트로 생성하게 되면 함수 호출하여 컴포넌트를 생성할 때 마다 상태가 초기화된 값을 가지기 때문에 Container 컴포넌트는 클래스형으로만 작성하였다.

  • this를 사용하기 때문에 클래스형 컴포넌트가 생성방식이 더 복잡하다.
  • 클래스형 컴포넌트에는 render() 메서드가 꼭 있어야 한다.
  • 클래스형 컴포넌트에서 이벤트 등록 시 this 바인딩을 꼭 해줘야만 한다.

📌 stateless 컴포넌트

stateless 컴포넌트는 말 그대로 상태를 가지지 않는 컴포넌트이다. 이러한 컴포넌트를 stateless 컴포넌트 또는 presentational(프레젠테이셔널) 컴포넌트라고 한다.

목적

  • 시각적 표현에 중점을 둔 컴포넌트
  • 시각적 표현을 목적으로 하니 복잡한 비즈니스 로직 필요없다
  • 상태를 가질 필요가 없고 외부에서 전달 받은 데이터를 화면에 그려준다.

✏️ stateless 컴포넌트는 함수형으로 작성한다?

표현을 목적으로 하는 컴포넌트는 복잡한 비즈니스 로직이 필요없고 이를 클래스로 작성할 경우 Babel 컴파일러가 클래스를 컴파일 해줄 때 함수형 컴포넌트일 때 보다 훨씬 더 많은 코드로 컴파일 해주기 때문에 stateless 컴포넌트를 클래스형으로 작성하는 것은 낭비이다.

📌 stateful 컴포넌트

상태를 가지는 컴포넌트를 stateful 컴포넌트 또는 Container(컨테이너) 컴포넌트라고 한다.

목적

  • 비즈니스 로직을 가지고 있다.
  • state(상태)를 가진다.
  • Presentational 컴포넌트에게 Props를 전달하여 UI 렌더링 하도록 한다.
  • 화면을 그리는 최소한의 스타일 정보를 가진다.

❓ stateful 컴포넌트는 함수형으로 작성하지 않는다?

하지만, React Hook의 등장으로 함수형 컴포넌트로도 Container 컴포넌트의 역할이 가능해졌다.

🏓 소감

클래스형 컴포넌트와 함수형 컴포넌트에 차이에 대해 알게되었다.

프로젝트를 시작하게 될 때, 컴포넌트를 생성할텐데 이 때, 이 컴포넌트가 상태를 가지는 컴포넌트인지 상태를 갖지 않는 컴포넌트인지 구분을 짓고 컴포넌트를 생성해야겠다.

오늘날 리액트에서 React Hook이 등장하게 되면서 클래스형으로만 작성되던 stateful Component가 어떻게 함수형 컴포넌트로 바뀌게 되었는지를 배우게 되면 리액트에 대한 이해가 한층 더 깊어질 것 같다.

댓글 공유

📌 Mock Fn(모의함수)란?

테스트를 위한 함수로, 원하는 함수의 로직을 테스트 하려면 작성해야할 코드가 많지만 테스트 함수를 사용하여 원하는 테스트를 시험할 코드만 간략하게 작성하여 테스트해볼 수 있어 편리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
});
  • 위 예제는 버튼이 클릭되었을 때, 해당 문구가 할당이 제대로 되는지 확인하는 테스트 코드이다.

테스트 목적은 버튼 클릭이 제대로 인식되는지, 선택된 요소가 예상된 값으로 할당이 되는지를 확인한다.

이렇게 테스트 코드를 작성하는 것은 번거로운 일이다.

그러므로 간략하게 테스트 함수를 생성하여 이벤트가 발생할 때, 모의 함수가 호출되면 앞서 말한 로직을 구현해줄 수 있으므로 버튼이 클릭되었을 때, 모의 함수 호출이 제대로 되는지만 확인하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
// mock Fn
test("`onToggle` 속성(prop)에 연결된 함수가 정상적으로 실행됩니다.", () => {
const mockFn = jest.fn();

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

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

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

expect(mockFn).toHaveBeenCalled();
});

✏️ React VirtualDOM Test

리액트에서는 VitualDOM에 렌더링을 하여 RealDOM과 비교하기 때문에 Test시에도 RealDOM이 아닌 VirtualDOM에 렌더링된 것으로 테스트를 해줘야한다.

1
2
3
4
5
6
7
test("컴포넌트는 기본적으로 <span> 요소로 렌더링 됩니다.", () => {
render(<A11yHidden data-testid="tester" />);

const element = screen.getByTestId("tester");

expect(element.localName).toBe("span");
});
  • RealDOM에서 querySelector()로 요소를 가져오는 것처럼 VirtualDOM에서는 screen으로 요소를 가져올 수 있다.
  • 해당 요소를 가져오기 위해서 data-testId 값을 사용하였다.

❗️ localName vs nodeName 차이

  • localName : 소문자를 반환
  • nodeName : 대문자를 반환

defaultProps 사용하기

리액트 컴포넌트에서는 props에 default 값을 주기 위해서 defaultProps를 사용한다.

직접 파라미터에 App({ as: ComponentName = 'div' }) 이런식으로 주는 것을 안티패턴으로 본다.

1
2
3
4
5
6
7
8
9
10
11
import styles from "./A11yHidden.module.css";

export function A11yHidden({ as: ComponentName, ...restProps }) {
return (
<ComponentName className={styles.container} {...restProps}></ComponentName>
);
}

A11yHidden.defaultProps = {
as: "span",
};
  • as 라는 props를 받으면 해당하는 ComponentName으로 태그를 감싸서 컴포넌트를 생성한다.

component는 PascalCase 명명 규칙을 따르기 때문에 이를 따르지 않으면 에러를 발생시킨다.

getByRole vs getByTestId

기본적으로 div, span 같은 태그는 Role(역할)이 없다. 테스트 케이스에서 이러한 요소를 가져오기 위해서는 Role이 없으니 다른 방법을 찾아야한다.

그에 대한 대안으로 테스트용 data-testId="tester" 값을 주고 getByTestId()를 사용하여 요소를 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// div 요소 가져올 경우
test('컴포넌트의 `as` 속성(prop) 값이 "div"인 경우, <div> 요소로 렌더링 됩니다.', () => {
render(<Banner as="div" data-testid="tester" />);

const element = screen.getByTestId("tester");
expect(element.localName).toBe("div");
});

// figure 요소 가져올 경우
test("컴포넌트는 기본적으로 <figure> 요소로 렌더링 됩니다.", () => {
render(<Banner />);

const element = screen.getByRole("figure");
expect(element.localName).toBe("figure");
});

figure, img 등 우리가 직접 Role을 선정해주지 않아도 브라우저에서 해당 태그들을 해석할 때 Role을 할당해주기 때문에 이러한 요소들은 getByRole()를 이용하여 가져올 수 있다.

🏓 소감

마침 우테코에서도 Jest를 사용하고 있었는데, 수업시간 때 리액트 DOM 테스트를 해볼 수 있는 기회가 있어서 유익했다. 프론트 엔드에서는 사용자의 행동에 의해 UI가 변경되는 요소들이 많기 때문에 이러한 사용자 액션에 의한 test를 하기 어렵다.

이럴 경우 사용자가 행동하는 것처럼 테스트를 진행하는 storyBook이라는 testing Tool도 있으니 Jest가 익숙해지면 storyBook도 사용해봐야겠다.

댓글 공유

📌 문제 및 요구사항

숫자 야구 게임 바로가기

✏️ 회고

❗️ 클래스 필드를 사용할 것인가? constructor() 안에서 정의할 것인가?

  • App이라는 클래스는 야구게임 App이다. 야구 게임의 룰을 변경하지 않는 이상 인스턴스를 생성할 때마다 초기화할 값이 없다. 그러므로 초기화 하지 않는 프로퍼티 값들은 클래스 필드에 정의해두었다.

❗️ 요구사항 제대로 읽기

  • 요구사항 제대로 읽지 않아서 라이브러리 사용하여 랜덤값 구하면 되는데, 랜덤값 구하는 로직 직접 짜서 다시 처음부터 시작하게되었다.
  • 또 제대로 안읽어서 특정 메서드 사용하여 구하라했는데 라이브러리에서 제공하는 다른 메서드로 구현하여 다시 작성했다…

❗️jest를 통한 테스트 케이스는 어떻게 확인할 수 있는지 알게되었다.

  • 주어진 라이브러리를 활용하여 1번은 테스트 하는 것을 알 수 있었다. 그런데 2번째 값을 얻기 위해서 내가 해주어야 하는 것이 무엇인지 모르겠다.

내가 직접 예상되는 값을 생각하여 테스트에 입력값만 적어주고 소스코드에서는 해당 입력값이 넘어왔다가 가정하고 그에 대한 처리를 해주면 되는 것이라는 것을 알게되었다.

❗️랜덤값 받은 것을 프로퍼티로 저장해두자

함수형 프로그래밍을 하면서 함수를 호출하고 반환된 값을 다른 함수에게 전달하는 방식으로 랜덤값과 입력값을 비교하고 결과를 반환하는 방식으로 하였는데, 이 방법보다는 App 이라는 클래스가 랜덤값과 입력값을 가져 한곳에서 이 데이터를 가져다가 사용하는 것이 적절하다고 판단하여 위와 같이 프로퍼티로 저장해두었다.

❗️형변환 확인하기

테스트 케이스를 작성할 때, answers 배열의 요소를 문자열로 넣어준 것을 제대로 확인하지 않아 왜 자꾸 문제가 발생하는지 제대로 파악하기 어려웠다. 아마도 Jest를 사용한 디버깅이 익숙하지 않은 탓이었다. 때문에 여기서 의외로 많은 시간을 보냈다. 단위 테스트를 자주 하면서 Jest 디버깅에도 익숙해지도록 노력해야겠다.

🏓 소감

저번주에 이어 2주차에 접어들게 되었다. 학원이 끝나고 혹은 주말에 틈틈히 문제를 풀었는데도 익숙하지 않는 Jest 테스트 방법 때문에 해결방법도 모른체로 삽질(?) 을 많이했던 한주였다.

그래도 주변에 물어도보고 검색도 하면서 구현까지는 어찌저찌 완성하였다.

하지만 리팩터링에 시간을 많이 쏟지 못한 것이 아쉬웠다. 우테코 문제 푸는 것도 중요하지만 당장 리액트 수업도 복습해야하고, CS 공부도 해야하고 알고리즘 문제도 풀어야하고 해야할 일이 너무 많다.

하지만 꾸준히 계획을 세워서 해나갈 것이니 언젠간 좋은 날이 올 것이라고 믿는다.

댓글 공유

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를 사용하고 있고 앞으로 자주 사용해보면서 테스트 주도 개발에 대해 몸을 익히도록 해야겠다.

댓글 공유

degit 이란?

카테고리 git

📌 degit 이란?

degit은 git 저장소의 복사본을 만드는 명령어이다.

git clone vs degit

git clone의 경우 .git 파일까지 같이 가져오므로 파일 용량이 크고 복제하려는 대상이 git으로 관리되고 있다는 내용을 굳이 가져올 필요가 없는데 .git 파일을 가져온 다는 점은 불편하다.

degit은 .git 파일을 다운로드 하지 않아 git clone 보다 훨씬 빠르다는 장점이 있다.

즉, degit으로 복제를 할 경우 버젼관리 없는 새로운 프로젝트를 다운 받을 때 용이하다.

1
npx degit [프로젝트 url] [로컬 디렉토리명]

위와 같이 npx 명령어로 degit을 설치하지 않고 사용할 수 있다.

댓글 공유

loco9939

author.bio


author.job