본문 바로가기

(31)

JDK 21, Virtual Thread

Virtual Thread란…? JDK 21에 새롭게 들어온 개념 (2023.09.19 에 LTS 출시) gradle 8.4v 부터 지원 kotlin v1.9.20 부터 21 바이트 코드 지원 Spring 6.1, Spring boot 3.2 부터 지원 Jetbrain Intellij 2023.3 JDK 21(LTS)에 추가된 경량 스레드, OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 수십만 ~ 수백만개의 스레드를 동시에 사용할 수 있게한다. 전통적인 Java의 Thread Java의 Thread는 OS Thread를 랩핑한 것 (Platform Thread) Java 애플리케이션에서 Thread를 사용하면 실제로 OS Thread를 사용한 것 OS Thread는 생성 갯수가 제한적..

Querydsl - 기본문법

김영한님의 Querydsl 강의를 수강하며 정리한 내용입니다. 백엔드 기술은 SpringBoot + Spring Data JPA를 조합해서 사용하는데, 이 조합으로 해결하지 못하는 한계점이 바로 복잡한 쿼리 즉, 동적 쿼리를 짜야할 때이다! 이런 문제를 해결해주는 것이 Querydsl이다! Querydsl은 쿼리를 자바 코드로 작성할 수 있고 문법 오류를 컴파일 시점에 확인할 수 있다. 동적 쿼리 문제를 해결해준다. SQL과 문법이 유사하여 쉽게 학습할 수 있다. Querydsl + Spring Data JPA -> 개발이 즐거워진다! Querydsl + Spring Data JPA 장점 단순 반복 X (쿼리도 코드로) Querydsl은 단순히 JPQL Builder 이다! 따라서 JPA의 특징들 (ex..

스무디 한 잔 마시며 끝내는 리액트 + TDD - 5장 나의 첫 리액트 프로젝트

한동안 팀프로젝트로 꽤 바쁜 일정을 소화하느라 스무디 한잔을 할 여유도 없었다... 😫 방학을 맞이하여 최소 한 챕터라도 보자는 마음으로 일단 책을 폈다. 그런데 이번장 너무 재밌겠잖어 ~? 😆 무려 시작부터 관심사였던 타입스크립트를 소개한다. 타입스크립트 리액트는 js이며, js는 동적 프로그래밍 언어(Dynamic Programming Language)이다. 동적 프로그래밍 언어는 런타임 시 변수의 타입이 결정된다. 그렇기 때문에 변수의 타입 때문에 발생한느 버그와 에러는 js를 실행해 보지 않으면 알 수가 없다. 리액트에서는 이런 문제를 해결하고자 플로우(Flow)라는 정적 타입 분석기를 사용할 수 있다. 플로우는 페이스북에서 만들었고, 리액트, 리액트 네이티브에서 변수의 타입을 미리 지정하여 변수..

스무디 한 잔 마시며 끝내는 리액트 + TDD - 4장 리액트 테스트 - react-testing-library

react-testing-library Jest는 자바스크립트 테스트 프레임워크로 js를 전반적으로 테스트하기 위한 프레임워크이다. 리액트토 js이기는 하지만, JSX를 사용하고 있으므로 일반적인 js는 아니다. 또한, 리액트의 JSX는 HTML의 DOM을 다루기 때문에 단순한 js의 테스트로 정확한 오류를 잡아내기는 어렵다. 리액트뿐만 아니라 최근 프론트엔드(Frontend) 프레임워크, 라이브러리인 앵귤러와 Vue도 js에서 DOM을 직접 다루기 때문에 일반적인 js 테스트 프레임워크로는 모든 테스트를 수행하기 어렵다. @testing-library는 이런 문제를 해결하고자 만들어진 DOM 테스팅 라이브러리(DOM Testing Library)이다. @tesing-library는 사용자 중심 방식으로..

스무디 한 잔 마시며 끝내는 리액트 + TDD -3장 리액트의 테스트 - Jest

테스트 코드가 없는 코드를 짜기 싫어하는 나에게 가장 매력적인 파트가 아닐까 싶다. 우테코에서 제작근로를 하면서, Jest, Cypass(?) 등 프론트 진영의 테스트 프레임워크(라이브러리? 프레임워크? 뭐가 맞는 표현일까?)를 언급하는 것을 많이 들어봤지만 한 번도 써본적은 없다. 해당 책에서는 'TDD로 리액트 프로젝트 완성하기'라는 거창한 목표가 있기에 벌써 기대가 되는 파트다. 이번 장에서는 리액트 테스트에 많이 사용되는 자바스크립트 테스트 프레임워크인 Jest에 대해서 살펴보도록 한다. Jest 공식 홈페이지: https://jestjs.io/ Jest의 장점 Jest는 페이스북에서 개발, 관리하는 js 테스트 프레임워크로써 단순함에 집중한 테스트 프레임워크이다. Jest는 자바스크립트 테스트 ..

스무디 한 잔 마시며 끝내는 리액트 + TDD - 1장 리액트란?

먼저 갑자기 리액트??? 하고 의문을 품을 여러 지인들에게 상황을 간략히 설명하자면 네 제가 그 5명 중 하나입니다. 뭐 엄청나게 상세하게 정리하려는 것은 아니고 각 장별로 기록하고 싶은 부분을 기록해나가면서 넘어가려고한다. (나중에 어드민 페이지 정도는 리액트로 만들어줘야 "오 프론트도 쫌 할줄아는데?" 소리한번 들어보는 것을 목표로...) 자바스크립트의 역사 리액트는 자바스크립트(JavaScript, 이하 js) 언어를 기반으로 동작하는 라이브러리이다. 따라서 리액트를 이해하기 위해서는 기본적으로 js에 대한 이해가 필요하다. js의 역사를 간략히 훑으면서 리액트가 탄생한 이유를 이해해보자! (js의 탄생 배경은 한번쯤은 들어봤을 수도 있다. 이번 기회에 해당 영상을 통해서 알아보는 것도 좋을 것 같..

프로그래밍 공부/Java

JDK 21, Virtual Thread

728x90

Virtual Thread란…?

  • JDK 21에 새롭게 들어온 개념 (2023.09.19 에 LTS 출시)
    • gradle 8.4v 부터 지원
    • kotlin v1.9.20 부터 21 바이트 코드 지원
    • Spring 6.1, Spring boot 3.2 부터 지원
    • Jetbrain Intellij 2023.3

JDK 21(LTS)에 추가된 경량 스레드, OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 수십만 ~ 수백만개의 스레드를 동시에 사용할 수 있게한다.

전통적인 Java의 Thread

  • Java의 Thread는 OS Thread를 랩핑한 것 (Platform Thread)
  • Java 애플리케이션에서 Thread를 사용하면 실제로 OS Thread를 사용한 것
  • OS Thread는 생성 갯수가 제한적이고 생성, 유지하는 비용이 비쌈
  • 이 때문에 애플리케이션에서는 플랫폼 스레드를 효율적으로 사용하기 위해 Thread Pool을 사용함
 

위와 같은 동작 매커니즘에의한 Throughput(처리량) 의 한계

  • 기본적인 Web Request 처리 방식은 Thread Per Request(하나의 요청/하나의 스레드)
  • 처리량을 높이려면 스레드 증가 필요, But 스레드는 한정적이다. (OS 스레드 제약)

특히나 Blocking I/O 쪽에서 많은 문제가 발생함.

  • Thread 에서 I/O 작업을 처리할 때, Blocking이 일어난다.
  • 작업을 처리하는 시간보다 대기하는 시간이 길다.
 

그래서 우리는 Reactive Programming을 사용했었음.

  • Webflux 스레드를 대기하지 않고 다른 작업 처리 가능 (장점)
  • 코드를 작성하고 이해하는 비용이 높다 (단점)
  • Reactive하게 동작하는 라이브러리 지원을 필요로 한다 (단점)
  • JPA를 사용할 수 없고 R2DBC라는 래퍼런스가 상대적으로 적은 라이브러리를 사용해야 함 (단점)
 

그렇다면 왜 이렇게 구성이 되어있을까…?

Java Design

  • 자바의 디자인은 ‘스레드 중심’으로 구성되어있다.
  • Exception Stack trace, Debugger, Profiling 모두 스레드 기반
  • Reactive할 때, 작업이 여러 스레드를 거쳐 처리되는데, 컨택스트 확인이 어려워 디버깅이 어려움.

Virtual Thread가 해결하고자하는 문제

  • 애플리케이션의 높은 처리량 확보
    • Blocking 발생시 내부 스케줄링을 통해 다른 작업을 처리 (= 기존 자바의 한계)
  • 자바 플랫폼의 디자인과 조화를 이루는 코드 생성
    • 기존 스레드 구조 그대로 사용(= Webflux가 못한부분)
 

결론은 Virtual Thread는 Reacitve와 MVC의 장점만 차용한 케이스!

 
 

Virtual Thread가 앞에 따로 존재함.
뒤에 Fork/Join Pool이 Carrier Thread(Platform Thread와 거의 동일한 형태)
Carrier Thread는 OS Thread와 1:1 매핑되는 구조이긴 하지만 실제 Application에서는 Platform Thread가 아니라 Virtual Thread만 사용하게된다.

 
  • Virtual Thread가 Blocking되면 Virtual Thread와 Carrier Thread에서 Unmount 된다.
  • 그리고 다른 Virtual Thread가 해당 Carrier Thread와 Mount 된다.
  • OS Thread가 갯수에 제한이 있는 것에 비해서 Virtual Thread는 엄청난게 많이 생성할 수 있다.
 

사용법 예시

// Virtual Thread 방법 1
Thread.startVirtualThread(() -> {
    System.out.println("Hello Virtual Thread");
});

// Virtual Thread 방법 2
Runnable runnable = () -> System.out.println("Hi Virtual Thread");
Thread virtualThread1 = Thread.ofVirutal().start(runnable);

// Virtual Thread 이름 지정 
Thread.Builder builder = Thread.ofVirtual().name("JVM-Thread");
Thread virtualThread2 = builder.start(runnable);

// 스레드가 Virtual Thread인지 확인하여 출력
System.out.println("Thread is Virtual? " + virtualThread2.isVirtual());

// ExecutorService 사용 
try(final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 3; i++) {
        executorService.submit(runnable);
    }
}

Spring Boot(MVC) 적용법 (3.2 이상)

# application.yaml

spring:
  threads:
    virtual:
      enabled: true

Spring Boot(MVC) 적용법 (3.x)

// Web Request를 처리하는 Tomcat이 Virtual Thread를 사용하여 유입된 요청을 처리하도록 한다. 
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
    return protocolHandler -> {
        protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }
}

// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
    return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}

유의사항

유의사항 1

  • Platform Thread => Virtual Thread (X)
    • 전통적으로 사용하던 이 개념으로 Virtual Thread를 쓰게되면 성능 향상을 체감할 수 없을 것이다.
  • Task => Virtual Thread (O)

리소스라고 생각하기보다는 Task별로 Virtual Thread 할당

유의사항 2

Thread Local 사용시 주의

  • Platform Thread Pool을 사용할 때, 공유를 위해 ThreadLocal을 사용하던 관습
  • Virtual Thread는 Heap을 사용하기 때문에 이를 남발하면 메모리 사용이 늘어남.
    • Platform Thread와는 달리 Virtual Thread는 수십 수백만개까지 생성될 수 있기 떄문에 메모리 점유량이 확 늘어날 수 있어 메모리 이슈 발생 가능성이 있음.

유의사항 3

synchronized 사용시 주의

synchronized 사용시 Virtual Thread에 연결된 Carrier Thread가 Blocking 될 수 있으니 주의
(이런 경우를 pinning 이라고 함)

// synchronized 사용 (pinning 발생)
// 순차적 접근을 보장한다. 
public synchronized String accessResource() {
    return access();
}

