#7 React TDD를 이용한 간단한 앱 생성 및 배포

김민성·2023년 5월 21일
0
post-thumbnail

TDD를 이용해서 만들 앱 소개


+, -, 그리고 on/off 버튼이 있다. 거기서 +를 누르면 숫자가 올라가고 -를 누르면 내려간다. 그리고 on/off 버튼(푸른색)을 누르면 +,- 버튼이 작동을 안하고 색깔이 변하는 간단한 앱을 만들어 보자. 그리고 궁극적으로 이 앱을 Testing 해보자.

폴더를 생성하고, 터미널에 npx react-create-app을 하여 React app을 만들어주자.

앱 만들기 시작

테스트 주도 개발을 할 것이기 때문에 먼저 테스트 코드 부터 작성해보자.

Counter 생성

해야 할 일은?

Counter는 0부터 시작한다.

테스트 작성

<App.test.js>

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

test("the counters starts at 0", () => {
  render(<App />);
  const counterElement = screen.getByTestId("counter");
  expect(counterElement).toBe(0);
});

테스트 실패

테스트에 대응하는 실제 코드 작성

<App.js>

import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  return (
    <div className='App'>
      <header className='App-header'>
        <h3 data-testid='counter'>{count}</h3>
      </header>
    </div>
  );
}

export default App;

테스트 실행



Expected가 0인데, Received에 완료멘트가 들어와서 fail.

테스트 코드 수정

App.test.js를 수정해줘야 한다. toBe가 아니라, toHaveTextContent이어야 한다.

<App.test.js>

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

test("the counters starts at 0", () => {
  render(<App />);
  const counterElement = screen.getByTestId("counter");
  expect(counterElement).toHaveTextContent(0); //toBe가 아니라..
});

테스트 실행


위와 같이 test가 잘 진행되어 passed된 것을 확인할 수 있다.


플러스, 마이너스 버튼 생성

카운터를 올리고 내릴 수 있는 버튼을 생성해보자.

버튼 생성

해야 할 일은?

+, - 버튼 두개를 생성하기

테스트 작성

<App.test.js>

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

test("the counters starts at 0", () => {
  render(<App />);
  const counterElement = screen.getByTestId("counter");
  expect(counterElement).toHaveTextContent(0); //toBe가 아니라..
});

test("minus button has correct text", () => {
  render(<App />);

  const buttonElement = screen.getByTestId("minus-button");
  expect(buttonElement).toHaveTextContent("-");
});

test("plus button has correct text", () => {
  render(<App />);
  const buttonElement = screen.getByTestId("plus-button");
  expect(buttonElement).toHaveTextContent("-");
});

테스트에 대응하는 실제 코드 작성

<App.js>

import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  return (
    <div className='App'>
      <header className='App-header'>
        <h3 data-testid='counter'>{count}</h3>
        <button data-testid='minus-button'>-</button>
        <button data-testid='plus-button'>+</button>
      </header>
    </div>
  );
}

export default App;

테스트 실행


위와 같이 3개 모두 pass를 받은 걸 확인할 수 있다.


플러스, 마이너스 버튼 기능 넣기(fire event)

카운터를 올리고 내릴 수 있는 버튼의 기능을 넣어서 카운터를 변화시켜주자.

FireEvent API

유저가 발생시키는 액션(이벤트)에 대한 테스트를 해야 하는 경우 사용한다.

<참고>

링크텍스트

버튼 생성

해야 할 일은?

+버튼을 누르면 카운터가 1로 변하게 된다.

테스트 작성

<App.test.js>

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

test("the counters starts at 0", () => {
  render(<App />);
  const counterElement = screen.getByTestId("counter");
  expect(counterElement).toHaveTextContent(0); //toBe가 아니라..
});

test("minus button has correct text", () => {
  render(<App />);

  const buttonElement = screen.getByTestId("minus-button");
  expect(buttonElement).toHaveTextContent("-");
});

test("plus button has correct text", () => {
  render(<App />);
  const buttonElement = screen.getByTestId("plus-button");
  expect(buttonElement).toHaveTextContent("+");
});

이전에 작성한 코드에서 아래 코드를 추가해준다.

