[우아한테크코스] 웹 프론트엔드 프리코스 2주 차

zimablue·2023년 10월 27일
post-thumbnail

1주 차 공통 피드백


  • 요구사항을 정확히 준수한다
    과제 제출 전에 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항의 항목을 모두 잘 지켰는지 다시 한 번 점검한다.

  • 커밋 메시지를 의미 있게 작성한다
    커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성한다.

  • git을 통해 관리할 자원에 대해서도 고려한다
    node modules 는 package.json 파일이 있으면 설치할 수 있고 버전 관리를 직접 하지 않으므로 git으로 관리하지 않아도 된다.
    Intellij의 .idea 폴더, VS Code의 .vscode 폴더 또한 개발 도구가 자동으로 생성하는 폴더이기 때문에 굳이 git으로 관리하지 않아도 된다.
    앞으로 git에 코드를 추가할 때는 git을 통해 관리할 필요가 있는지를 고려해볼 것을 추천한다.

  • Pull Request를 보내기 전 브랜치를 확인한다
    기능 구현 작업을 fork된 Repository의 main branch가 아닌, 기능 구현을 위해 새로 만든 브랜치에서 작업한 후 PR을 보낸다.

  • PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
    PR을 이미 한 번 보냈다면, 새로운 PR을 생성할 필요가 없다. 수정이 필요하다면 추가 커밋을 하면 자동으로 반영된다. 단, 미션 제출 기간 이후에는 추가 커밋을 하지 않는다.

  • 이름을 통해 의도를 드러낸다
    나 자신, 다른 개발자와의 소통을 위해 가장 중요한 활동 중의 하나가 좋은 이름 짓기이다.
    변수 이름, 함수(메서드) 이름, 클래스 이름을 짓는데 시간을 투자하라.
    이름을 통해 변수의 역할, 함수의 역할, 클래스의 역할에 대한 의도를 드러내기 위해 노력하라.
    연속된 숫자를 덧붙이거나(a1, a2, ..., aN), 불용어(Info, Data, a, an, the)를 추가하는 방식은 적절하지 못하다.

  • 축약하지 않는다
    의도를 드러낼 수 있다면 이름이 길어져도 괜찮다.
    누구나 실은 클래스, 메서드, 또는 변수의 이름을 줄이려는 유혹에 곧잘 빠지곤 한다.
    그런 유혹을 뿌리쳐라. 축약은 혼란을 야기하며, 더 큰 문제를 숨기는 경향이 있다.
    클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자.
    클래스 이름이 Order라면 shipOrder라고 메서드 이름을 지을 필요가 없다.
    짧게 ship()이라고 하면 클라이언트에서는 order.ship()라고 호출하며, 간결한 호출의 표현이 된다.

  • 객체 지향 생활 체조 원칙 5: 줄여쓰지 않는다 (축약 금지)

  • 공백도 코딩 컨벤션이다
    if, for, while문 사이의 공백도 코딩 컨벤션이다.

  • 공백 라인을 의미 있게 사용한다
    공백 라인을 의미 있게 사용하는 것이 좋아 보이며, 문맥을 분리하는 부분에 사용하는 것이 좋다. 과도한 공백은 다른 개발자에게 의문을 줄 수 있다.

  • space와 tab을 혼용하지 않는다
    들여쓰기에 space와 tab을 혼용하지 않는다.
    둘 중에 하나만 사용한다.
    확신이 서지 않으면 pull request를 보낸 후 들여쓰기가 잘 되어 있는지 확인하는 습관을 들인다.

  • 의미 없는 주석을 달지 않는다
    변수 이름, 함수(메서드) 이름을 통해 어떤 의도인지가 드러난다면 굳이 주석을 달지 않는다.
    모든 변수와 함수에 주석을 달기보다 가능하면 이름을 통해 의도를 드러내고, 의도를 드러내기 힘든 경우 주석을 다는 연습을 한다.

  • linter와 Code Formatter의 기능을 활용한다
    가능하면 eslint와 prettier를 이용해 더욱 생산적으로 코드를 작성하자.
    린트(lint)는 소스 코드에 문제가 있는지 탐색하는 작업을 의미하며, 린터(linter)는 이 작업을 도와주는 소프트웨어를 말한다.
    자바스크립트와 같은 인터프리터 언어의 경우, 런타임 에러가 발생할 확률이 높기 때문에, 이 린트 작업을 통해 사전에 에러를 최대한 잡아준다면 훨씬 생산성 높은 개발을 할 수 있다.
    lint 중 eslint는 자바스크립트 진영의 오픈소스로 확장되고 있는 정적 분석 도구이다.
    prettier는 일종의 Code Formatter이다.
    Code Formatter란 개발자가 작성한 코드가 정해진 코딩 스타일을 따르도록 변환해주는 도구이다.
    이 두 가지 도구를 이용하면 코드를 짜는데 발생할 수 있는 오류를 미리 예방하고 쉽게 정돈할 수 있다.

  • EOL(End Of Line)
    최종 제출하는 코드에서 EOL을 확인한다.
    환경에 따라 의도한 바와 다르게 개행 문자 처리가 되지 않도록 EOL 설정을 확인한다.

  • 불필요한 console.log를 남기지 않는다
    디버깅을 위해 사용한 console.log가 최종 제출하는 코드에 의미 없이 남아있지 않도록 주의한다.

  • JavaScript에서 제공하는 API를 적극 활용한다
    함수(메서드)를 직접 구현하기 전에 JavaScript API에서 제공하는 기능인지 검색을 먼저 해본다.
    JavaScript API에서 제공하지 않을 경우에 직접 구현한다.
    예를 들어 우승자를 출력할 때 우승자가 2명 이상이면 쉼표(,) 기준으로 출력을 위한 문자열은 다음과 같이 구현할 수 있다.