// ReentrantLock 사용 (pinning 발생하지 않음)
private static final ReentrantLock LOCK = new ReentrantLock();

public String accessResource() {
    // 순차적 접근을 보장한다.
    LOCK.lock();
    try {
        return access();
    } finally {
        LOCK.unlock();
    }
}

I/O와 관련된 부분에 있어서는 Virtual Thread를 적용했을 때, 상당히 큰 효과를 누릴 수 있다.

적합한 사용처

  • I/O Blocking이 발생하는 경우 Virtual Thread가 적합
  • CPU Intensive 작업에는 적합하지 않음
    • ex) 이미지 프로세싱을해서 썸네일을 만든다. 동영상 인코딩을 한다거나. (I/O Intensive 작업이 아니라 CPU Intensive 작업이기 때문에 Virutal Thread를 도입한다고해서 크게 달라질게 없다)
  • Spring MVC 기반 Web API 제공시 편리하게 사용할 수 있다.
    • 높은 Throughput을 위해서 Webflux를 고려중이라면 대안이 될 수 있다.
    • Webflux 도입에도 (1)Stream 형태의 서비스를 제공해야하는 경우 vs (2)단순히 많은 처리를 하고 싶어서 가 있을 수 있는데, 이 중 (2)의 케이스는 Virtual Thread라는 새로운 대안으로 고민해볼 수 있다.

Virtual Thread에 대한 오해

  • Virtual Thread는 기존(Platform) Thread를 대체하는 것이 목적이 아니다.
  • Virtual Thread는 기다림에 대한 개선, 그리고 플랫폼 디자인과의 조화
  • 도입한다고 무조건 처리량이 높아지지 않는다.
  • Virtual Thread는 그 자체로 Java의 동시성을 완전히 개선했다고 보기는 어렵다.

Virtual Thread의 제약

  • Thread Pool에 적합하지 않다. Task 별로 Virtual Thread를 할당해야 함
  • Thread Local 사용시 메모리 사용이 늘어날 수 있음
  • synchronized 사용시 주의가 필요함. (carrier thread가 blocking 될 수 있음)
    • ReentrantLock을 사용
  • 제한된 리소스의 경우 semaphore를 사용

참고 자료

  • https://www.youtube.com/watch?v=vQP6Rs-ywlQ
  • https://techblog.woowahan.com/15398/
728x90
프로그래밍 공부/JPA

Querydsl - 기본문법

728x90

김영한님의 Querydsl 강의를 수강하며 정리한 내용입니다.

백엔드 기술은 SpringBoot + Spring Data JPA를 조합해서 사용하는데, 이 조합으로 해결하지 못하는 한계점이 바로 복잡한 쿼리 즉, 동적 쿼리를 짜야할 때이다!

이런 문제를 해결해주는 것이 Querydsl이다!

Querydsl은

  • 쿼리를 자바 코드로 작성할 수 있고 문법 오류를 컴파일 시점에 확인할 수 있다.
  • 동적 쿼리 문제를 해결해준다.
  • SQL과 문법이 유사하여 쉽게 학습할 수 있다.

Querydsl + Spring Data JPA -> 개발이 즐거워진다!

Querydsl + Spring Data JPA 장점

  • 단순 반복 X (쿼리도 코드로)

Querydsl은 단순히 JPQL Builder 이다! 따라서 JPA의 특징들 (ex. 지연로딩)

JPQL vs Querydsl (W. 영한님)

image

JPQL이 제공하는 모든 검색 쿼리 조건

eq : '=' 조건 
ne : '!=' 조건 (eq과 not의 조합으로도 동일한 조건 걸 수 있음!) 

ex) 아래의 두 조건식은 동일하다! 
member.username.ne("member1")
member.username.eq("member1").not()

isNotNull : null이 아니다.

in(1, 10) : 'in 조건' 1, 10 중 하나라도 해당 
notIn(1, 10) : 'in 조건'의 not  1, 10 중 하나라도 아닌 경우
between(1, 10) : 'between 조건`   1 ~ 10에 포함 

like("member") : 'like 검색' member와 정확히 일치하는 값 검색 
contains("member") : 'like %member% 검색', member라는 단어가 포함된 값 검색 
startsWith("member") : 'like member% 검색' member로 시작되는 값 검색

goe : greater than equal
gt : greater than
loe : lower than equal
lt : lower than

결과 조회

* fetch() : 리스트 조회, 없으면 빈 리스트 반환
* fetchOne() : 단건 조회 
    * 결과가 없으면 null
    * 결과가 둘 이상있으면, `com.querydsl.core.NonUniqueResultException` 예외 발생 
* fetchFirst() : `limit(1).fetchOne()`
* fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 
* fetchCount() : count 쿼리로 변경해서 count 수 조회 

참고)

더보기

실제 content를 가지고 오는 쿼리와 실제 total count를 가지고 오는 쿼리가 다를 경우가 있다.
복잡하고 성능이 중요한 곳에서의 페이징 쿼리는 이 함수를 쓰면 안된다.
이런 경우에는 쿼리 두 개를 따로 날리는 것이 좋다.

fetchResults는 deprecated 되었다!

fetchCount 또한 deprecated 되었다!

정렬 순서

내용이 간단해서 테스트 예제로 대체

/**
     * 회원 정렬 순서
     * 1. 회원 나이 내림차순 (DESC)
     * 2. 회원 이름 올림차순 (ASC)
     * 단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
     */
    @Test
    internal fun sort() {
        entityManager.persist(Member(null, age = 100))
        entityManager.persist(Member("member5", age = 100))
        entityManager.persist(Member("member6", age = 100))

        val result = queryFactory.selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(
                member.age.desc(),
                member.username.asc().nullsLast(), // nullFirst()도 있음 
            )
            .fetch()

        assertThat(result[0].username).isEqualTo("member5")
        assertThat(result[1].username).isEqualTo("member6")
        assertThat(result[2].username).isNull()
    }

참고자료

728x90

'프로그래밍 공부 > JPA' 카테고리의 다른 글

3. 영속성 관리  (0) 2020.03.14
2. JPA 시작  (0) 2020.03.11
1. JPA 소개  (0) 2020.03.10
프로그래밍 공부/React

스무디 한 잔 마시며 끝내는 리액트 + TDD - 5장 나의 첫 리액트 프로젝트

728x90

한동안 팀프로젝트로 꽤 바쁜 일정을 소화하느라 스무디 한잔을 할 여유도 없었다... 😫
방학을 맞이하여 최소 한 챕터라도 보자는 마음으로 일단 책을 폈다. 그런데 이번장 너무 재밌겠잖어 ~? 😆
무려 시작부터 관심사였던 타입스크립트를 소개한다.

타입스크립트

리액트는 js이며, js는 동적 프로그래밍 언어(Dynamic Programming Language)이다. 동적 프로그래밍 언어는 런타임 시 변수의 타입이 결정된다. 그렇기 때문에 변수의 타입 때문에 발생한느 버그와 에러는 js를 실행해 보지 않으면 알 수가 없다. 리액트에서는 이런 문제를 해결하고자 플로우(Flow)라는 정적 타입 분석기를 사용할 수 있다.

플로우는 페이스북에서 만들었고, 리액트, 리액트 네이티브에서 변수의 타입을 미리 지정하여 변수의 타입으로 발생하는 문제를 해결한다.
하지만 이번에는 타입스크립트(Type Script, 이하 ts)를 사용하려고 한다. ts에 대해 더 자세히 공부하고 싶다면 공식 사이트를 참고하라.

타입스크립트를 권장하는 이유

ts는 js 전반에 걸쳐 사용할 수 있기 때문이다. 따라서 플로우보다 좀 더 범용적으로 사용할 수 있다. 또한 많은 js 라이브러리에서 이미 ts 타입 정의 파일(DefinitelyTyped)을 제공하고 있다. 우리는 타입 정의 파일을 통해 라이브러리를 사용하기 위한 올바른 데이터 타입, 매개변수를 쉽게 확인할 수 있다.

텍스트 에디터에서의 지원이 좋다. 특히 MS가 만든 VSCode 에디터는 기본적으로 TS를 지원하고 있으며, 이는 개발 생산성에 크게 도움이 된다. 물론 아톰, WebStorm, Sublime Text 등 많은 에디터에서도 사용할 수 있다. ts의 공식 사이트를 확인하여 자신의 에디터에 맞는 방법으로 에디터를 설정하길 바란다.

이제 create-react-app으로 생성한 리액트 프로젝트에 ts를 적용하기 위해 리액트 프로젝트를 준비해 보자.
생성한 리액트 프로젝트에 ts를 적용하기 위해서는 ts 라이브러리와 리액트의 타입이 정의된 타입 정의 파일을 설치할 필요가 있다. 다음 명령어를 사용하여 ts타입 정의 파일을 설치한다.

# cd my-app
npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest 

여기서 설치한 라이브러리와 타입 정의 파일은 다음과 같다.

  • typescript: ts 라이브러리
  • @types/node: 노드의 타입이 정의된 타입 정의 파일
  • @types/react: 리액트의 타입이 정의된 타입 정의 파일
  • @types/react-dom: react-dom의 타입이 정의된 타입 정의 파일
  • @types/jest: Jest의 타입이 정의된 타입 정의 파일

혹시나 위의 작업을 수행하는데 아래와 같은 경고가 뜬다면....
image
해당 참고 자료를 참고해서

이제 프로젝트에 ts를 설정하기 위해 tsconfig.json 파일을 프로젝트 루트 폴더(./my-app/tsconfig.json)에 만들고 다음 내용을 추가한다.

{
  "compilerOptions": {
    "target": "est5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSuntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCaseInSwitch": true,
    "module": "esnext",
    "moduleResolution": "noed",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
     "src",
     "custom.d.ts"
  ]
}

위의 스크립트를 일일이 수작업으로 작성했는데, 인텔리제이(얼티밋 버전)를 이용한다면, 자동완성이 되니까 적극 활용하자!(VSCode도 지원하지 않을까..? 👀)
image

이렇게 타입스크립트 설정을 끝냈다면 js 파일들을 ts 파일로 변경해야 한다.

  • ./src/App.js 파일을 ./src/App.tsx로 변경
  • ./src/App.test.js 파일을 ./src/App.test.tsx로 변경
  • ./src/index.js 파일을 ./src/index.tsx로 변경
  • ./src/reportWebVitals.js 파일을 ./src/reportWebVitals.ts로 변경
  • ./src/setupTests.ts 파일을 ./src/setupTests.ts로 변경

여기서 .tsx 파일은 TypeScript JSX 파일을 의미하며 .ts 파일은 TypeScript JavaScript 파일을 의미한다. 이렇게 ts를 사용함을 알리기 위해서는 위와 같이 파일 확장자명을 변경해야 한다.

다음으로 ts를 사용하여 js 코드를 ts에 맞게 변경해야 한다. 우선 App.tsx 파일과 App.test.tsx 파일 상단에 다음과 같이 추가한다.

ts 혹은 tsx 파일로 모든 파일 확장자를 변경한 모습
image

App.tsx와 App.test.tsx에 import React from 'react';를 추가하고 reportWebVitals.ts 파일을 열어 아래와 같이 수정한다.
(책의 내용을 직관적으로 이해하기 어려웠는데... IntelliJ가 오류 표시를 해줘서 바로 이해했다. 또한 자동 import도 해주기 때문에 더더욱... 갓텔리제이 짱짱 👍)
image

마지막으로 ts 파일에서 svg 파일을 ts에서 불러올 수 있게 하도록 ./src/custom.d.ts 파일을 생성하고 다음과 같이 수정한다. 여기서 생성하는 d.ts 파일은 타입 정의 파일로 ts 인식하지 못하는 타입이나 ts 내에서 사용할 타입들을 정의할 때 사용한다.