test("When the + button is pressed, the counter changes to 1", () => {
  render(<App />); // App 컴포넌트를 렌더링합니다.
  const buttonElement = screen.getByTestId("plus-button");
  // "plus-button"이라는 테스트 ID를 가진 요소를 화면에서 찾아서 buttonElement 변수에 할당합니다.
  fireEvent.click(buttonElement);
  // buttonElement에 클릭 이벤트를 발생시킵니다. 이는 실제 사용자가 "+" 버튼을 클릭하는 것을 시뮬레이션합니다.
  const counterElement = screen.getByTestId("counter");
  // "counter"라는 테스트 ID를 가진 요소를 화면에서 찾아서 counterElement 변수에 할당합니다.
  expect(counterElement).toHaveTextContent(1);
  // counterElement의 텍스트 내용이 1인지 확인합니다. "+" 버튼을 클릭했으므로, 카운터 값이 0에서 1로 증가했어야 합니다.
});

테스트에 대응하는 실제 코드 작성

minus-button일 떄와 plus-button일 때의 onClick 을 추가해준다.
<App.js>

import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  return (
    <div className='App'>
      <header className='App-header'>
        <h3 data-testid='counter'>{count}</h3>
        <button
          data-testid='minus-button'
          onClick={() => setCount((prev) => prev - 1)} //이전에서 -1
        >
          -
        </button>
        <button
          data-testid='plus-button'
          onClick={() => setCount((prev) => prev + 1)} //이전에서 +1
        >
          +
        </button>
      </header>
    </div>
  );
}

export default App;

테스트 실행


on/off 버튼 만들기(toHaveStyle)

on/off 버튼을 만들어보자.

on/off 버튼 생성

해야 할 일은?

on/off 버튼을 만든다. 이 버튼은 파란색으로 스타일을 주도록 하자.

테스트 작성

App.test.js에 다음 test 코드를 추가해준다.

// 테스트를 정의합니다. "on/off 버튼이 파란색인지"를 테스트하는 것입니다.
test("on/off button has blue color", () => {
  // 우리의 App 컴포넌트를 렌더링합니다. 이것은 테스트를 시작하기 위해 필요합니다.
  render(<App />);
  // "on/off-button" 이라는 데이터-testid 속성을 가진 요소를 선택합니다. 이 요소는 on/off 버튼을 가리킵니다.
  const buttonElement = screen.getByTestId("on/off-button");
  // 이 버튼의 배경색이 파란색인지 확인합니다.
  // 이 행은 "버튼 요소가 파란색 배경을 가지고 있는가?"를 테스트하는 expect 문입니다.
  expect(buttonElement).toHaveStyle({ backgroundColor: "blue" });
});

테스트에 대응하는 실제 코드 작성

App.js에서 plus-button 아래에 on/off 버튼에 대한 코드를 추가해준다.

<div>
	{/* 버튼 요소를 생성합니다. */}
	<button
		// 이 버튼의 배경색을 파란색으로 설정합니다.
		style={{ backgroundColor: "blue" }}
		// 데이터-testid 속성을 'on/off-button'으로 설정합니다.
        // 이 속성은 테스트할 때 이 버튼을 식별하는 데 사용됩니다.
        data-testid='on/off-button'
        // 이 버튼의 텍스트를 'on/off'로 설정합니다. on/off
	>on/off</button>
</div>

테스트 실행


on/off 버튼 클릭 시 버튼 disabled

on/off 버튼을 클릭 시 -, + 버튼을 disabled 시켜보자.

on/off 버튼 생성

해야 할 일은?

on/off 버튼을 클릭할 때 -,+ 버튼을 못누르게 막기

테스트 작성

fireEvent를 사용해서 on/off 버튼을 클릭을 할 것이다. 그러면 그 상태 동안, -, + 버튼은 disabled가 되어있어야 한다.
아래 test 코드를 App.test.js에 추가해준다.

