Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b557228
docs: 자동차 경주 기능 구현 체크리스트 추가
yooaknow Oct 22, 2025
ac625e8
docs: 함수 분해 관련 내용 추가
yooaknow Oct 26, 2025
e808f55
docs: 리드미 출처 구문을 인용문 형식으로 수정
yooaknow Oct 26, 2025
70b2892
docs: 구조 설계 및 파일 분리 기준 작성
yooaknow Oct 26, 2025
c09b3ef
chore: @woowacourse/mission-utils 모듈 설치 및 프로젝트 의존성 설정
yooaknow Oct 26, 2025
803cbc5
chore: package.json 설정 수정 및 description 필드 정리
yooaknow Oct 26, 2025
c5ae408
feat:자동차 이름과 시도 횟수 입력 기능 추가
yooaknow Oct 26, 2025
40c1572
docs: 입력 처리 단계 구현 항목 체크 완료
yooaknow Oct 26, 2025
c0075fb
feat: 입력값 검증 로직 추가
yooaknow Oct 26, 2025
b4a6b00
docs: 자동차 이름/ 시도 획수 검증 단계 구현 항목 체크 완료
yooaknow Oct 26, 2025
b84e3e2
feat: 자동차 경주 로직 및 전진 구현
yooaknow Oct 26, 2025
3b400a9
docs: 전진 로직 구현 항목 체크 완료
yooaknow Oct 26, 2025
03fc5dd
feat: 라운드 결과 및 최종 우승자 출력 추가
yooaknow Oct 26, 2025
ea46a7a
docs: 출력 및 우승자 항목 체크 완료
yooaknow Oct 26, 2025
996dcef
feat: 실행 흐름 통합
yooaknow Oct 26, 2025
5067774
fix(output): 변수 오타 수정 (car → cars, distance → c.distance)
yooaknow Oct 26, 2025
30fa32f
fix(app): 예외 처리 정리 및 default export 추가
yooaknow Oct 26, 2025
4018450
fix(output):파일명 오타 수정 (ouput.js -> output.js)
yooaknow Oct 26, 2025
a794c02
Fix(output): 'Winners'-> 'winner'로 수정하여 최종 우승자 출력 오류 해결
yooaknow Oct 27, 2025
719393c
fix(output): 우승자 출력 포맷 일치(콜론 앞뒤 공백)
yooaknow Oct 27, 2025
b85c754
docs: 예외 처리 확인 및 체크리스트 반영
yooaknow Oct 27, 2025
d6c8e6d
docs: README에서 불필요한 문구 삭제
yooaknow Oct 27, 2025
83e0255
refactor(race): 3항 연산자 제거
yooaknow Oct 27, 2025
5f95892
docs: 3항 연산자를 쓰지 않아야하는 이유 작성
yooaknow Oct 27, 2025
385b2a0
fix(input): 입력 문자열 수정
yooaknow Oct 27, 2025
6c13fa2
refactor(race): race 로직에 랜덤 의존성 주입
yooaknow Oct 27, 2025
25e3009
docs: 랜덤 생성 로직 분리 이유 추가
yooaknow Oct 27, 2025
515d95c
test: race 모듈 테스트 코드 추가
yooaknow Oct 27, 2025
6cb7e9e
test: validate 모듈 단위 테스트 추가
yooaknow Oct 27, 2025
1318086
docs: 개발을 마무리하며 느낀 생각 정리
yooaknow Oct 27, 2025
78dc735
docs: README 헤딩 레벨(# → ###) 수정
yooaknow Oct 27, 2025
02df73e
docs: README 오타 수정
yooaknow Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,137 @@
# javascript-racingcar-precourse
---
## 💡 왜 구현을 해야 하는가
두 번째 과제는 자동차 경주 게임을 구현하는 것임. 그 중에서도 이번 과제의 목표가 “큰 함수를 단일 역할을 수행하는 작은 함수로 분리”하는 것이라는 점을 보았음.처음에는 단순히 랜덤 값을 뽑아 자동차를 움직이면 되는 문제라고 생각했으나, 실제로 구현을 진행하려고 보니 어떻게 함수를 분리해야 좋은 구조가 되는지 궁금해졌음. 그래서 단순히 함수를 작게 쪼개면 되는 것인지 아니면 다른 기준이 필요한 것인지에 대해 고민하게 되었고 자료를 찾아보게 되었음.

---
## 관련 논문 요약
그 중에서도 나의 궁금증을 풀어준 연구 자료가 있었음.

연구에서는 두 가지 기능을 비교했음.
하나는 날짜 차이를 계산하는 단순한 절차형 기능(입력 → 계산 → 출력처럼 단계가 명확한 구조)이었고, 다른 하나는 조건문과 데이터 흐름이 복잡한 기능(여러 상황을 분기 처리해야 하는 구조)이었음.

실험 결과, 전자의 경우에는 여러 함수를 나누어 구현한 방식이 이해하기 더 쉬웠음.
하지만 후자의 경우에는 오히려 함수가 많아질수록 흐름을 따라가기 어려워져 하나의 함수로 구현한 코드가 더 쉽게 이해되었음.

따라서 기능의 성격에 따라 결과가 달랐으며,
“함수를 잘게 쪼개면 항상 이해가 쉬워진다”는 일반적인 법칙은 성립하지 않았음.
결론적으로, 함수 분해는 맥락 의존적이며,
코드의 목적·데이터 흐름 복잡도·변경 빈도 등을 기준으로 결정해야 한다고 제안함.

> **출처:**
> Tempero, E., Denny, P., Finnie-Ansley, J., Luxton-Reilly, A., et al. (2024).
> *On the Comprehensibility of Functional Decomposition: An Empirical Study.*
> In *Proceedings of the 32nd IEEE/ACM International Conference on Program Comprehension (ICPC ’24)*, 214–224. IEEE/ACM.
> [https://doi.org/10.1145/3643916.3644432]
---

## 구조 설계 및 파일 분리 기준
따라서 이번 과제에서는 하나의 파일 안에 모든 기능을 넣지 않고 기능 단위로 파일을 분리하고, 각 파일 내부에서도 역할별 함수로 세분화했음.
파일은 크게 입력 / 검증 / 경주 / 출력 / 실행 흐름 관리로 나눴고,

각 파일 내부 함수는 논문 기준에 따라
절차형 단계는 작게 분리, 분기·예외가 많은 로직은 응집하는 방향으로 구성했음.

1. App.js
App은 프로그램의 시작점이라,
로직을 넣기보단 전체 흐름만 관리하도록 단순하게 유지하는 게 맞다고 판단함.

2. input.js
사용자 입력을 받는 부분은 절차형 단계로 구분함.
자동차 이름을 받고, 그 다음 시도 횟수를 받는 순서가 고정되어 있으므로 각 입력 과정을 별도의 단계로 나누는 것이 자연스러움.

3. validate.js
입력값 검증은 여러 조건을 동시에 확인해야 하므로 분기형 로직임. 예외 케이스(빈 문자열, 이름 길이 초과, 숫자 아님 등)가 다양해서
함수를 세분화하면 오히려 흐름이 끊어질 가능성이 있음. 그래서 “자동차 이름 전체 검증”과 “시도 횟수 검증”처럼 입력 단위로 묶어서 한 번에 처리하기로 함.

4. race.js
자동차 경주 로직은 여러 단계로 이루어지는 절차형 흐름이므로 단계별로 나누는 게 효율적임.

- 전체 라운드를 반복하는 단계
- 한 라운드를 실행하는 단계
- 전진할지 판단하는 정책 단계
특히 전진 여부를 결정하는 랜덤 정책은 변경 가능성이 높기 때문에 별도로 분리하는 방식으로 설계함.

5. output.js
출력은 프로그램의 마지막 절차형 단계로, 콘솔에 결과를 표시하는 역할만 담당함. 로직과 출력이 섞이면 테스트하기 어려워지므로 완전히 분리함.
라운드별 출력과 최종 우승자 출력은 서로 다른 책임이므로 각각 독립적으로 다루는 것이 좋다고 판단했음.

---

## ✅ 구현 체크리스트

### 입력 처리
- [x] 안내 문구 출력: `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)`
- [x] 자동차 이름 문자열 입력받기
- [x] 안내 문구 출력: `시도할 횟수는 몇 회인가요?`
- [x] 시도 횟수 입력받기

### 자동차 이름 파싱 및 검증
- [x] 쉼표(`,`) 기준으로 이름 분리
- [x] 각 이름 길이 1~5자 검증
- [x] 빈 이름/연속 구분자(`pobi,,jun`) 검증

### 시도 횟수 검증
- [x] 정수 여부 확인
- [x] 1 이상인지 확인

### 전진 로직
- [x] 매 시도마다 모든 자동차에 대해 난수 생성
- [x] 난수가 4이상이면 전진, 그 외 정지
- [x] 전진 시 해당 자동차의 이동 기록에 `-` 1개 추가

### 차수별 실행 결과 출력
- [x] 첫 라운드 전 `실행 결과` 출력
- [x] 각 라운드 종료 시 `이름 : -` 형식으로 모든 자동차 상태 출력

### 우승자 판별 및 출력
- [x] 최종 최대 진행 거리 계산
- [x] 최대 거리 동률 자동차 전부 우승 처리
- [x] `최종 우승자 : 이름1, 이름2` 형식으로 출력

### 예외 처리 (발생 시 종료)
- [x] 모든 에러 메시지는 **`[ERROR]`** 로 시작
- [x] 잘못된 이름 형식(빈 토큰/연속 구분자) → `[ERROR] 잘못된 이름 형식입니다.
- [x] 이름 길이 초과(6자 이상) → `[ERROR] 자동차 이름은 5자 이하만 가능합니다.
- [x] 시도 횟수가 음수 or 0일때 → `[ERROR] 시도 횟수는 1 이상의 숫자여야 합니다.'
---
## 💭 코드 개선 고민 1 🤔
### 3항 연산자를 쓰지 않아야하는 이유
자바스크립트에 3항 연산자가 있는데도 사용을 줄여야하는 이유는 무엇일까?라는 생각이 들었음.

MDN 문서에 따르면, if...else를 한 줄로 줄이기 위한 문법적 축약이지,
여러 조건을 처리하거나 로직 분기를 표현하기 위한 구조가 아님.

특히 삼항 연산자는 오른쪽 결합형(right-associative) 이라
중첩될 경우 a ? b : (c ? d : e)처럼 해석되어 코드를 읽기 어렵게 만들 수 있음.

> **출처:**
> Mozilla Contributors. (2025, July 8). Conditional (ternary) operator.
> In MDN Web Docs – Expressions and Operators. Mozilla Foundation.
> Retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator

이에 따라 race.js / playRound에서 3항 연산자 제거했음.

---

## 💭 코드 개선 고민 2 🤔
### 랜덤 의존성 주입
전체 코드를 다시 보며 “각 함수가 한 가지 일만 하도록 잘 나눠졌는가?”를 점검하던 중,
race.js에서 랜덤 생성과 이동 규칙 적용이라는 두 가지 역할이 한 함수에 섞여 있다는 걸 발견함.
“함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.”라는 요구사항을 떠올리고,
두 역할을 분리해 함수가 한 가지 일만 수행하도록 수정함.
랜덤 생성 부분을 매개변수(rng) 로 분리하여 외부에서 주입받도록 변경함.

---

## 개발을 마무리하며
### 개발자의 관점에서의 생각
이번 과제를 진행하며 단순히 기능을 구현하는 것보다, 의도한 대로 정확히 동작하도록 구조를 다듬는 과정의 중요성을 깨닫게 되었음.
처음에는 작은 실수들이 예상보다 큰 오류로 이어졌음. output.js 파일명 오타(ouput.js)로 인해 모듈이 로드되지 않거나, 변수명 불일치(car → cars, distance → c.distance)로 출력이 깨지는 문제가 여러 번 발생했음. 또한 우승자 출력 시 'Winners' → 'winner'처럼 단어 하나의 차이로 결과가 달라지는 과정을 겪으며, 출력 포맷의 일관성이 사용자 신뢰와 가독성에 직접적인 영향을 준다는 점을 배웠음.

### 설계자 관점에서의 생각
우테코 2주 차 과제의 핵심 목표가 “여러 역할을 수행하는 큰 함수를 단일 역할을 수행하는 작은 함수로 분리한다”는 점이었음.
따라서 이번 과제에서는 단순히 기능을 나누는 데 그치지 않고, 어디까지를 분리하고 어디서부터 응집해야 하는가를 역할을 나누면서 고민하며 구조를 설계했음. 처음에는 가능한 한 모든 로직을 세분화하려 했으나 실제로는 작은 함수로 분리하면서도 흐름의 자연스러움이 유지되도록 하는 균형이 중요하다는 것을 깨달았음. 이 경험을 통해 함수 분해는 단순한 규칙이 아니라 로직의 성격과 데이터 흐름의 복잡도를 고려해야 하는 설계적 결정임을 배웠음.

### 기획자 관점에서의 생각
단순히 자동차가 랜덤으로 움직이는 게임으로 끝내기보다 라운드마다 플레이어가 전략적으로 선택할 수 있는 구조로 확장하면 더 재미있을 것이라 생각했음. 매 라운드 승리 시 단순히 전진하는 대신 ‘전진’하거나 ‘상대를 방해’하는 선택권을 주면 게임에 심리전이 생기며 단순 확률 게임이 리스크 관리와 판단이 공존하는 구조로 바뀜. 이렇게 하면 매 턴이 의미 있는 결정의 순간이 되어 짧은 콘솔 게임 안에서도 전략적 재미와 몰입감을 줄 수 있을 것이라 생각했음.
45 changes: 45 additions & 0 deletions __tests__/raceTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { shouldMove, playRound, playAllRounds } from "../src/race.js";


const seqRng = (...nums) => {
let i = 0;
return () => nums[i++ % nums.length];
};

describe("shouldMove", () => {
test("rng 결과가 4 이상이면 true", () => {
expect(shouldMove(() => 4)).toBe(true);
expect(shouldMove(() => 9)).toBe(true);
});

test("rng 결과가 3 이하면 false", () => {
expect(shouldMove(() => 3)).toBe(false);
expect(shouldMove(() => 0)).toBe(false);
});
});

describe("playRound", () => {
test("각 차의 이동 여부가 rng에 따라 결정", () => {
const cars = [{ name: "a", distance: 0 }, { name: "b", distance: 2 }];

const rng = seqRng(4, 3);
const next = playRound(cars, rng);
expect(next).toEqual([
{ name: "a", distance: 1 },
{ name: "b", distance: 2 }
]);
});
});

describe("playAllRounds", () => {
test("tryCount 만큼 라운드 진행", () => {
const cars = [{ name: "a", distance: 0 }, { name: "b", distance: 0 }];

const rng = seqRng(4, 4, 3, 9, 4, 0);
const result = playAllRounds(cars, 3, rng);
expect(result).toEqual([
{ name: "a", distance: 2 },
{ name: "b", distance: 2 }
]);
});
});
37 changes: 37 additions & 0 deletions __tests__/validateTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { validateCarNames, validateTryCount } from "../src/validate.js";

describe("validateCarNames", () => {
test("정상: 공백 트림 + 5자 이하", () => {
expect(validateCarNames(["pobi", " woni ", "jun"])).toBe(true);
});

test("에러: 배열이 아님 또는 빈 배열", () => {
expect(() => validateCarNames(null)).toThrow("[ERROR] 잘못된 이름 형식입니다.");
expect(() => validateCarNames(undefined)).toThrow("[ERROR] 잘못된 이름 형식입니다.");
expect(() => validateCarNames([])).toThrow("[ERROR] 잘못된 이름 형식입니다.");
});

test("에러: 빈 문자열/공백만", () => {
expect(() => validateCarNames(["", "pobi"])).toThrow("[ERROR] 잘못된 이름 형식입니다.");
expect(() => validateCarNames([" "])).toThrow("[ERROR] 잘못된 이름 형식입니다.");
});

test("에러: 6자 이상", () => {
expect(() => validateCarNames(["longggg"])).toThrow("[ERROR] 자동차 이름은 5자 이하만 가능합니다.");
});
});

describe("validateTryCount", () => {
test("정상: 1 이상의 정수", () => {
expect(() => validateTryCount(1)).not.toThrow();
expect(() => validateTryCount(3)).not.toThrow();
});

test("에러: 1 미만/정수 아님/NaN", () => {
const msg = "[ERROR] 시도 횟수는 1 이상의 숫자여야 합니다.";
expect(() => validateTryCount(0)).toThrow(msg);
expect(() => validateTryCount(-1)).toThrow(msg);
expect(() => validateTryCount(1.5)).toThrow(msg);
expect(() => validateTryCount(NaN)).toThrow(msg);
});
});
Loading