declare module '*.svg' {
  import * as React from 'react';

  export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;

  const src: string;
  export default src;
}

이렇게 모든 설정이 끝났다면 App.test.tsx 파일을 열어 앞의 장에서 작성한 테스트 코드를 다음과 같이 복붙하여 npm run test로 테스트 코드를 실행해본다.

image

앗... 실패했다.
에러 로그를 읽어보니 error TS6046: Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'es2021', 'esnext'. 라고 한다. 이와 관련된 설정은 처음에 tsconfig.json에서 해줬던 것 같다는 기억에 찾아가보니...

image
'est5'라니...ㅎㅎ 오타 주의합시다... 이 외에도 noFallthroughCaseInSwitch라는 컴파일러 옵션이 없다고한다. (Did you mean 'noFallthroughCasesInSwitch'? 이라며 자신의 똑똑함을 과시한다... 눈이 침침해서 그런가... 's' 하나 빠진걸 한참을 찾지 못했다 😂)

image

스크립트를 올바르게 고쳐주니 정상적으로 동작한다!

이 테스트로 우리는 리액트 컴포넌트를 js에서 ts로 리팩토링했음에도 아무 문제 없이 우리가 표시하고자 하는 화면이 잘 표시되었음을 알 수 있다. 이렇게 테스트 코드는 우리가 안심하고 코드를 변경할 수 있게 도와준다.

npm start 명령어를 실행해보니 리액트 프로젝트가 정상적으로 실행되는 것도 확인햇다!

이로써 create-react-app을 사용하여 생성한 리액트 프로젝트에 ts를 설정하는 방법을 알아봤다. 하지만 너무 복잡하다.. 이런 복잡함을 개선하고자 create-react-apptemplate이라는 옵션을 제공하고 있다. 다음 명령어를 사용하여 새로운 리액트 프로젝트를 생성해 보자.

npx create-react-app my-app-typescript --template=typescript

위의 명령어로 생성된 프로젝트를 확인해보면 우리가 앞에서 template 옵션 없이 생성한 리액트 프로젝트에 ts를 적용한 것과 같다는 것을 알 수 있다. 이렇게 우리는 create-react-apptemplate 옵션으로 ts가 적용된 리액트 프로젝트를 간단하게 생성할 수 있다.

이전 예시와 똑같이 test 코드들을 App.test.tsx 파일에 복붙하여 실행해보자.
테스트 결과는 앞의 결과와는 달리 테스트가 실패한다.

콘솔창에 표시된 에러 내용을 자세히 살펴보면 <p /> 태그의 설명문에서 에러가 난 것을 확인할 수 있다. 따라서 App.tsx 파일을 열어 <p /> 태그의 설명문 부분을 확인해보면 설명문 내용이 src/App.js가 아닌 src/App.tsx로 표시된 것을 확인할 수 있다.
바뀐 텍스트에 맞게 테스트 코드 또한 수정한다. 그러면 테스트 코드가 정상적으로 동작한다!

또한 리액트 프로젝트 또한 정상적으로 실행된다.

styled-components

리액트는 웹 애플리케이션 개발에 사용하는 라이브러리이다. 그러므로 스타일링에는 물론 웹 개발과 동일한 CSS(Cascading Style Sheets)를 사용할 수 있다.

우선 리액트 스타일링을 사용하는 방법을 이해하기 위해 다음의 명령어를 사용하여 ts 새로운 리액트 프로젝트를 생성해보자.
npx create-react-app my-app-style --template=typescript

이번에는 인텔리제이로 새로운 ts기반 react 프로젝트를 생성해본다.
image

내용은 확인해보면 동일하다!

우리는 리액트 프로젝트의 폴더와 파일 구조를 확인했다. 리액트도 웹 서비스이므로 기본적으로 HTML 파일이 필요하고 public/index.html 파일이 이 역할을 한다고 설명했다.

그러므로 보통의 웹 페이지처럼 CSS 파일을 생성하고 <link /> 태그를 추가하여 웹 서비스의 스타일링을 할 수 있다. 그렇다면 ./public/temp.css 파일을 생성하고 다음과 같이 수정한다.

.App-header {
  background-color: red !important;
}

우리가 만든 CSS가 제대로 적용되는지 확인하기 위해 .App-header 클래스의 배경에 강제적(import)으로 빨간색을 표시하도록 했다. 그리고 ./public/index.html 파일을 열어 다음과 같이 수정한다.

<link rel="stylesheet" href="%PUBLIC_URL%/temp.css">
<title>React App</title>  <!--기존에 있던 내용-->

보통의 웹 서비스처럼 <link /> 태그를 사용하여 우리가 만든 temp.css 파일을 추가하였다. create-react-app으로 만든 프로젝트의 public 폴더는 위와 같이 %PUBLIC_URL%을 사용하여 지정한다. 이렇게 추가했다면 실제로 우리가 만든 CSS가 제대로 적용되는지 확인해보자.

그 결과는 엄청났다(리액트 납량특집...)
image

하지만 리액트는 보통의 웹 서비스 개발과는 다르게 컴포넌트 중심으로 개발한다. 그러므로 이렇게 모든 CSS를 한 곳에서 관리하게 되면 어떤 컴포넌트에서 어떤 스타일을 활용하는지 쉽게 알 수 없다. 그래서 리액트에서는 CSS 파일을 리액트 컴포넌트 파일에서 import하는 방식으로 스타일도 컴포넌트 중심으로 설계할 수 있도록 하고 있다.

우리가 create-react-app 명령어로 생ㅅ어한 프로젝트의 리액트 컴포넌트인 ./src/App.tsx 파일을 열어보면 다음과 같이 CSS 파일을 import하는 것을 확인할 수 있다.

...
import './App.css';
...

리액트는 위와 같이 JSX 파일에서 직접 CSS를 import하는 것으로 해당 리액트 컴포넌트가 어떤 스타일을 사용하는지 알 수 있도록 하고 있다. 해당 스타일 파일이 적용되고 있는지 확인하기 위해 ./src/App.css 파일을 열어 다음과 같이 배경 색상을 강제적으로 되돌리도록 수정한다. (동일하게 !important를 뒤에 붙인다)

다시 리액트 프로젝트를 실행해보면 빨간 배경이 원래대로 돌아왔다. 이렇게 리액트에서는 HTML에 <link /> 태그를 통해 CSS를 사용할 수도 있고, 컴포넌트에서 CSS를 import해서 사용할 수도 있다.

보통 리액트는 컴포넌트를 기반으로 개발하게 되며 컴포넌트별로 CSS를 갖는 형식으로 스타일을 관리하게 된다. 하지만 모든 CSS를 한 곳에서 관리하지 않다 보면 CSS의 클래스 명이 중복되어 잘못된 스타일이 적용될 수 있다. 만약 한 곳에서 모든 스타일을 관리하다 보면 어떤 스타일이 컴포넌트에 적용되고 있는지 한눈에 알 수 없다.

이런 문제를 해결하고자 styled-components 라이브러리가 탄생했으며, 리액트에서 이 styled-components를 사용하여 스타일을 적용할 수 있다. 리액트에서 styled-components를 사용하여 스타일링을 하게 되면 다음과 같은 장점이 있다.

  • 클래스 이름 버그 해결
    보통 CSS에 클래스 이름을 생성하고 스타일을 작성한 다음 해당 이름을 HTML 태그에 적용함으로써 스타일을 적용한다. 하지만 이런 방식은 클래스명의 중복, 겹침 또는 철자 오류가 발생할 수 있다. styled-components는 스타일을 컴포넌트에 직접 적용함으로써 이런 문제를 해결하고 있다.

  • 더 쉬운 CSS관리
    일반적인 방식으로 스타일을 적용하면 해당 스타일의 클래스가 코드의 어디에서 사용되는지 쉽게 알 수 없다. styled-components는 모든 스타일이 특정 컴포넌트에 연결되기 때문에 더 명확히 사용되는 스타일을 알 수 있다. 또한, styled-components는 모든 스타일이 특정 컴포넌트에 연결되어있기 때문에 사용되지 않은 불필요한 스타일을 쉽게 제거할 수 있다.

  • 간단한 동적 스타일 적용
    동적인 스타일을 관리하기 위해 여러 클래스를 만들 필요가 없으며 컴포넌트의 상태에 따라 쉽고 직관적으로 동적 스타일을 적용할 수 있다.

  • CSS 자동 구성
    styled-components는 페이지에 렌더링되는 컴포넌트를 추적하여 해당 스타일을 완전히 자동으로 추가한다. 또한, 코드 분할(Code splitting)과 결합하여 사용자가 필요한 최소한의 코드를 자동으로 추가한다.

실무에서도 이런 장점 때문에 styled-components를 많이 사용하며 이 책에서도 앞으로 styled-components를 사용하여 스타일링을 할 예정이다. 처음에는 styled-components를 사용하면 이런 장점이 잘 체감되지 않지만, 자주 사용하다 보면 앞에서 소개한 장점들이 점차 이해될 것이다.

styled-components를 사용하기 위해 다음 명령어를 실행하여 앞에서 만든 리액트 프로젝트에 styled-components를 설치한다.

cd my-app-style
npm install --save styled-components
npm install --save-dev @types/styled-components jest-styled-components

설치가 완료되었다면 styled-components를 사용하여 현재 페이지를 리팩토링해 보자. 일단 styled-components를 사용하기 위해 App.tsx 파일을 열어 다음과 같이 라이브러리를 import 한다.

...
import './App.css';
import Styled from 'styled-components';
...

그리고 styled-components를 사용하여 .App 클래스를 대체할 새로운 컴포넌트를 생성하기 위해 App.tsx 파일에 다음과 같은 코드를 추가한다.

import Styled from 'styled-component';

const Container = Styled.div`
`;
...