// 테스트 케이스를 정의합니다. 이 테스트는 on/off 버튼이 클릭되었을 때 -,+ 버튼이 눌리지 않도록 막는지를 확인합니다.
test("Prevent the -,+ button from being pressed when the on/off button is clicked", () => {
  // 우선, App 컴포넌트를 렌더링합니다.
  render(<App />);
  // on/off 버튼 요소를 화면에서 찾아 변수에 저장합니다. 이 요소는 'on/off-button'이라는 testid를 가지고 있습니다.
  const onOffButtonElement = screen.getByTestId("on/off-button");
  // on/off 버튼 요소에 클릭 이벤트를 발생시킵니다.
  fireEvent.click(onOffButtonElement);
  // + 버튼 요소를 화면에서 찾아 변수에 저장합니다. 이 요소는 'plus-button'이라는 testid를 가지고 있습니다.
  const plusButtonElement = screen.getByTestId("plus-button");
  // 이제 + 버튼이 비활성화되었는지 확인합니다. on/off 버튼이 클릭되었으므로, + 버튼은 비활성화되어야 합니다.
  expect(plusButtonElement).toBeDisabled();
});

테스트에 대응하는 실제 코드 작성

disabled state를 만들어두고, false로 지정해두고, onClick함수가 호출되면, true로 변경시켜주게끔, 그리고 plus-button과 minus-button이 disabled가 되도록 App.js에 실제 코드를 작성해야한다.

<App.js>

import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0); 

  // "disabled"라는 상태를 만들고, 초기값을 false로 설정합니다. 
  // 이 상태는 "-"와 "+" 버튼이 비활성화되어 있는지(즉, 클릭할 수 없는 상태인지) 나타냅니다.
  const [disabled, setDisabled] = useState(false);

  return (
    <div className='App'>
      <header className='App-header'>
        <h3 data-testid='counter'>{count}</h3>
        <button
          data-testid='minus-button'
          onClick={() => setCount((prev) => prev - 1)}
          
          // disabled 속성을 설정하여 이 버튼이 비활성화 될 수 있도록 합니다. 
          // "disabled" 상태가 true이면 버튼은 비활성화되고, false이면 활성화됩니다.
          disabled={disabled}
        >
          -
        </button>
        <button
          data-testid='plus-button'
          onClick={() => setCount((prev) => prev + 1)}
          
          // "+" 버튼에도 disabled 속성을 설정하여 이 버튼이 비활성화 될 수 있도록 합니다. 
          disabled={disabled}
        >
          +
        </button>
        <div>
          <button
            style={{ backgroundColor: "blue" }}
            data-testid='on/off-button'
            
            // 이 버튼을 클릭하면 "setDisabled" 함수를 호출하여 "disabled" 상태를 현재와 반대로 변경합니다. 
            // 즉, 이 버튼을 클릭하면 "-"와 "+" 버튼의 활성화/비활성화 상태가 전환됩니다.
            onClick={() => setDisabled((prev) => !prev)}
          >
            on/off
          </button>
        </div>
      </header>
    </div>
  );
}

export default App;

테스트 실행


Github Action을 이용한 AWS S3로 앱 자동 배포하기

앱을 배포하는 방법


AWS EC2에 Front-End와 Back-End를 함께 배포하는 방법이 있고,

AWS EC2에 Back-End만 배포하고, Front-End는 AWS S3 Github Page에 배포하는 방법이 있다.

그리고, AWS EC2에 도커 컨테이너를 실행을 하여 안에다가 Front-End와 Back-End를 함께 배포하는 방법도 있고,

Heroku(히로쿠)를 통해서 쉽게 배포하는 방법도 있다.

지금까지 AWS 이야기가 많지만, Azure, Google cloud, Digital Ocean 등 여러가지 클라우드 서비스가 있다.

나는 지금 Front-End를 중심으로 공부를 하고 있기에,

AWS S3를 이용할 것이다. 하지만 그냥 S3를 사용하는 것 보단, CI환경을 구축하여 Github action을 이용해서 앱을 배포해볼 것이다.



저장소 생성

저장소 연결

workflow 생성

이 코드는 Node.js 프로젝트의 CI (Continuous Integration) 워크플로우를 정의한다. 해당 워크플로우는 GitHub Actions를 사용하여 주어진 Node.js 버전에서 종속성을 설치하고 캐시/복원한 다음 소스 코드를 빌드하고 테스트한다. 각 코드가 무엇을 의미하는지 알아보자.