const members = ['east', 'west', 'south'];
members.map((member) => member).join(','); // "east,west,south"

추가 학습 자료


2주 차 목표

: 2주 차 미션에서는 1주 차에서 학습한 것에 더해 함수를 분리하고, 각 함수별로 테스트를 작성하는 것에 익숙해지는 것을 목표로 하고 있어요.
이번에 테스트를 처음 접하시는 분들은 언어별 테스트 도구를 학습하고 작은 단위의 기능부터 테스트를 작성해보길 바랍니다.





2주 차에서 생각했던 것


2주차에서 신경썼던 내용을 보면 다음과 같습니다.

  1. 코드 컨벤션

  2. 깃 컨벤션

  3. jest



1. eslint prettier airbnb 컨벤션 만들기


ESLint는 린터(Linter)이고, Prettier는 포맷터(Formatter)입니다.
포맷터는 코드의 들여쓰기와 빈칸을 자동으로 포맷팅해 줍니다.
린터는 형식을 수정해 주지만 코드 스타일도 수정해 주는 확장된 포맷터에 가깝습니다.

2주차에서는 ESLintPrettier를 airbnb가 사용하고 있는 컨벤션으로 맞춰서 코드를 작성하기 시작했습니다.

이 과정에서 프론트엔드 회고 스터디원의 도움을 받아 Eslint / prettier을 쉽게 세팅할 수 있었습니다.


# 터미널에 입력하여 패키지 설치

npm install -D eslint prettier
npx install-peerdeps --dev eslint-config-airbnb
npm install -D eslint-config-prettier eslint-plugin-prettier
.eslintrc.cjs

module.exports = {
  extends: ['airbnb', 'plugin:prettier/recommended'],
  rules: {
    'prettier/prettier': ['error', { endOfLine: 'auto' }],
    'operator-linebreak': ['error', 'before'],
    'max-depth': ['error', 2],
  },
};
.prettierrc.cjs

module.exports = {
  printWidth: 80, // 한 줄 최대 문자 수
  tabWidth: 2, // 들여쓰기 시, 탭 너비
  useTabs: false, // 스페이스 대신 탭 사용
  semi: true, // 문장 끝 세미콜론 사용
  singleQuote: true, // 작은 따옴표 사용
  trailingComma: 'all', // 꼬리 콤마 사용
  bracketSpacing: true, // 중괄호 내에 공백 사용
  arrowParens: 'avoid', // 화살표 함수 단일 인자 시, 괄호 생략
  proseWrap: 'never', // 마크다운 포매팅 제외
  endOfLine: 'auto', // 개행문자 유지 (혼합일 경우, 첫 줄 개행문자로 통일)
};
.vscode/setting.json

{
  "[javascript]": {
    "editor.formatOnSave": true
  }
}

폴더구조는 아래와 같습니다.


이후 VSC 확장기능에서 ESLintPrettier를 설치하면 됩니다.





2. 깃 컨벤션

깃 커밋 메시지의 형식도 Angular 깃 컨벤션에 맞게 작성해봤습니다.

git commit 할 때 -m을 사용해서 바로 작성하지 않고 git commit을 사용합니다.
그럼 git commit 메시지 작성 페이지가 나타납니다.

여기서 i를 클릭하여 Angular 깃 컨벤션에 맞춰 메시지를 작성하고 esc 로 작성을 마무리하고 :wq를 사용하여 git commit을 완료합니다.





3. jest 단위 테스트

1주차와 2주차 과제에 포함되어 있는 테스트 함수를 분석해보았습니다.
Jest 문법에 관한 내용은 Testing Javascript에 정리되어 있습니다.


1주차 전체 테스트 코드

// ApplicationTest.js