styled-components를 사용하여 리액트 컴포넌트를 생성하기 위해서는 Styled.[HTML 태그] 형식과 js의 템플릿 리터럴(Template literals, `)을 사용한다. 이 템플릿 리터럴 기호 안에 다음과 같이 스타일링을 작성함으로써 컴포넌트의 스타일링을 하게 된다.

...
const Container = Styled.div`
  text-align: center;
`;
...

추가한 스타일은 App.css 파일의 .App 클래스명 스타일 내용을 복사/붙여넣기 한 것이다.

.App {
  text-align: center;
}
...

이제 styled-components로 생성한 리액트 컴포넌트를 사용하기 위해 .App 클래스를 사용하는 부분을 찾아 다음과 같이 수정한다.

...
function App() {
  return (
    // <div className="App">
    <Container>
      ...
    </Container>
    // </div>
  );
}
...

기조에 있던 <div /> 태그 부분은 삭제해도 되지만, 비교하기 쉽게 하려고 주석 처리했다. 우리가 만든 styled-components를 사용하기 위해 <div /> 태그의 CSS 클래스 명을 제거하고 <div />태그 대신 우리가 만든 <Container /> 컴포넌트를 사용했다. 참고로 리액트에서는 HTML 태그에 class 대신 className을 사용하여 클래스를 지정한다.

이렇게 App.tsx 파일을 수정하고 저장한 후 브라우저를 확인하면 처음 화면과 똑같은 것을 확인할 수 있다. 즉, 우리가 styled-components를 사용하여 만든 리액트 컴포넌트가 이전의 CSS 방식의 스타일링을 잘 대체 했음을 알 수 있다.

이제 <header /> 태그를 styled-components로 변경해보자. <header /> 태그를 styled-components로 변경하기 위해 App.tsx 파일을 다음과 같이 수정한다.

코드는 아래와 같고 결과는 여전히 똑같다!

image

이제 애니메이션이 포함되어조금 복잡한 .App-logo 클래스를 styled-components로 변경해 보자. .App-logo 클래스를 styled-components로 변경하기 위해 App.tsx 파일을 열어 다음과 같이 수정한다.

...
const AppLogo = Styled.img`
    height: 40vmin;
    pointer-everts: none;
`;
...

image

<img /> 태그 대신 우리가 styled-components로 만든 AppLogo 컴포넌트를 사용했다.

이렇게 수정하고 저장한 후 웹 브라우저를 확인하면 스타일이 잘 적용된 것을 확인할 수 있다. 하지만 아직 회전 애니메이션을 추가하지 않았기 때문에 로고가 회전하지는 않는다.

이제 우리가 styled-components을 사용하여 만든 컴포넌트에 회전 애니메이션을 추가하기 위해 App.tsx 파일을 열어 다음과 같이 회전 애니메이션을 추가한다.

...
const AppLogo = Styled.img`
    hieght: 40vmin;
    pointer-events: none;

    @media (prefers-reduced-motion: no-preference) {
        animation: App-logo-spin infinite 20s linear;
    }

    @keyframes App-logo-spin {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }
`;

이 역시 App.css 파일의 내용을 복사하여 붙여넣은 것이다. 다만, 차이가 있다면 CSS에서는 애니메이션을 사용할 클래스명을 다음과 같이 지정하였다.

...
@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}
...

하지만 styled-components에서는 해당 컴포넌트가 직접 애니메이션을 수행하므로 클래스명을 특별히 지정하지 않아도 된다.

이렇게 수정한 후 App.tsx 파일을 저장하고 브라우저를 확인해보면 이전과 같이 로고 이미지가 잘 회전하고 있는 것을 확인할 수 있다.

이처럼 애니메이션이 한 곳에서만 사용되는 경우, 하나의 컴포넌트에 전부 선언하여도 된다. 하지만 만약 여러 곳에서 같은 애니메이션을 사용한다면 다음과 같이 애니메이션을 분리하여 사용할 수 있다.

애니메이션을 분리하기 위해서 styled-components의 keyframes을 사용할 필요가 있다. styled-components의 keyframes을 사용하기 위해 App.tsx 파일을 열어 다음과 같이 수정한다.

...
import Styled, {keyframes} from 'styled-components';
...

그리고 다음과 같이 styled-components의 keyframes을 사용하여 우리가 로고 이미지에서 사용할 회전 애니메이션을 선언한다.

...
const spin = keyframes`
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
`;

마지막으로, 실제 애니메이션을 사용하느 부분에 다음과 같이 styled-components의 keyframes를 사용해 생성한 애니메이션을 추가한다.

...
const AppLogo = Styled.img`
    height: 40vmin;
    pointer-events: none;

    @media (prefers-reduced-motion: no-preference) {
        animation: ${spin} infinite 20s linear;
    }

    @keyframes App-logo-spin {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }
`;
...

styled-components는 js의 탬플릿 리터럴을 사용하기 때문에 위와 같이 문자열 중간에 js 변수를 사용할 수 있다. 이제 이렇게 수정한 App.tsx 파일을 저장하고 브라우저를 확인하면 여전히 로고의 애니메이션이 잘 동작하는 것을 확인할 수 있다. 이처럼 자주 반복되어 사용되는 애니메이션은 styled-components의 keyframes를 사용하여 미리 정의하고 필요한 부분에서 정의된 애니메이션을 사용하면 된다.

설명문인 <p /> 태그에는 어떤 스타일도 적용되어 있지 않으므로 그대로 <p /> 태그를 유지하도록 한다. 마지막으로 .App-link 클래스를 styled-components로 변경해 보자.

.App-link 클래스를 styled-components로 만들기 위해 App.tsx 파일을 다음과 같이 수정한다.

...
const AppLink = Styled.a`
    color: #61dafb;
`;
...

이처럼 App.test.tsx 파일을 수정한 후, 테스트 코드를 실행하면 이전과는 다르게 다음과 같은 에러를 확인할 수 있다.

image

우리는 styled-components를 사용하여 모든 HTML 요소에서 클래스를 제거하였기 때문에 너무도 당연한 에러가 발생하였다.
글면 이 에러를 수정하기 위해 테스트 코드를 수정해 보자. App.test.tsx파일을 열어 다음과 같이 에러가 나는 부분을 수정해준다.

...
const AppLogo = screen.getByAltText('logo');
expect(appLogo).toBeInTheDocument();
expect(appLogo).toHaveAttribute('src', 'logo.svg');
...

수정한 내용을 살펴보면 react-testing-library의 screen을 활용하여 화면에서 logo라는 alt 속성을 가진 HTML 요소를 찾은 다음, 해당 요소가 화면에 표시되었는지를 Jest의 toBeInTheDocument를 사용하여 확인하였다. 또한, 해당 요소가 우리가 원하는 로고 이미지를 제대로 표시하는지 확인하기 위해 HTML의 src 속성을 확인하여 logo.svg 이미지가 제대로 표시되는지 확인하였다.

이렇게 수정하니 테스트가 정상적으로 통과한 것을 확인할 수 있다. 이를 통해 우리가 styled-components를 사용하여 만든 리액트 프로젝트도 문제없이 동작하는 것을 확인할 수 있다.

절대 경로로 컴포넌트 추가

우리는 리액트 프로젝트를 개발할 때 수많은 리액트 컴포넌트를 제작하고 제작한 컴포넌트를 조합하여 페이지를 제작하게 된다. 이처럼 리액트 컴포넌트를 조합할 때 보통은 상대경로를 사용하여 컴포넌트를 불러오게 된다.

몇 개 안되는 컴포넌트를 추가하고 관리할 때에는 큰 문제가 안되지만, 프로젝트가 커지고 수많은 컴포넌트가 추가되고 프로젝트의 폴더 구조가 복잡해지면 이 상대 경로 추가방식은 어떤 경로를 지정하고 있는지 명확하게 파악하기 어려운 단점이 있다.

이런 문제점을 ts의 설정으로 간단히 해결할 수 있다. 우선 테스트하기 위해 다음의 명령어로 새로운 프로젝트를 생성하자.

이제 ts 설정 파일인 tsconfig.json을 열어 다음과 같이 수정한다.

"compilerOptions": {
  ...
  "jsx": "react-jsx",
  "baseUrl": "src"
},
...

이제 ts 설정 파일인 tsconfig.json에 baseUrl을 설정하면 절대 경로로 컴포넌트를 추가할 수 있다. 물론, 상대 경로 추가는 기본적으로 지원하므로 상대 경로와 절대 경로를 동시에 사용할 수 있다.

이제 절대 경로로 컴포넌트를 추가해 보기 위해 src 폴더 하위에 Component라는 폴더를 생성하고 App 컴포넌트와 관련이 있는 파일인 App.css, App.test.tsx, App.tsx, logo.svg을 이동시킨다.

그리고 src/index.tsx 파일을 열어 다음과 같이 절대 경로로 App 컴포넌트를 추가해 본다.

...
import App from './Component/App';
...

현재는 하나의 컴포넌트만 있고 폴더 구조가 많이 복잡하지 않으므로 필요성이 크게 느껴지지 않지만, 앞으로 예제를 진행하면서 이 절대 경로 추가가 얼마나 중요한 역할을 하게 되는지 알게 될 것이다.

리액트 프로젝트를 실행해보면 정상적으로 작동한다.

이제 우리가 만든 컴포넌트를 테스트하기 위해 ./src/Component/App.test.tsx 파일을 열어 이전에 작성한 테스트코드를 테스트 코드를 다음과 같이 복붙한다. 큰 문제없이 테스트 코드 또한 정상적으로 통과한다.

Prettier

Prettier는 js, CSS, JSON 등을 지원하는 코드 포맷터(Code formatter)이다. Prettier는 미리 정의한 코드 스타일에 맞춰 자동으로 코드의 형식을 수정해주는 도구이다. (Prettier: https://prettier.io/)

그렇다면 절대 경로 컴포넌트 추가에서 만든 리액트 프로젝트에 Prettier를 적용하기 위해 다음의 명령어를 실행하여 Prettier를 설치한다.

npm install --save-dev husky lint-staged prettier

Prettier를 활용하기 위해 huskylint-staged도 함께 설치하였다. husky와 lint-staged의 역할은 다음과 같다.

  • husky
    package.json 파일에서 githook을 사용할 수 있게 해준다. githook은 깃에 특정 이벤트(소스 코드 커밋, 푸시 등)를 감지하여 이벤트를 실행하기 전에 특정 동작을 수행할 수 있도록 도와준다.

  • lint-staged
    깃의 stage된 파일들에 특정 동작을 수행할 수 있도록 해준다.

필요한 라이브러리 설치가 완료되었다면 Prettier의 설정 파일을 만들 필요가 있다. 프로젝트의 루트 폴더에 .prettierrc.js 파일을 생성하고 다음과 같이 수정한다.

image

Prettier에서 사용할 기본적인 규칙을 설정하였다. JSX 문법에서 괄호를 같은 라인에 표시하게 하고 싱글쿼터(')를 주로 사용하도록 설정하는 등의 내용이다.

이렇게 설정이 완료되었다면 lint-staged와 husky를 사용하여 깃 이벤트에 Prettier 명령어를 연동해 보자. lint-staged와 husky를 설정하기 위해서 package.json 파일을 열어 script 하단을 다음과 같이 수정한다.

"scripts": {
  ...
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
    "prettier --write"
  ]
},

조금 자세히 살펴보면 husky를 사용하여 깃의 커밋 이벤트 전에(pre-commit) lint-staged 명령어를 실행하도록 설정한 것을 알 수 있다.

그리고 lint-staged에서는 src 폴더 하위 파일 중 JSX, CSS, json 등과 관련 있는 파일들이 있는 경우 Prettier의 명령어를 실행하도록 하였다. 여기서 사용한 write 옵션은 Prettier를 사용하여 파일을 직접 수정하도록 하는 옵이다.

이제 Prettier는 소스 코드를 깃에 커밋할 때 우리가 정의한 규칙을 기반으로 소스 코드의 형식을 자동으로 수정해 줄 것이다.

실무에서는 husky와 lint-staged는 보조적인 도구로 사용, 개발에 사용하는 IDE에 플러그인 등을 추가하여 파일을 수정하고 저장할 때마다 코드의 형식을 자동으로 맞추도록 설정한다!

728x90
프로그래밍 공부/React

스무디 한 잔 마시며 끝내는 리액트 + TDD - 4장 리액트 테스트 - react-testing-library

728x90

react-testing-library

Jest는 자바스크립트 테스트 프레임워크로 js를 전반적으로 테스트하기 위한 프레임워크이다. 리액트토 js이기는 하지만, JSX를 사용하고 있으므로 일반적인 js는 아니다. 또한, 리액트의 JSX는 HTML의 DOM을 다루기 때문에 단순한 js의 테스트로 정확한 오류를 잡아내기는 어렵다.

리액트뿐만 아니라 최근 프론트엔드(Frontend) 프레임워크, 라이브러리인 앵귤러와 Vue도 js에서 DOM을 직접 다루기 때문에 일반적인 js 테스트 프레임워크로는 모든 테스트를 수행하기 어렵다.

@testing-library는 이런 문제를 해결하고자 만들어진 DOM 테스팅 라이브러리(DOM Testing Library)이다. @tesing-library는 사용자 중심 방식으로 UI 컴포넌트를 테스트하는 데 도움을 주는 라이브러리이다.

@testing-library는 리액트 이외에도 앵귤러, Vue, Sevele 등 UI 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 확인할 수 있다

react-testing-library의 장점

react-testing-library는 리액트 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 작성할 수 있다.

react-testing-library도 같은 원리로 리액트 컴포넌트용 테스트 코드 작성을 도와준다. 이 말은 즉, 테스트 코드를 작성할 때 컴포넌트 세부 구현 사항을 포함하지 않으면서도 신뢰할 수 있는 테스트 코드 작성에 도움을 준다. 이렇게 컴포넌트의 세부 구현 사하을 포함하지 않은 테스트 코드를 작성하면 컴포넌트의 세부 구현 부분을 리팩토링하여도 테스트 코드를 수정할 필요가 없다. 이로 인해 한번 작성한 테스트 코드는 긴 시간 유지할 수 있으며 오랜 기간 유지 가능한 테스트 코드는 테스트 코드를 자주 수정하지 않아도 되므로 개발 생산성을 향상 시켜준다.

react-testing-library는 react-dom위에서 동작한다. 다른 테스트 프레임워크는 리액트 컴포넌트를 단순히 인스턴스로 처리하지만, react-testing-library는 실제 DOM 노드에서 작동하므로 더 신뢰할 수 있는 테스트를 할 수 있다.

react-testing-library는 사용자 중심의 테스트 유틸리티를 제공한다. 사용자 중심의 테스트 유틸리티란 react-testing-library를 사용하여 DOM을 찾는 기능들이 실제 사용자 DOM을 사용하는 방식과 유사한 형태로 제공되고 있음을 의미한다. 예를 들어, 텍스트로 폼(Form)의 요소를 찾거나 텍스트에서 링크 및 버튼 등을 찾는 테스트 코드는 마치 사용자가 화면을 보면서 찾는 것과 같은 형태로 테스트 코드를 작성할 수 있도록 돕는다.

react-testing-library는 테스트 실행기 또는 프레임워크가 아니다. 따라서 react-testing-library를 사용하기 위해서는 다른 테스트 프레임워크와 함께 사용해야 한다. react-testing-library의 공식 사이트에서는 함께 사용할 테스트 프레임워크로 Jest를 추천하고 있지만, react-testing-library는 특정 테스트 프레임워크에 종속되어 있지 않았으므로 Jest이 외에 다른 테스트 프레임워크에서도 동작한다.

프로젝트 준비

react-testing-library는 리액트의 컴포넌트를 테스트하기 위한 라이브러리이므로 react-testing-library를 사용하기 위해서는 리액트 프로젝트를 준비할 필요가 있다.

다음 명령어를 실행하여 react-testing-library를 사용할 리액트 리액트 프로젝트를 생성하자.

npx create-react-app react-testing-library-test

react-testing-library 설치

react-testing-library는 Jest와 마찬가지로 create-react-app으로 생성한 리액트 프로젝트에 기본적으로 같이 설치된다. 우리는 create-react-app을 사용하여 리액트 프로젝트를 생성하였으므로 추가적인 설치 과정을 진행하지 않아도 된다.

만약 create-react-app으로 프로젝트를 생성하지 않는 경우에는 다음의 명령어를 실행하여 react-testing-library를 설치해야 한다.

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

사용 방법

create-react-app으로 생성한 리액트 프로젝트의 src 폴더를 열어보면 리액트 컴포넌트가 작성된 App.js 파일과 그에 관한 테스트 코드가 작성된 App.test.js 파일을 확인할 수 있다.

우선, 리액트 컴포넌트가 작성된 App.js 파일을 열어 컴포넌트의 내용을 확인해 보자.

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and saved to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
            Learn React
        </a>
      </header>
    </div>
    );
}

이 App 컴포넌트는 이미지()와 설명문(

), 그리고 리액트 공식 사이트로 이동할 수 있는 링크()를 JSX 문법을 사용하여 화면에 표시하는 단순한 컴포넌트이다.

이제 App.test.js 파일을 열어 이 App 컴포넌트를 테스트하는 테스트 코드를 확인해 보자.

import { render, screen } from '@testing-library/react';
import App from './app';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

App.test.js 파일을 보면 @testing-library/react에서 render와 screen을 불러와 테스트에 사용하고 있음을 알 수 있다. 또한, Jest의 test 함수(it 함수와 같은 역할을 하는 함수)를 사용하여 테스트 명세를 작성한 테스트 코드임을 알 수 있다.

테스트 코드를 자세히 살펴보면 우선, @testing-library/react에서 불러온 render 함수는 리액트 컴포넌트를 화면에 표시하기 위함이고, screen은 리액트 컴포넌트가 표시된 화면을 의미한다. 여기서는 이해하기 쉽도록 표시된 화면이라고 표현했지만, 실제로는 화면에 표시되지는 않는다. 여기서 render 함수는 메모리상에 돔을 만들고 screen을 통해 해당 돔에 접근하는 것을 의미한다.

react-testing-library의 render 함수를 사용하여 App이라는 컴포넌트를 렌더링하였다. 이렇게 렌더링된 컴포넌트에서 screen.getByText를 통해 화면에서 'learn react'라는 글자를 가지고 있는 돔 요소(DOM Element)를 찾고 찾은 요소를 Jest의 expect().toBeInTheDocument()를 사용하여 돔에 표시되어 있는지 확인하였다.

이제 테스트 코드를 실행하기 위해 package.json 파일을 열어 스크립트의 내용을 확인해본다.

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
},

create-react-app에서는 이미 test 스크립트를 기본적으로 지원하고 있음을 확인할 수 있다.
npm run test를 실행하여 Jest 테스트를 실행해 본다.

명령어를 실행하면 Jest가 App.test.js 파일의 테스트 코드를 실행하여 테스트를 수행하고 이전장과 동일한 결과를 표시하는 것을 알 수 있을 것이다.

이제 App.test.js 파일의 테스트 코드를 수정하여 react-testing-library의 사용법을 익혀보도록 하자. App 컴포넌트는 이미지와 설명문도 표시하고 있으므로 해당 이미지와 설명문이 잘 표시되었는지 확인하는 테스트 코드를 추가해 보도록 한다.

이미지와 설명문을 잘 표시하는지 확인하기 전에 테스트의 명세를 다음과 같이 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    render(<App />);
    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
  });
});

앞의장에서 배운 Jest의 내용을 복습하기 위해 Jest의 describe 함수와 it 함수를 사용하였다. 또한, 테스트 명세 안에 링크 이외에 다른 항목도 테스트할 예정이므로 테스트 명세의 설명문을 알기 쉽게 수정하였다.

이미지가 잘 표시되는지 확인하기 위해 다음과 같이 테스트 코드를 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    const {container} = render(<App/>);
    expect(container.getElementsByClassName('App-logo')).toHaveLength(1);
    expect(container.getElementsByClassName('App-logo')[0]).toHaveAttribute(
        'src', 'logo.svg'
    )

    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
  });
});

react-testing-library의 render 함수는 유용한 오브젝트들을 반환하는데 그중 하나인 container를 받아서 사용하였다. container는 리액트 컴포넌트에서 화면에 표시되는 부분을 담고있는 오브젝트이다. 이 container를 사용하여 getElementsByClassName 함수로 화면에 표시되고 있는 <img />를 클래스명으로 찾아가져 왔으며 가져온 HTML 요소 한개가 존재하는지를 toHaveLength 함수로 체크하였다.

또한, getElementsByClassName 함수를 통해 우리가 찾고자 하는 이미지가 존재하는지 확인하였고 해당 이미지가 실제로 우리가 원하는 이미지를 표시하고 있는지 toHaveAttribute 함수를 사용하여 <img/> 태그의 src를 가져와 비교하였다.

현재 npm run test로 Jest의 테스트가 계속 실행 중이므로 이렇게 App.test.js 파일을 수정한 후 저장하면 명령 프롬프트에 다음과 같은 결과가 표시되는 것을 확인할 수 있다.

Watch Usage: Press w to show more.
 PASS  src/App.test.js
  ✓ renders learn react link (30 ms)
  <App />
    ✓ renders component correctly (5 ms)

이제 화면에 설명문(<p/>)이 잘 표시되는지 확인해 보자. 화면에 설명문이 잘 표시되는지 확인하기 위해 테스트 명세를 다음과 같이 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    const {container} = render(<App/>);
    ...
    expect(container.getElementsByTagName('p')).toHaveLength(1);
    expect(container.getElementsByTagName('p')[0]).toHaveTextContent(
        'Edit src/App.js and save to reload.'
    );
  });
});

이번에는 container 오브젝트에서 getElementsByTagName 함수를 사용하여 <p> 태그를 찾고 해당 태그가 한 개 존재하고 있음을 toHaveLength 함수를 통해 테스트하였다. 또한, 해당 <p> 태그가 우리가 화면에 표시되길 원하는 문자열(Edit src/App.js and save to reload.)을 잘 표시하고 있는지 toHaveTestContent 함수를 사용하여 테스트하였다.

이제 App 컴포넌트의 모든 요소가 화면에 잘 표시되는 것을 테스트 코드를 통해 확인하였다. 마지막으로, 화면에 표시되는 내용이 변경되었는지 알기 위해 스냅샷 테스트를 추가한다.

describe('<App/>', () => {
  it('renders component correctly', () => {
    const { container } = render(<App />);
    ...
    expect(container).toMatchSnapshot();
  });
});

이렇게 파일을 수정하여 스냅샷 테스트를 추가한 후 저장하면 이전과는 다르게 다음과 같은 화면을 명령 프롬프트에서 확인할 수 있다.
image

또한, src/__snapshots__/App.test.js.snap이라는 파일이 생성된 것을 확인할 수 있다. 파일을 열어 내용을 확인해 보면 App 컴포넌트가 화면에 렌더링될 때 표시되는 HTML 내용이 저장된 것을 확인할 수 있다.

image

이렇게 저장된 스냅샷은 App 컴포넌트가 수정되어 화면에 표시되는 HTML 구조가 변경되면 에러를 표시하게 된다. 이렇게 스냅샷은 화면에 표시되는 컴포넌트가 변경되었는지 감지하기 위한 테스트로 많이 사용된다.

스냅샷 테스트가 제대로 동작하는지 확인하기 위해 App.js 파일을 열어 다음과 같이 수정한다.

만약 이 상태에서 스냅샷의 내용을 변경하면, 아래와 같이 테스트 코드가 실패하는 것을 확인할 수 있다.

image

이런 스냅샷 테스트는 우리가 리액트 컴포넌트를 수정했을 때, 수정한 내용이 의도치 않게 화면 표시를 변경하는 실수를 알 수 있게 해준다.

만약 컴포넌트를 수정하여 화면 표시가 변경된 것이 의도된 수정이었다면 스냅샷 테스트로 저장된 파일을 업데이트해 주어야 한다. 터미널에 위의 그림과 같은 에러가 표시된 상태에서 키보드의 'u' 키를 누르면 스냅샷으로 생성된 파일이 업데이트된다. 그러면 새롭게 업데이트된 스냅샷 파일이 다시 기준이 되어 변경을 감지하고 에러를 표시하게 된다.

728x90
프로그래밍 공부/React

스무디 한 잔 마시며 끝내는 리액트 + TDD -3장 리액트의 테스트 - Jest

728x90

테스트 코드가 없는 코드를 짜기 싫어하는 나에게 가장 매력적인 파트가 아닐까 싶다. 우테코에서 제작근로를 하면서, Jest, Cypass(?) 등 프론트 진영의 테스트 프레임워크(라이브러리? 프레임워크? 뭐가 맞는 표현일까?)를 언급하는 것을 많이 들어봤지만 한 번도 써본적은 없다.

해당 책에서는 'TDD로 리액트 프로젝트 완성하기'라는 거창한 목표가 있기에 벌써 기대가 되는 파트다.

이번 장에서는 리액트 테스트에 많이 사용되는 자바스크립트 테스트 프레임워크인 Jest에 대해서 살펴보도록 한다.

Jest 공식 홈페이지: https://jestjs.io/

Jest의 장점

Jest는 페이스북에서 개발, 관리하는 js 테스트 프레임워크로써 단순함에 집중한 테스트 프레임워크이다. Jest는 자바스크립트 테스트 프레임워크이므로 리액트 이외에 TypeScript, Node, Angular, Vue 등에서도 사용할 수 있다.
(참고로 이 외에도 자바스크립트의 테스트 프레임워크에는 Mocha, Jasmine, Karma 등이 있다)
리액트에서는 Jest가 특히 많이 사용되고 있다. 리액트의 테스트에 Jest가 많이 상요되는 이유는 리액트를 개발, 관리하는 기업인 페이스북에서 만들었기 때문이고 리액트를 설치하면 기본적으로 Jest도 함께 설치되기 때문이다.

그러나 이런 단순한 이유로 많은 리액트 프로젝트의 테스트에 Jest를 사용하는 것은 아니다. Jest의 장점을 살펴보면서 Jest가 많이 사용되는 이유를 설명한다.

1) 제로 설정

많은 테스트 프레임워크들이 테스트를 하기 위해 많은 설정을 해야 한다. Jest는 이런 설정 때문에 테스트를 쉽게 시작하지 못하고, 테스트에 집중하지 못하는 단점을 보완하기 위해 제로 설정을 지향하고 있다.

2) 스냅샷

테스트하다 보면 값을 일일이 확인하기 힘든 큰 자바스크립트 오브젝트가 존재할 때가 있다. Jest는 이렇게 값 확인이 어려운 큰 오브젝트를 그대로 저장한 후 추후에 값이 변경되면 에러를 표시하는 스냅샷 기능을 제공한다. 리액트에서는 이 스냅샷 기능을 통해 렌더링된 컴포넌트의 변경 사항이 있는지를 체크한다.

3) 모의 객체

Jest는 쉽게 모의 객체(Mocking)를 생성할 수 있다. 이를 통해 테스트 범위를 벗어나는 객체들을 간단하게 모의 객체로 만듦으로써 실제로 테스트해야 할 부분을 집중해서 테스트할 수 있도록 한다.

4) 테스트 코드의 분리

Jest의 테스트 코드는 완전히 분리되어 있으며, 이렇게 분리된 테스트는 동시에 실행할 수 있도록 한다. 따라서 분리된 테스트를 제공하는 Jest는 테스트 코드를 동시에 실행하여 빠른 성능을 제공한다.

5) 간단한 API

Jest는 쉽고 간단하게 테스트할 수 있는 뛰어난 API를 제공하고 있다. 또한, --coverage 옵션을 통해 코드 커버리지(Code, coverage)를 간단하게 확인할 수 있다.

Jest는 페이스북에서 개발, 관리하는 테스트 프레임워크라는 이유뿐만 아니라 이와 같은 장점들 때문에 리액트의 테스트에 많이 사용되고 있다.
(이 책에서는 Jest를 기본으로 리액트 테스트 코드를 작성한다)

프로젝트 준비

자바스크립트 테스트 프레임워크인 Jest를 사용하는 방법에 대해서 살펴보기 위해 간단한 자바스크립트 프로젝트를 생성할 예정이다.

우선 자바스크립트 프로젝트를 위해 폴더를 만든 후에 다음의 명령어를 실행하여 자바스크립트 프로젝트를 준비한다.

cd jest-test
npm init

jest-test 라는 폴더를 만들고 해당 명령어를 실행하였다. 위의 명령어를 실행하면 다음과 같이 package.json을 만들기 위한 질문들이 나오는데 아무것도 입력하지 않고 Enter 키를 눌러 진행한다. (한번만 누르면 되는줄 알았는데, 여러번 눌러야한다...)

image

이게 뭐시여... 약간 당황했지만....

image

당황하지 않고 엔터를 연타 '타타닥'.... 끝

Enter 키를 눌러 진행하였다면 해당 폴더에 package.json이라는 파일이 생성된 것을 확인할 수 있다.

이제 pacakge.json 파일과 같은 위치에 자바스크립트 코드를 추가할 index.js 파일을 생성한다. 이 파일에 테스트 대상이 될 자바스크립트 코드를 작성할 예정이다.

image

나는 갓텔리제이에서 import를 바로 해서 index.js를 생성해줬다!

Jest 설치

이제 자바스크립트 테스트 프레임워크인 Jest를 설치하고 실제로 Jest를 사용하여 테스트하기 위한 준비를 해 보자. 다음의 명령어를 실행하여 Jest를 설치한다.

(해당 명령어는 jest-test폴더에서 실행해야한다)

npm install --save-dev jest

위의 명령어를 실행하면 아래와 같이 뭔가 쭉쭉 다운로드 한다.

image

이렇게 뭔가 다운로드가 완료됐다는 것 같다

image

Jest의 설치가 완료되었다면 package.json 파일을 열어 package.json의 명령어를 모아두는 scripts 부분을 다음과 같이 수정한다.

"scripts": {
  "test": "jest --watch"
},

이제 package.json 파일에 만든 명령어를 실행하기 위해 다음의 명령어를 실행하여 jest를 실행해 보자.

npm run test

우리는 test라는 명령 스크립트에 jest라는 명령어를 --watch 옵션으로 실행하도록 하였다. jest의 watch 옵션은 파일을 감시하고 있다가 파일이 변경되면 해당 파일의 테스트 코드를 다시 실행하기 위한 옵션이다.

명령어가 실행되면 다음과 같은 화면을 볼 수 있다.

Watch Usage
  > Press a to run all tests.
  > Press f to run only failed tests.
  > Press p to filter by a filename regex pattern.
  > Press t to filter by a test name regex pattern.
  > Press q to quit watch mode.
  > Press Enter to trigger a test run.

watch 옵션 때문에 자동으로 테스트가 실행되지만, 위와 같이 터미널에 표시된 키를 눌러 해당 동작을 실행시킬 수도 있다.

이래야 되는데.... 아래와 같은 오류가 발생한다.

image

log로 추가적인 내용을 보라는 거 같으니. 해당 로그로 이동해보자. (vim 명령어로 열어봤다)

image

문제 해결을 위해 참고한 사이트

React "code ELIFECYCLE" Error 해결 (해결 X)

reactjs - TypeError : NPM 테스트를 실행할 때 environment.dispose가 함수가 아닙니다 (O)

참고 사이트에서 git 설정을 초기화하는 git init 명령어를 먼저 실행해보라는 얘기가 있었다. 어려운게 아니니 바로 git init 후 테스트를 실행해보았다.

image

비록 테스트가 깨지긴 하지만, 책에서 소개하는 내용과 동일한 창이 나온다!

사용 방법

자자바스크립트 테스트 프레임워크인 Jest의 사용 방법을 확인하기 위해 package.json 파일이 존재하는 폴더에 index.test.js 파일을 생성하자. Jest는 파일 학장자가 .test.js로 끝나는 파일들을 테스트 파일로써 인식하고 해당 파일을 실행한다. 우리는 이 테스트 파일에 index.js 파일에 관한 테스트를 작성할 것이다.

간단한 함수와 테스트 코드를 작성하여 jest의 사용법을 익혀보도록 하자. index.js 파일을 열어 다음과 같이 수정한다.

image

매우 간단한 함수이다. 이 함수는 a, b 두 개의 매개변수를 받아 더한 결과값을 반환하는 sum 함수이다. Jest를 사용하여 이 함수를 테스트하는 코드를 작성하고 Jest의 사용법을 확인해 보자. Jest의 테스트 파일인 index.test.js 파일을 열어 다음과 같이 수정한다.

image

(와 인생 첫 js 테스트 코드다!! 🎉 감동의 순간)

describe 함수는 Jest가 제공하는 함수로써 여러 테스트를 한 그룹으로 묶고 설명을 붙이기 위해 사용한다. 첫 번째 매개변수는 터미널에 표시할 설명이고, 두 번째 매개변수는 여러 테스트를 그룹으로 묶을 콜백(Callback) 함수이다.

Jest가 제공하는 it 함수는 실제 테스트가 실행되는 테스트 명세를 작성할 때 사용한다. 첫 번째 매개변수테스트명세의 설명두 번째 매개변수에는 실제로 테스트를 실행하는 테스트 코드를 작성한다.

우리는 npm run test로 Jest가 파일을 감시하고 있다가 변경되면 테스트를 다시 실행하도록 jest --watch 명령어를 실행해 두었기 때문에 위와 같이 파일을 작성하고 저장하면 Jest가 파일의 변경을 감지하고 자동으로 테스트 코드를 실행한다. 따라서 터미널에 다음과 같은 화면을 확인할 수 있다. (사진의 터미널 창을 확인하면 된다!)

여기서 우리는 변경돈 ./index.test.js 파일이 실행되었음을 알 수 있다.

index.js 파일을 열어 다음과 같이 수정하여 테스트에 실패하도록 만들어 보자.

image

테스트가 실패하면 실패한 테스트 명세(it 함수 내용) 앞에 체크가 아닌 엑스가 표시되고 실패한 내용이 자세히 나오는 것을 확인할 수 있다.

Matcher

우리는 앞의 사용법 절에서 toBe라는 Matcher를 사용하여 테스트 코드를 작성했다. Matcher는 Jest가 제공하는 함수로써 결과값을 비교하여 테스트의 성공 여부를 판단할 때 사용한다. 여기서는 Jest에서 자주 사용되는 Matcher를 소개한다.

1) toEqual

toEqual은 오브젝트(Object)를 비교할 때 사용되는 Matcher이다. toEqual의 사용법을 확인하기 위해 index.js를 다음과 같이 수정한다.

image

person 함수는 이름(name)과 나이(age) 매개변수를 전달받아 person 오브젝트를 반환하는 함수이다. 이제 index.test.js 파일을 열어 다음과 같이 수정한다.

(꿀Tip: 인텔리제이를 쓴다면, command + N 을 누른 뒤, 아래의 키를 누르면 테스트 케이스를 자동으로 생성해준다)

image

자바스크립트에서는 오브젝트의 내용이 같아도 다른 값으로 인식한다. 따라서, 단순히 person 함수를 통해 생성한 값과 toEqual에서 사용한 값을 단순히 비교(===)하면 다른 값이라고 판단하게 된다. 이와 같은 문제가 있으므로 Jest에서 오브젝트를 테스트할 때는 toEqual을 사용하여 위와 같이 테스트한다. 만약 이곳에서 toBe를 사용하면 단순히 값을 비교하므로 에러가 발생하며 테스트를 통과하지 못한다.

이렇게 파일을 수정하고 수정한 파일들을 저장하면 우리가 실행한 명령어에 의해 테스트 코드가 자동으로 실행되면 다음과 같이 성공적으로 테스트가 실행되었음을 확인할 수 있다.

2) toBeTruthy, toBeFalsy

toBeTruthy와 toBeFalsy는 참 / 거짓 값(Boolean)을 체크할 때 사용하는 Matcher이다. toBeTruthy와 toBeFalsy를 사용하여 테스트하기 위해 index.js 파일을 열어 다음과 같이 수정한다. (사진은 그만 첨부하는걸로 하자!)

const toggle = (a) => {
  return !a;
};

module.exports = {
  ...
  toggle,
};

toggle 함수는 전달받은 매개변수의 반대값을 반환하는 간단한 함수이다. 이제 Jest의 toBeTruthy, toBeFalsy를 사용하여 toggle 함수를 테스트하기 위해 index.test.js 파일을 열어 다음과 같이 수정한다.

const { ..., toggle } = require('./index');

describe('test index.js file', () => {
  ...
  it('return false', () => {
    expect(toggle(true)).toBeFalsy();
    expect(toggle(false)).toBeTruthy();
  });
});

우리가 만든 toggle 함수에 참값(true)을 매개변수로 전달하였고 우리가 결과값으로 예상하는 값인 거짓값을 확인하기 위해 toBeFalsy를 사용한 것을 확인할 수 있다. 또한, not과 toBeTruthy를 사용하여 반환된 값이 참이 아님으르 확인했다.

이렇게 파일을 수정하고, 수정한 파일들을 저장하면 Jest가 수정된 테스트 파일을 실행하고 명령 프롬프트에 다음과 같이 테스트가 성공했음을 표시한다.

3) toContain

Jest에서는 배열(Array)에 특정값이 포함되어 있는지를 확인할 때 toContain을 사용한다. Jest의 toContain을 사용하여 배열값을 확인하기 위해 index.js 파일을 열어 다음과 같이 수정한다.

...
const range = (start, end) => {
  let result = [];
  for (let i = start; i <= end; i++) {
    result.push(i);
  }
  return result;
};

module.exports = {
  ...
  range,
}

range 함수는 배열에 시작과 끝값을 전달받아서 배열을 만들어 반환하는 함수이다. Jest의 toContain을 사용하여 이 함수를 테스트하기 위해 index.js 파일을 열어 다음과 같이 수정한다.

const { ..., range } = require('./index');

describe('test index.js file', () => {
  ...
  it('has 2', () => {
    expect(range(1, 3).toContain(2));
  });
});

toContain을 사용해서 1로 시작하여 3으로 끝나는 배열에 2가 포함되어 있는지 확인하는 테스트 코드를 작성하였다.

이렇게 파일을 수정하고, 수정한 파일을 저장하면 다음과 같이 모든 테스트가 성공적으로 실행됨을 확인할 수 있다.

4) 기타

이 밖에도 Jest에는 많은 Matcher가 존재한다. 다음 링크에서 Jest에서 사용 가능한 다른 Matcher를 확인할 수 있다. 앞으로 Jest를 사용하여 계속 테스트할 예정이므로 아래 링크를 한 번 방문하여 어떤 Matcher가 존재하는지 확인해 보길 권장한다.

코드 커버리지

테스트에서 코드 커버리지(Code coverage)란 테스트 대상이 되는 소스 코드 중 테스트 코드를 통해 검증된 코드의 비율을 의미하며, 테스트 수행 결과를 정량적으로 나타내는 수치이다.

코드 커버리지를 통해 테스트 코드가 얼마나 많은 소스 코드를 테스트하고 있는지 나타내는 중요한 지표이다. 이 지표를 통해 우리는 테스트 코드가 작성되지 않은 코드를 확인할 수 있다.

Jest에서는 다음의 명령어를 통해 간단하게 코드 커버리지를 확인할 수 있다.
npx test --coverage

우리가 앞서 테스트 코드를 작성한 폴더에서 위 명령어를 실행해 보면 다음과 같은 결과를 얻을 수 있다.

image

이렇게 Jest의 코드 커버리지를 통해 우리가 실제로 사용하는 코드에 대한 테스트 코드가 얼마나 많이 작성되어 있는지, 작성되어 있지 않은 부분은 어디인지 한눈에 확인할 수 있다.

728x90
프로그래밍 공부/React

스무디 한 잔 마시며 끝내는 리액트 + TDD - 1장 리액트란?

728x90

먼저 갑자기 리액트??? 하고 의문을 품을 여러 지인들에게 상황을 간략히 설명하자면

네 제가 그 5명 중 하나입니다.

뭐 엄청나게 상세하게 정리하려는 것은 아니고 각 장별로 기록하고 싶은 부분을 기록해나가면서 넘어가려고한다.
(나중에 어드민 페이지 정도는 리액트로 만들어줘야 "오 프론트도 쫌 할줄아는데?" 소리한번 들어보는 것을 목표로...)

자바스크립트의 역사

리액트는 자바스크립트(JavaScript, 이하 js) 언어를 기반으로 동작하는 라이브러리이다. 따라서 리액트를 이해하기 위해서는 기본적으로 js에 대한 이해가 필요하다. js의 역사를 간략히 훑으면서 리액트가 탄생한 이유를 이해해보자!

(js의 탄생 배경은 한번쯤은 들어봤을 수도 있다. 이번 기회에 해당 영상을 통해서 알아보는 것도 좋을 것 같다)

js는 '넷스케이프 커뮤니케이션즈 '(Netscape Communications)라는 회사에서 만들었다. 넷스케이프 커뮤니케이션즈는 정적인 HTML을 동적으로 표현하기 위한 경량 프로그래밍 언어를 도입하기로 하고, 새로운 프로그래밍 언어 개발에 착수하게 되는데, 이때 탄생한 것이 js이다.

더보기

이 당시에 넷스케이프는 시장 점유율이 약 90%에 달했다고 한다.

js의 기반이 되는 언어인 모카(Mocha)는 Brendan Erich가 10일만에 만들었으며, 이는 1993년 3월 넷스케이프 커뮤니케이션즈의 웹 브라우저인 넷스케이프 네비게이터2(Netscape Navigator 2)에 처음 탑재된다. 그리고 그해 9월 라이브스크립트(LiveScript)로 이름이 변경되고 최종적으로 12월에 자바스크립트(JavaScript)로 명명된다.

js는 많은 언어로부터 영감을 받아 만들어졌다. 변수 스코프(Scopre)와 클로져(Closure) 등의 규칙은 리스프(Lisp) 언어에서 가져왔으며, 프로토타입 상속은 스몰토크(Smalltalk)에서 파생된 언어인 셀프(Self) 프로그래밍 언어에서 영감을 얻었다. 넷스케이프 커뮤니케이션즈는 그 당시 MS와 경쟁하기 위해 썬 마이크로시스템즈(Sun Microsystems)와 협업하고 있었는데 자신들의 새로운 언어가 그 당시 인기 있었던 썬 마이크로시스템즈의 자바 문법과 유사하길 희망했다.

js의 이름의 유례는 다양한데 그 중 유력한 설은 넷스케이프 커뮤니케이션즈가 마케팅을 위해 당시 인기있었던 '자바'의 이름을 차용하여 '자바스크립트'라는 이름을 지었다는 것이다. 조금 더 자세히 설명하면, 자바스크립트는 자바에서 파생되었다는 인상을 주어 많은 프로그래머가 js를 사용하도록 유도하기 위함이었다는 것이다.

넷스케이프 네비게이션 브라우저가 흥행하자 js 언어도 크게 유행하게 된다. 이를 경쟁사인 MS가 가만히 있을 수 없지 않겠는가? MS는 js와 호환을 할 수 있는 J스크립트(Jscript)라는 언어를 개발하여 내놓는다. 이 J스크립트는 인터넷 익스플로러 3.0에 처음 도입되면서 js와 경쟁 구도를 가지게 된다.

이렇게 J스크립트를 사용하는 인터넷 익스플로러와 js를 사용하는 넷스케이프 네비게이션이 브라우저의 시장을 공유하기 시작하면서 크로스 브라우징(Cross Browsing) 이슈가 발생하기 시작했다. 넷스케이프 커뮤니케이션즈는 js에 새로운 스펙을 추가하면서 자신들의 브라우저에 새로운 기능을 추가하였고, MS는 J스크립트에 새로운 스펙을 추가하면서 새로운 기능을 추가하기 시작했다. js와 호환이 되던 J스크립트는 독자적인 스펙들이 더해지면서 점점 호환되지 않게 되었다. 호환되지 않은 두 브라우저가 브라우저 시장을 공유하면서, 개발자들은 넷스케이프 네비게이션과 인터넷 익스플로러에서 동작하는 웹 페이지를 개발하는 데 큰 어려움을 겪기 시작한다.

넷스케이프 커뮤니케이션즈는 이런 크로스 브라우징 문제를 일으키는 js의 파편화를 방지하고 모든 브라우저에서 동일하게 동작하는 표준화된 js에 대한 필요성을 느꼈으며, 컴퓨터 시스템의 표준을 관리하는 비영리 표준화 기군인 ECMA 인터내셔널에 자바스크립트 표준화를 요청하게 된다.

ECMA 인터내셔널에서는 1997년 1월, ECMA-262라 불리는 표준화 된 자바스크립트 초판(ECMAScript 1)의 명세서를 완성하지만, 자바스크립트의 상표권 문제로 인해 ECMAScript로 명명하게 된다. 우리가 흔히 알고있는 ECMAScript가 이렇게 탄생했다.

하지만 비영리 표준화 기구의 표준화된 명세서는 큰 힘이 없었으며, 크로스 브라우징 이슈는 여전히 존재하였다. 또한 js와 J스크립트를 사용한 브라우저의 돔(DOM, Document Object Model) 조작은 너무나 복잡하고 불편했다.

이 문제를 해결한 것이 jQuery이다. jQuery는 당시 가지고 있던 크로스 브라우징 이슈와 더불어 자바스크립트보다 배우기 쉽고 직관적인 API(Application Programming Interface)를 제공함으로써 선풍적인 인기를 끌게 된다. 지금까지도 jQuery는 돔을 다루는 방식에서 가장 쉽고 효율적인 방식으로 인정받고 있으며, 웹 브라우저에서 사실상 표준으로 오랜 기간 사랑받았다.

웹 서비스에서 jQuery가 크게 사랑을 받던 중에 아이폰, 안드로이드가 등장하면서 애플리케이션(Application)이라는 개념이 대중적으로 크게 확장되었다. 또한, 사용자들이 사용하는 단말기(PC, 스마트폰 등)의 성능이 크게 향상되었다. 이 때문에 그동안 웹 페이지라는 개념이었던 웹 서비스에도 웹 애플리케이션(Web Application)이라는 개념과 이에 대응하는 서비스들이 쏟아져 나오기 시작했다.

2010년, 구글은 이런 웹 애플리케이션 트렌드에 대응하고자 AngularJS라는 웹 애플리케이션 프레임워크를 출시하게 된다. AngularJS는 웹 서비스에 싱글 페이지 애플리케이션(SPA, Single Page Application)이라는 새로운 시대를 열게 된다 AngularJS는 MV(Model-View-Whatever), 양방향 데이터 바인딩(Two-way Data Binding) 등 웹 애플리케이션에 새로운 개념들을 많이 도입하였다. 하지만, 그 당시 AngularJS는 jQuery를 기반으로 하고 있었으며, 싱글 페이지 애플리케이션을 모두 다루는 프레임워크로써 너무 많은 변화와 새로운 개념으로 많은 개발자가 쉽게 접근하기 어려운 러닝 커브(Learning curve)를 안겨주었다.

2011년, 페이스북 개발자였던 Jordan Walke가 PHP용 HTML 컴포넌트 프레임워크였던 XHP에 영감을 받아 리액트를 개발하게 된다. 이렇게 개발된 리액트는 2011년 페이스북의 뉴스피드에 처음 적용하게 되고 이후 2012년 인스타그램닷컴에 적용된다. 페이스북은 2013년 5월 JSConf US에서 리액트를 오픈 소스로 발표하면서 리액트의 역사가 시작된다.

리액트는 자바스크립트 프레임워크였던 앵귤러(Angular)와 다르게 UI 자바스크립트 라이브러리로 출시된다. 싱글 페이지 애플리케이션의 거의 모든 부분을 담당했던 앵귤러와 다르게 리액트는 UI(User Interface)에 집중한 라이브러리로 출시하게 된다.

거의 새로운 언어에 가깝던 앵귤러와 달리 리액트는 js에 HTML을 포함하는 JSX(JavaScript XML)라는 간단한 문법과 양방향 데이터 바인딩이 가지는 문제점을 보완하고자 단방향 데이터 바인딩(One-way Data Binding)을 채택하였다. 그리고 가상 돔(Virtual DOM)이라는 새로운 개념으로 큰 인기를 끌게 된다.

리액트는 싱글 페이지 애플리케이션의 UI를 만드는 js 라이브러리이다. 그러므로 싱글 페이지 애플리케이션 프레임워크였던 앵귤러보다 러닝 커브가 낮다. 하지만 프레임워크가 아닌 라이브러리이므로 부족한 부분들이 존재하고, 이런 부분들을 채우기 위해서는 다른 라이브러리들과 함께 사용해야 한다. (ex. 리액트는 페이지 전환의 기능을 지원하지 않으므로 react-router 같은 다른 라이브러리를 사용해야 함)

리액트의 특징

리액트는 가상 돔(Virtual DOM)과 같은 새로운 개념과 다른 프레임워크와 달리 단방향 데이터 바인딩을 사용하는 등 리액트만의 특징을 가진다.

가상 돔

리액트는 가상 돔이라는 개념으로 웹 퍼포먼스 향상에 새로운 접근 방식을 제안하였고, 이를 통해 웹 애플리케이션의 성능을 극대화하였다. 가상 돔이 웹 퍼포먼스의 성능을 어떻게 향상시키는지 이해하기 위해서는 우선 브라우저에서 HTML, CSS가 렌더링(Rendering) 되는 부분을 이해할 필요가 있다.

image

브라우저가 네트워크를 통해 HTML을 전달받으면 브라우저의 렌더 엔진은 HTML을 파싱하여 돔 노드(DOM Node)로 이뤄진 트리를 만든다. 또한, CSS파일과 각 엘리먼트의 인라인 스타일을 파싱하여 스타일 정보를 가진 새로운 스타일 렌더 트리를 만든다.

이렇게 렌더 트리가 생성되면 브라우저는 Attachment라는 과정을 통해 스타일 정보를 계산하게 된다. 렌더 트리는 모든 노드는 attach라는 메서드를 가지고 있는데, Attachment 과정에서 이 메서드가 호출하게 되며 해당 메서드는 스타일 정보를 계산하고 결과값을 객체 형태로 반환하게 된다. 이 과정은 동기적으로 작동하며 만약 새로운 렌더 트리에 새로운 노드가 추가되면 해당 노드의 attach 메서드가 실행된다.

image

렌더 트리는 Attachment 과정을 거친 후 레이아웃이라는 과정을 거치게 된다. 레이아웃 과정에서는 브라우저가 렌더 트리와 각 노드에 좌표를 부여하고 정확히 어디에 어떻게 표시되는지 결정하게 된다.

마지막으로, 브라우저는 각 노드에 paint() 메서드를 호출하여 렌더링된 욧들에 색상을 입히는 Painting이라는 과정을 거친 후 최조적으로 화면을 표시하게 된다.

이렇게 화면에 표시된 후 자바스크립트를 사용하여 돔을 조작하게 되면 각 노드에 좌표를 계산하고 부여하는 레이아웃 과정이 다시 수행되며, 그 이후 색상을 입히는 페인팅 과정이 다시 수행되게 된다. 여기서 레이아웃 과정이 다시 수행되는 것을 리플로우(Reflow)라 하며 페인팅 과정을 다시 수행하는 것을 리페인트(Repaint)라고 한다. 이 리플로우와 리페인트는 돔의 각 노드에 관해 많은 연산을 수행하므로 이 과정을 수행하게 되면 웹 서비스의 성능 이슈가 발생한다.

물론 정적인 웹 사이트나 화면을 구성하는 돔에 변경이 적은 웹 사이트면 크게 문제가 되지 않지만, 싱글 페이지 애플리케이션처럼 돔 변경이 동시다발적으로 빈번히 발생하는 사이트인 경우에는 리플로우와 리페인트를 많이 수행되면 사이트의 성능 이슈가 발생하게 된다.

리액트는 리플로우와 리페인트의 문제를 해결하기 위해 화면에 표시되는 돔과 동일한 돔을 메모리상에 만들고, 돔 조작이 발생하면 메모리상에 생성한 가상 돔에서 모든 연산을 한 후 실제 돔을 갱신하여 리플로우와 리펜인트의 연산을 최소화하였다.

즉, 리액트는 가상돔을 통해 리플로우와 리페인트를 최소화하여 성능 최적화를 하였다.

단방향 데이터 바인딩

SPA의 대표적인 프레임워크 프레임워크인 Angular, Vue는 양방향 데이터 바인딩을 사용한다.

image

) (출처: 이 책의 저자분의 블로그)

양방향 데이터 바인딩을 사용자 UI의 데이터 변경을 감시하는 Watcher와 js 안에서 변경되는 데이터를 감시하는 Watcher를 통해 UI와 프로그램 안에 데이터를 자동으로 동기화해주는 시스템이다. 이를 통해 프로그래머는 js 내에 데이터 변경과 사용자 UI에서 데이터 변경 및 동기화를 크게 신경쓰지 않고 프로그램을 작성할 수 있다.

양방향 데이터 바인딩은 자동으로 데이터를 동기화해주는 장점이 있는 반면에 데이터 동기화를 위해 데이터 하나에 두 개의 Watcher가 사용되기 때문에 오버스펙일 경우가 발생할 수 있다. 또한, 수많은 Watcher에 의해 반대로 성능 저하가 발생할 수 있다. 앵귤러는 이런 오버 스펙과 많은 Watcher에 의한 성능 저하를 방지하기 위해 단방향 데이터 바인딩을 지원하고 있다.

리액트양방향 데이터 바인딩이 가지는 문제점과 복잡성을 피하고자 단방향 데이터 바인딩을 채택하고 있다.

image

단방향 데이터 바인딩은 단 하나의 Watcher가 js의 데이터 갱신을 감지하여 사용자의 UI 데이터를 갱신한다. 사용자가 UI를 통해 데이터를 갱신할 때는 양방향 데이터 바인딩과 다르게 Watcher가 아닌 Event를 통해 데이터를 갱신하게 된다. 이처럼 하나의 Watcher를 사용하기 때문에 양방향 데이터 바인딩에서 발생하는 문제들을 해결할 수 있으며, 더 확실하게 데이터를 추적할 수 있다.

또한, 리액트는 이런 단방향 데이터 바인딩과 더불어 Flux라는 개념을 도입하여 데이터의 흐름이 한쪽으로만 진행되도록 하고 있다.

JSX

리액트에서는 JSX라는 독특한 문법을 가진다. 이 문법 때문에 많은 js 개발자들이 큰 혼란을 겪지만, 다른 프로그래밍 언어를 조금이라도 다뤄 봤다면 쉽게 이해할 수 있다.

JSX는 js와 HTML을 동시에 사용하며, HTML에 js 변수들을 바로 사용할 수 있는 일종의 템플릿 언어(Template language)이다.

const App = () => {
  const hello = 'Hello world!';
  return <div>{hello}</div>;
};

리액트의 JSX를 사용하여 화면에 "Hello world!"를 출력하는 코드이다. js 변수인 hello를 HTML, 태그인 div 안에 {hello}로 사용하여 출력하고 있음을 확인할 수 있다. js라는 틀 안에서 보면 굉장히 이상한 코드이지만, 다른 언어들의 템플릿 언어를 생각하면 조금 이해가 될 것이다.

예를 들아, jsp에서는 아래와 같은 코드가 쓰인다. (jstl이라고 부른다)
<div><%= hello %></div>

이미 우리는 많은 언어에서 템플릿 언어를 사용하고 있다. 이처럼 JSX도 js 일종의 템플릿 문법이라고 기억하면 조금 더 쉽게 이해할 수 있을 것이다.

선언형 프로그래밍

프로그래밍은 크게 명령형 프로그래밍선언형 프로그래밍으로 구별할 수 있다. 명령형 프로그래밍은 어떻게(How)에 집중하고 선언형은 무엇(What)에 집중하여 프로그래밍한다.

// 명령형 프로그래밍 
const double = (arr) => {
  let results = [];
  for (let i = 0; i < arr.length; i++) {
    results.push(arra[i] * 2);
  }
  return results;
}

// 선언형 프로그래밍 
const double = (arr) => {
  return arr.map((elem) => elem * 2);
}

두 함수는 같은 동작을 하는 자바스크립트 함수이다. 첫 번째는 명령형 프로그래밍으로 작성된 함수이며, 두 번째 함수는 선언형 프로그래밍으로 작성된 함수이다. 이렇게 명령형 프로그래밍은 과정을 중심으로 프로그래밍을 하게 된다.

반면에, 선언형 프로그래밍은 map 함수를 사용하여 주어진 배열 값을 두 배로 만들어 반환하였다. map이 어떻게 동작하는지는 크게 신경 쓰지 않고 결과인 배열 값을 두 배로 만들기에 집중하여 프로그래밍하게 된다.

두 프로그래밍의 결과값은 같지만 명령 프로그래밍은 그 값을 얻기 위해 어떻게 하는지에 집중하고 있는 것을 알 수 있다. 그리고 선언형 프로그래밍은 js의 기본 제공 함수인 map을 사용하여 결과값이 무엇인지에 집중하고 있다. 이처럼, 라이브러리나 프레임워크 등으로 비선언형 부분을 캡슐화함으로써 명령형 프로그래밍 언어로 선언형 프로그래밍을 할 수 있다.

리액트에서는 특히 JSX를 사용함으로써 더욱 명확하게 선언형 프로그래밍을 활용하고 있다.

<script>
  var arr = [1, 2, 3, 4, 5]
  var elem = document.querySelector("#list");

  for (var i = 0; i < arr.length; i++) {
    var child = document.createElement("li");
    child.innerHTML = arr[i];
    elem.appendChild(child);
  }
}
</script>

앞의 예제는 js를 사용하여 HTML에 새로운 리스트를 추가하는 코드이다. 예제를 자세히 살펴보면 명령형 프로그래밍으로써 새로운 리스트를 표시할 li 태그를 작성한 후 js의 querySelector를 사용하여 표시할 위치를 가져오고, For문을 사용하여 리스트에 아이템을 하나씩 추가하고 있다.

리액트의 JSX 코드를 사용하면 아래와 같이 선언형으로 변환할 수 있다.

const arr = [1, 2, 3, 4, 5];
return {
  <ul>
    {arr.map((elem) => (
      <li>{elem}</li>
    ))}
  </ul>
};

리액트는 JSX라는 문법을 사용하기 때문에 위와 같이 HTML 안에서 map 함수를 사용하여 리스트에 아이템을 추가할 수 있다. 이처럼 리액트는 JSX를 활용하여 HTML을 조작할 때에도 선언형 프로그래밍을 할 수 있다.

이런 선언형 프로그래밍은 코드를 예측할 수 있게 하고 디버깅을 쉽게 할 수 있도록 도와주므로 전체적인 코드 퀄리티의 상승과 코드의 이해를 도와주는 효과를 얻을 수 있다.

컴포넌트 기반

리액트로 웹 UI를 개발할 때는 컴포넌트라고 불리는 작고 고립된 코드들을 이용하여 구현하게 된다.

const Title = () => {
  return <h1>Hello world</h1>
};

const Button = () => {
  return <button>This is a Button</button>;
};

const App = () => {
  return (
    <div>
      <Title />
      <Button />
    </div>
  );
};

앞의 예제처럼 리액트에서는 Title과 Button 컴포넌트를 만든 후에, App 컴포넌트에서는 이미 만들어진 UI 컴포넌트를 활용하여 페이지를 제작한다. 물론 Title 컴포넌트와 Button 컴포넌트는 다른 컴포넌트에서도 반복적으로 사용할 수 있다. 이처럼 리액트는 JSX를 활용하여 UI를 제작할 때 기본적으로 컴포넌트 기반 프로그래밍을 하게 된다.

앞으로 우리는 리액트의 JSX를 활용하여 필요한 컴포넌트를 제작하고 제작한 컴포넌트를 조합하여 화면을 구성하는 컴포넌트 기반 프로그래밍을 경험하게 될 것이다.

image

2장은 리액트 개발 환경을 만드는 것인데, 이는 이전에 리액트를 써본적이 있어서, 어느정도 다 환경이 다 형성돼있는 관계로 과감히 스킵하겠다. (책으로는 쭉 훑어보겠지만 기록은 안남기겠다 😉)

728x90