name: Node.js CI

이 워크플로우의 이름을 "Node.js CI"로 설정한다.

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

이 워크플로우는 main 브랜치에 push 또는 pull request 이벤트가 발생했을 때 실행한다.

jobs:
  build:

하나의 작업(build)을 정의한다.

    runs-on: ubuntu-latest

이 작업은 최신 버전의 Ubuntu에서 실행된다.

    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x]

이 작업은 세 가지 다른 Node.js 버전 (14.x, 16.x, 18.x)에서 실행되는 매트릭스 전략을 사용합니다. 따라서 각 버전에 대해 작업이 병렬로 실행된다.

    steps:
    - uses: actions/checkout@v3

GitHub 저장소를 체크아웃하여 작업 디렉토리로 가져온다.

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

해당 작업에서 사용할 Node.js 버전을 설정합니다. actions/setup-node 액션을 사용하여 Node.js 환경을 설정하고, npm 캐시를 사용하여 종속성을 캐시하고 복원한다.

    - run: npm i

npm i 명령을 실행하여 종속성을 설치한다.

    - run: npm run build --if-present

npm run build 명령을 실행하여 소스 코드를 빌드합니다. --if-present 플래그는 해당 스크립트가 package.json 파일에 정의되어 있는 경우에만 실행되도록 한다.
npm test 명령을 실행하여 테스트를 실행한다.

  - run: npm test

이 워크플로우는 세 가지 다른 Node.js 버전에서 종속성 설치, 캐시/복원, 빌드, 테스트를 반복적으로 수행하여 애플리케이션이 여러 버전의 Node.js에서 올바르게 작동하는지 확인한다.

앱 배포를 위한 AWS S3 버킷 생성하기

S3 서비스에 가기

AWS 사이트에 들어가 로그인을 한 후 아래 사진과 같이 검색을 한다.

버킷 만들기

AWS 리전(Region)

AWS 인프라를 지리적으로 나누어 배포한 것을 의미한다. 사용자와 리전이 가까울수록 네트워크 지연을 최소화할 수 있다. AWS도 서버를 설치한 곳이 많을텐데, 내가 지금 있는 곳과 가까운 곳의 서버를 연결해야한다.

생성한 버킷을 웹사이트 호스팅을 위해서 사용할 수 있게 설정

속성 탭으로 이동 -> 정적 웹 사이트 호스팅

정적 웹사이트 호스팅==> 활성화 호스팅 유형 ==> 정적 웹 사이트 호스팅 인덱스 문서 ===> index.html => 변경사항 저장

AWS S3 버킷 설정 및 애플리케이션 배포하기

버킷 - 속성 탭에 들어가서 가장 아래로 스크롤을 내리면 다음과 같이 웹사이트 주소를 얻을 수 있는데,

해당 웹사이트 링크를 클릭해서 들어가면 다음과 같이 접근이 되지 않는 모습을 확인할 수 있다.

이를 Public에서도 접근할 수 있게끔 바꿔주어야 한다.

버킷 정책 변경

S3에 올라간 React 정적 파일을 웹에서 액세스 할 수 있게 버킷 정책을 변경해주고 추가해준다.(S3 버킷에 익명의 사용자들이 파일들을 조회할 수 있도록 권한 설정)

권한 탭으로 이동

1. 퍼블릭 액세스 차단 설정 편집 (웹에서 액세스 할 수 있게 차단 비활성화)

2. 버킷 정책 작성