import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";

const mockQuestions = (inputs) => {
  
  MissionUtils.Console.readLineAsync = jest.fn();

  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();
    return Promise.resolve(input);
  });
};

const mockRandoms = (numbers) => {
  MissionUtils.Random.pickNumberInRange = jest.fn();
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
};

const getLogSpy = () => {
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  logSpy.mockClear();
  return logSpy;
};

describe("숫자 야구 게임", () => {
  test("게임 종료 후 재시작", async () => {
    // given
    const randoms = [1, 3, 5, 5, 8, 9];
    const answers = ["246", "135", "1", "597", "589", "2"];
    const logSpy = getLogSpy();
    const messages = ["낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료"];

    mockRandoms(randoms);
    mockQuestions(answers);

    // when
    const app = new App();
    await expect(app.play()).resolves.not.toThrow();

    // then
    messages.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });

  test("예외 테스트", async () => {
    // given
    const randoms = [1, 3, 5];
    const answers = ["1234"];

    mockRandoms(randoms);
    mockQuestions(answers);

    // when & then
    const app = new App();

    await expect(app.play()).rejects.toThrow("[ERROR]");
  });
});



mockQuestions

mockQuestionsanswers 배열의 값을 왼쪽부터 하나씩 프로미스로 반환하는 역할을 갖습니다.

// ApplicationTest.js

const mockQuestions = (inputs) => {
  
  // @woowacourse 모듈 Console 클래스의 readLineAsync 메서드를 모킹합니다.
  MissionUtils.Console.readLineAsync = jest.fn();

  // MissionUtils.Console.readLineAsync mock 함수는 주어진 callback을 실행합니다.
  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    // mockQuestions으로 answers 배열의 첫 번째 요소를 제거하고 반환합니다.
    // answers 배열은 다음과 같습니다. [ "246", "135", "1", "597", "589", "2" ]
    const input = inputs.shift();
    // 반환된 요소를 프로미스(resolve)에 담아 반환합니다.
    return Promise.resolve(input);
  });
};

mockRandoms

// ApplicationTest.js

const mockRandoms = (numbers) => {
  // @woowacourse 모듈 Console 클래스의 pickNumberInRange 메서드를 모킹합니다.
  MissionUtils.Random.pickNumberInRange = jest.fn();
  
  // pickNumberInRange 목 함수에 randoms 배열의 요소를 주입합니다.
  // pickNumberInRange 목 함수는 mockReturnValueOnce로 주입된 요소를 반환합니다.
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
  // 첫 numbers 에는 [ 1, 3, 5, 5, 8, 9 ]가 담기게 됩니다.
};
// generateRandomNum.js

import { Random } from '@woowacourse/mission-utils';

export const generateRandomNum = () => {
  const computer = [];
  while (computer.length < 3) {
    // Random.pickNumberInRange(1, 9);의 반환값은 [ 1, 3, 5, 5, 8, 9 ]입니다.
    const num = Random.pickNumberInRange(1, 9);
    if (!computer.includes(num)) {
      computer.push(num);
    }
  }

  // computer는 [ 1, 3, 5 ]와 [ 5, 8, 9 ]가 됩니다.
  return computer;
};

getLogSpy

getLogSpyconsole.log로 출력되는 값을 확인합니다.

// ApplicationTest.js

const getLogSpy = () => {
  // MissionUtils.Console.print 의 호출에 대한 정보 염탐
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  // logSpy의 모든 이전 호출 정보를 지우고 상태를 초기화
  logSpy.mockClear();
  return logSpy;
};



test('게임 종료 후 재시작')

resolves.not.toThrow()

App 클래스의 play 매서드를 실행하고 반환되는 Promise가 resolve일 경우 throw가 발생하지 않는지 확인합니다.

// ApplicationTest.js

const app = new App();
await expect(app.play()).resolves.not.toThrow();

toHaveBeenCalledWith

output'낫싱', '3스트라이크', '1볼 1스트라이크', '3스트라이크', '게임 종료'를 순서대로 갖습니다.
logSpy로 반환되는 console.log 값과 output의 값이 일치하는지 확인합니다.

// ApplicationTest.js

messages.forEach(output => {
  expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});



test('예외 테스트')

예외 테스트는 일부로 옳지 않은 값을 입력합니다.
예외 테스트의 랜덤 값은 [ 1, 3, 5]이고 입력 값은 '1234'입니다.

rejects.toThrow

App 클래스를 실행하고 비동기 함수가 반환한 Promise가 reject일 때 에러가 '[ERROR]'문자열을 가지고 있는 지 확인합니다.


const randoms = [1, 3, 5];
const answers = ['1234'];

mockRandoms(randoms);
mockQuestions(answers);

const app = new App();