[링크텍스트]( https://docs.aws.amazon.com/ AmazonS3/latest/userguide/ WebsiteAccessPermissionsReqd.html)
위 링크에 이동해서 스크롤을 내리면 다음과 같은 코드가 있는데 이를 복사해서 붙여넣기 하면된다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::Bucket-Name/*"
            ]
        }
    ]
}

그리고 Bucket-Name을 다음과 같이 변경시켜주면 된다.

그리고 다시 들어가보면 Forbidden이 아니라 Not Found가 뜨는 것을 확인할 수 있다.

이제는 access는 할 수 있지만 내가 파일을 아직 올리지 않았기 때문에 Not Found가 뜨는 것이다.


S3로 앱 자동 배포를 위한 yml 파일 완성하기

yml 파일에서 버전을 다음과 같이 16.x 하나로만 바꿔준다.

그리고나서, 아래 링크에 들어간다.
링크텍스트

여기에 S3에다가 deploy를 해주는 action들 중에서

- uses: awact/s3-action@master
      with:
        args: --acl public-read --follow-symlinks --delete
      env:
        SOURCE_DIR: './public'
        AWS_REGION: 'us-east-1'
        AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

이 부분을 가져와서 yml 파일에 넣어주자.

그리고 SOURCE_DIR는, 내가 npm run build를 하면, build 폴더 안에 들어가기 때문에 build로 바꿔준다.
그리고 AWS_REGION은 ap-northeast-2 라고 바꿔준다.

그리고 아래에 환경변수들이 있는데, 이러한 주요정보들은 여기다가 써주는 것이 아니라, 'Settings'에 들어가서 써줄 수 있는 부분이 있는데, 거기다가 쓰면 자동으로 가져와서 실행된다.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY는 어떻게 넣어줘야 하냐면 AWS IAM이라는 것이 있다.

IAM은 무엇인가 ? (Identity and Access Management)

AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹 서비스이다. IAM을 사용하여 리소스를 사용하도록 인증(로그인) 및 권한 부여(권한 있음)된 대상을 제어한다.

모든 사람이 모든 권한을 가지고 있으면 안되니까 루트 사용자가 IAM 사용자에게 일정 권한을 부여해준다.


검색 창에 IAM 이라고 입력해서 최상단의 IAM에 들어간다.

그리고 사용자에 들어가서 사용자 추가 버튼을 누른다.

다음과 같이 action-test-user라고 사용자 이름을 지정해주고, 다음 버튼을 누른다.

직접 정챙 연결을 클릭해주고

s3를 검색한 후, AmazonS3FullAccess를 체크해준다. 그러면 이 AmazonS3FullAccess 사용자는 이 권한을 부여받는다. 그리고 하단의 다음 버튼을 누르고,

사용자 생성 버튼을 누른다.

나는 강의를 잘 따라 왔지만 바로 생성되지 않아 다음과 같은 방법을 따랐다.

  • 사용자 목록에서 엑세스 키를 활성화하려는 사용자를 선택합니다.
  • "Security credentials" 탭을 클릭합니다.
  • "Access keys" 섹션에서 "Create access key" 버튼을 클릭합니다.
  • "Access key ID"와 "Secret access key"가 생성됩니다. 이 정보를 안전한 장소에 보관해야 합니다.
  • "Download .csv" 버튼을 클릭하여 엑세스 키 정보를 CSV 파일로 다운로드할 수도 있습니다.

엑세스키가 생성되었고, 이는 다시 볼 수 없기 때문에 하단에 .csv 파일 다운로드를 통해 잘 보관해두도록 하자.

우선, github로 돌아와서 commit changes를 클릭해주자.

그러면 다음과 같이 레포지토리에 커밋과 push가 된 것을 확인할 수 있다.

이제 환경변수를 넣어보자. 환경변수를 넣어줄 때는 Settings에 들어가서, Secrets and Variables에서 Actions에 들어간다.

그리고 New repository Secret을 클릭해서, 내 버킷 이름이었던 react-action-test-bucket-minseong를 환경변수로 넣어주고,

AWS_ACCESS_KEY_ID는 방금 저장해두었던 id를 넣어주고,

받았던 AWS_SECRET_ACCESS_KEY도 넣어준다.

Actions에 들어오면 fail이 난 것을 확인할 수 있는데, 환경변수를 못불러와서 그런 것이다.

들어가서 Re-run jobs를 눌러준다.

환경변수 설정이나, yml파일을 잘 확인하고 실행시키자.. 자꾸 오류가 발생해서 많이 애먹었다..
다음과 같이 실행되는 것을 볼 수 있다.

AWS에도 이렇게 잘 올라와 있는 것을 확인할 수 있다.

이제 링크에 access가 잘 되고 앱이 작동하는 것을 확인할 수 있다.

링크텍스트

profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글

관련 채용 정보