await expect(app.play()).rejects.toThrow('[ERROR]');





2주차 전체 테스트 코드

// ApplicationTest.js

import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";

const mockQuestions = (inputs) => {
  MissionUtils.Console.readLineAsync = jest.fn();

  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    const input = inputs.shift();
    return Promise.resolve(input);
  });
};

const mockRandoms = (numbers) => {
  MissionUtils.Random.pickNumberInRange = jest.fn();
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
};

const getLogSpy = () => {
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  logSpy.mockClear();
  return logSpy;
};

describe("자동차 경주 게임", () => {
  test("전진-정지", async () => {
    // given
    const MOVING_FORWARD = 4;
    const STOP = 3;
    const inputs = ["pobi,woni", "1"];
    const outputs = ["pobi : -"];
    const randoms = [MOVING_FORWARD, STOP];
    const logSpy = getLogSpy();

    mockQuestions(inputs);
    mockRandoms([...randoms]);

    // when
    const app = new App();
    await app.play();

    // then
    outputs.forEach((output) => {
      expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
    });
  });

  test.each([
    [["pobi,javaji"]],
    [["pobi,eastjun"]]
  ])("이름에 대한 예외 처리", async (inputs) => {
    // given
    mockQuestions(inputs);

    // when
    const app = new App();

    // then
    await expect(app.play()).rejects.toThrow("[ERROR]");
  });
});



mockQuestions

mockQuestionsanswers 배열의 값을 왼쪽부터 하나씩 프로미스로 반환하는 역할을 갖습니다.

// ApplicationTest.js

const mockQuestions = (inputs) => {
  
  // @woowacourse 모듈 Console 클래스의 readLineAsync 메서드를 모킹합니다.
  MissionUtils.Console.readLineAsync = jest.fn();

  // MissionUtils.Console.readLineAsync mock 함수는 주어진 callback을 실행합니다.
  MissionUtils.Console.readLineAsync.mockImplementation(() => {
    // mockQuestions으로 answers 배열의 첫 번째 요소를 제거하고 반환합니다.
    // answers 배열은 다음과 같습니다. ["pobi,woni", "1"]
    const input = inputs.shift();
    // 반환된 요소를 프로미스(resolve)에 담아 반환합니다.
    return Promise.resolve(input);
  });
};

mockRandoms

// ApplicationTest.js

const mockRandoms = (numbers) => {
  // @woowacourse 모듈 Console 클래스의 pickNumberInRange 메서드를 모킹합니다.
  MissionUtils.Random.pickNumberInRange = jest.fn();
  
  // pickNumberInRange 목 함수에 randoms 배열의 요소를 주입합니다.
  // pickNumberInRange 목 함수는 mockReturnValueOnce로 주입된 요소를 반환합니다.
  numbers.reduce((acc, number) => {
    return acc.mockReturnValueOnce(number);
  }, MissionUtils.Random.pickNumberInRange);
  // 첫 numbers 에는 [ 4, 3 ]이 담기게 됩니다.
};
// generateRandomNum.js

import { Random } from '@woowacourse/mission-utils';

  static isMinNumFour() {
    // Random.pickNumberInRange()은 4와 3을 순서대로 반환합니다.
    return (
      Random.pickNumberInRange(
        MAGIC_NUM.MIN_RANDOM_NUM,
        MAGIC_NUM.MAX_RANDOM_NUM,
      ) >= MAGIC_NUM.THRESHOLD_NUM
    );
  }

getLogSpy

getLogSpyconsole.log로 출력되는 값을 확인합니다.

// ApplicationTest.js

const getLogSpy = () => {
  // MissionUtils.Console.print 의 호출에 대한 정보 염탐
  const logSpy = jest.spyOn(MissionUtils.Console, "print");
  // logSpy의 모든 이전 호출 정보를 지우고 상태를 초기화
  logSpy.mockClear();
  return logSpy;
};



test('전진-정지')

toHaveBeenCalledWith

클래스 App을 실행합니다.
output은 문자열 pobi : -를 갖습니다.
logSpy로 반환되는 console.log 값과 outputpobi : -이 일치하는지 확인합니다.

// ApplicationTest.js

const app = new App();
await app.play();

messages.forEach(output => {
  expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});



test('이름에 대한 예외 처리"')

test.each()

javajieastjun는 조건에 맞지 않는 입력이기 때문에 에러가 발생해야 합니다.
에러 메시지에 "[ERROR]" 문자열이 포함되어 있다면 통과합니다.

test.each([
  [["pobi,javaji"]],
  [["pobi,eastjun"]]
])("이름에 대한 예외 처리", async (inputs) => {
  mockQuestions(inputs);

  const app = new App();

  await expect(app.play()).rejects.toThrow("[ERROR]");
});

0개의 댓글