# 10/14 (화)
자.. 시작입니다!
마치 1년짜리 크루즈선 이용권을 끊고 크루즈에 처음 올라타는 순간인 것 같은 기분이 듭니다.
## 시작부터 헤매주고

이모지를 보니 돌고래단, 헬스인, 로켓단 등이 있는 것 같습니다.
그나저나,, 분명 지원 플랫폼 - 과제라면서요.

여기서 아무리 찾아도 '과제' 카테고리가 없는 게 아니겠어요?

겨우겨우 지원서 탭에서 찾아낸 뒤 시작할 수 있었습니다..
## 미션 시작 전
프리코스 안내에 대한 내용과 요구 사항 등을 꼼꼼히 읽고 난 뒤
본격적으로 미션 준비를 순서대로 하기 시작했습니다.
1. Node 업데이트
노드 버전이 최소 22.19.0 이상이어야 한다고 하길래 오래된 노드부터 업데이트해 주고

2. Repository fork 후 VSCode에 기본 세팅까지 완료
크게 어려운 부분은 없었습니다. 템플릿을 꽤 자세하게 줘서 좋았습니다.
눈에 띄는 건 package.json에 이전에 변경된 기록이 남아있었는데

이전 기수와 크게 다를 게 없었는지 Node 최소 버전이 올라간 것과 jest가 최신 버전으로 바뀐 것뿐이었습니다.
3. 미션 꼼꼼히 읽어보기
1주 차 미션은 문자열 덧셈 계산기였습니다.
예상대로 문제는 어렵지 않았지만 대충 읽어도 예외 케이스가 2000개 정도 나올 것 같았습니다.
4. Javascript Style Guide (airbnb style) 정독
이와 관련해선 이미 글을 포스팅했으니 참고해 주시면 감사하겠습니다 👍
5. @woowacourse/mission-utils import
npm i @woowacourse/mission-utils
구현 전에 기본적으로 개발 환경 세팅 등 잘 동작하는지 확인을 해야 하기 때문에
우테코에서 제공하는 라이브러리 정도까지만 import 후 실행했을 때
에러 없이 모두 잘 동작하는 것을 확인했습니다.
## 시작
### README
첫 번째로 해야 할 일은 README.md에 구현할 기능 목록을 정리해 추가하는 것이었습니다.
(사실 제대로 된 리드미도 처음 써보는 나예요)
미션의 기능 요구 사항을 다시 살펴보면
입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.
쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다.
커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨 후 애플리케이션은 종료되어야 한다.
이게 전부였습니다.
이제 이걸 개발 관점에서 구체적으로 작성하면 되는 것이겠죠.
하지만.. VSC에서 직접 마크다운으로 작성하기엔 불편한 점이 많았습니다..
이럴 때 필요한 게 커뮤니티겠죠?? 저는 바로 커뮤니티에 물어봤습니다.

순식간에 많은 분들이 답변을 달아주셨고 그중에 가장 접근성이 좋고 간편한 노션으로 작성하기로 했습니다.
평소에 노션을 자주 쓰기에 나에겐 빠르고 쉽게 작성할 수 있는 방법이었습니다.

먼저 큰 흐름을 정리해 봤습니다.
문제 자체는 브론즈 4 정도 되어 보였습니다.
그리곤 조금 더 구체적으로 적어봤습니다.

나름 한 단위 기능에 따라 숫자로 매겨서 총 5개의 과정으로 분류해 봤고
내부적으로도 의사코드보다 살짝 낮은 level 수준 정도로 적어보았습니다.
이 짧은 몇 줄을 보기 좋고 일관성 있게 적기 위해 1시간은 걸린 것 같습니다..
### 예외처리 Thinking
그리고 나선 예외처리에 대해 생각해 보았습니다.
가장 처음에 생각나는 대로 적은 예외처리는 다음과 같았습니다.

그리곤 친구들한테 한 번 물어봤습니다.


친구들은 적극적으로 같이 생각해 주었고 함께 얘기하다 보니 몇 가지 생각해 볼 점이 생겼습니다.
1. 여러 문자(문자열)가 구분자가 될 수 있는가
2. 그렇다면 \n도 구분자가 될 수 있는가
3. 문자열이 '//\n1,2,3'과 같은 경우, 커스텀 구분자 없이 continue인가 아니면 커스텀 문자열 미지정으로 error인가
4. 모든 문자가 커스텀 구분자가 될 수 있다면 공백도 커스텀 구분자가 될 수 있는가
5. 모든 문자가 커스텀 구분자가 될 수 있다면 한글이나 이모지, 다른 언어 등도 커스텀 구분자가 될 수 있는가
처음 생각했던 것보다 더 많은 예외 케이스와 경우의 수가 생길 것 같았고 위 질문을 토대로 다시 정리한 예외 처리는 다음과 같았습니다.

그리고 위의 질문에 대한 제 답변은 이렇습니다.
1. 여러 문자(문자열)가 구분자가 될 수 있는가
YES > 문제 요구사항에서의 커스텀 구분자의 정의는 '문자열 앞부분의 //와 \n 사이에 위치한 문자'
문자의 길이 제한은 따로 없기 때문에 이론상 //와 \n 사이에 긴 문자열이 오더라도 문제 요구사항에 부합한다고 판단했습니다.
사실 이에 대해선 고민을 많이 해봤는데 이와 같은 판단을 내린 이유는 제 경험에서 있었습니다.
저는 군대에서 프로젝트 개발 중 방화벽 로그 패킷의 여러 정보를 추출해서 String으로 만드는 작업이 있었는데
이때 구분자로써 거의 사용되지 않는 문자인 $를 사용했습니다.
하지만 가끔 어떤 패킷은 URI나 탐지 문자열 등에서 $가 포함된 정보가 있었고 버그가 난 적이 있었죠.
그렇다고 다른 특수문자를 쓴다고 해결될 문제는 아니었습니다.
어떤 특수문자도 패킷에 포함될 가능성은 있었고 그렇게 된다면 구분자로서의 역할을 할 수 없기 때문이죠.
따라서 저는 절대 로그 패킷에 있을 수 없는 구분자를 생각해야만 했습니다.
그 결과 특정 부분에 한해서 구분자를 저의 이름을 영어로 변환시켜 '영어 문자열'로 사용했고 그 결과 버그는 아직까지 없었습니다.
마치 암호화랑 비슷한 것 같습니다. 한 자리 암호는 경우의 수가 두 자리밖에 안 되지만 다섯 자리의 조합만 해도 경우의 수가 몇 십억이 되니까 말이죠.
따라서 문제 요구 사항에도 한 자리라고 명시되어 있지 않음과 더불어 경험상으로
커스텀 구분자는 한 자리가 아니더라도 에러가 아니라고 판단했습니다.
2. 그렇다면 \n도 구분자가 될 수 있는가
NO > //\n\n이 되는 순간 먼저 나온 \n에서 커스텀 구분자 지정이 끝나기 때문에 뒤에 있는 \n은 숫자가 아니라 [자료형 예외 처리 1]에서 ERROR
3. 문자열이 '//\n1,2,3'과 같은 경우, 커스텀 구분자 없이 continue인가 아니면 커스텀 문자열 미지정으로 error인가
ERROR > 이 부분은 고민이 조금 되었는데 제 판단은 다음과 같습니다.
커스텀 구분자를 지정하려는 의도가 있었지만 커스텀 구분자가 없기 때문에 사용자의 실수일 확률이 높습니다. (의도성이 분명하기 때문에)
따라서 그대로 실행된다면 에러라고 판단하고 ERROR 예외 처리를 따로 해주는 것이 맞다고 생각합니다.
4. 모든 문자가 커스텀 구분자가 될 수 있다면 공백도 커스텀 구분자가 될 수 있는가
NO > 코드 로직 상이라면 '숫자를 제외한' 문자들은 전부 커스텀 구분자로 지정할 수 있기에 공백도 커스텀 구분자로 들어갈 것입니다.
하지만 공백은 특수한 케이스로 커스텀 문자열을 지정하다가 앞뒤로 들어갈 수도 있고
개발자의 코드 관점이 아닌 UX적인 관점에서 공백은 단순히 형체가 없는 띄어쓰기 정도의 수준이기 때문에 구분자로서 지정할 수 없다고 생각했습니다.
5. 모든 문자가 커스텀 구분자가 될 수 있다면 한글이나 이모지, 다른 언어 등도 커스텀 구분자가 될 수 있는가
YES > 한글이나 이모지도 문자로서 커스텀 구분자가 안 될 이유가 없기 때문에 일관성에 의하면 가능하다고 생각했습니다.
이렇게 어느 정도 예외처리에 대한 기준이 확립되었고 코드로 작성해 보기 시작했습니다.
### 예외처리 Coding
우선 해당 로직을 바로 작성하기엔 메인 코드부터 작성해야 하기 때문에
에러 메시지를 저장할 상수 파일을 먼저 만들었습니다.

그러고 나서보니 이 프로그램에서의 또 다른 메시지인 안내 메시지까지 한 번에 만들어주었습니다.

분명 나중에 또 수정될 테지만 우선 여기까지 완성하였고
커밋 컨벤션을 또 한참 여러 가지 찾아보며 힘겹게 커밋까지 완료했습니다.

### 마무리
이렇게 프리코스 첫 미션의 첫날이 마무리되었습니다.
남은 시간 동안은 위에 언급한 Airbnb JS Style Guide글을 작성하였고
오늘 위의 회고 글도 작성하며 하루가 끝났습니다.

# 10/15 (수)
## 본격적인 로직 작성
App.js를 작성할 시간이 왔습니다.
우선 목표는 함수 분리 없이 로직대로 쭉 작성해 본 다음에
기능별로 혹은 필요에 의해 함수로 쪼개는 방식으로 진행해 보기로 했습니다.
처음부터 큰 설계는 자신(실력)이 없기에..
다시 구현 기능 목록을 보면 이렇습니다.

### 1. 시작 문구 출력
우선 처음 기능 명세에 적었던 Console.print() 함수는 필요가 없었습니다.

입력받을 때 사용할 readLineAsync 파라미터에 출력 메시지를 받기 때문이죠.
따라서 기능 명세를 다음과 같이 수정했습니다.

const INPUT_STRING = await Console.readLineAsync(INFORMATION_MESSAGE.START);
코드로는 다음과 같이 일단 작성하였습니다.
### 2. 문자열에 커스텀 구분자가 존재한다면 찾기

커스텀 구분자를 찾기 위해선 정규표현식을 써야 하는데
LLM을 쓰고 싶은 마음이 굴뚝같았지만 세미 러다이트단의 자아가 용납하지 않았기 때문에
정규표현식을 구글에 검색해서 작성하기 시작했습니다.
참고로 정규표현식은 과거 프로젝트에서 회원가입 시 validation 용도와 SQLD 자격증 취득 시에 공부한 적이 이미 있었습니다 :)
처음 작성한 정규식은 다음과 같았습니다. 해당 링크의 글을 참고했습니다!
const FIND_CUSTOM_SEPERATOR_REGEX = /^\/\/(something want to find)\\n/;
1. /^ : 커스텀 구분자는 가장 앞부분에 와야 하므로 해당 식을 첫 조건으로 넣고
2. \ / \ / : //로 시작해야 하므로 특수기호인 /를 쓰기 위해 \ / \ /를 넣어줍니다.
3. (something want to find) : 이 부분에 찾고자 하는 구분자를 찾는 식을 나중에 넣어주고
4. \ \n / : 맨 뒤에는 \n으로 끝나야 하기 때문에 \\n과 정규식 끝을 의미하는 /를 넣어줍니다.
이제 something want to find에 숫자와 공백이 아닌 모든 문자가 올 수 있게 해 주면 됐죠.
하지만 여기서 잠깐 생각을 했습니다.
정규식으로 모든 예외 처리를 적용해 버리면 각 케이스마다 다른 예외 처리 메시지를 던질 수 없게 될 것입니다.
보통의 '서비스 개발'이라면 정규식으로의 조건 설정은 가장 확실한 방법이기 때문에 정규식으로 모두 해결할 테지만
우선 이 프리코스 과정의 목적상 각 케이스에 대한 예외 메세지를 따로 출력해 주고 싶어
정규식에서는 따로 조건을 걸지 않기로 결정했습니다. (주관적 판단)
그래서 괄호 안 부분을 한 글자 이상 모든 문자열을 의미하는 .*를 넣었습니다.
그리곤 생각보다 금방 작성해서 뿌듯해하고 있었지만.. 아니었습니다.
실제로 테스트케이스를 돌려보니 안 되는 케이스가 있어서 검색을 좀 해봤는데
.*만 쓰게 될 경우 그 뒤의 조건 전까지의 가장 길게 일치하는 문자열을 찾는다'라고 합니다.
즉
//\nabc\n1,2,3
이런 입력 문자열에 대해서 해당 정규식은
//\nabc
를 커스텀 구분자로 인식하게 되는 거였죠.
따라서 [구분자 예외 처리 1]은 정규식에서 걸러야 하는 항목이었습니다.

'분명히 조금만 더 추가하면 가장 먼저 나오는 \n에서 거를 수 있을 텐데..'
를 생각하며 조금 더 찾아본 결과 .* 대신 .*?를 쓰면 된다는 사실을 알게 되었습니다.
이 둘의 관계는 이미 널리 사용되고 있었습니다. (해당 링크 참고)
.*는 Greedy 모드라고 하여 최대한 길게 찾으려고 하는 식이고
.*? 는 Lazy(Non-Greedy) 모드라고 하여 가장 짧게 찾으려고 하는 식이라고 했습니다.
즉, 가장 먼저 \n을 만나는 순간 return 하므로 제가 찾고 있던 식이었죠.
따라서 최종 정규식은 다음과 같이 나왔습니다.
const FIND_CUSTOM_SEPERATOR_REGEX = /^\/\/(.*?)\\n/;
### 3. 구분자(커스텀 구분자 포함)로 구분하여 숫자 배열 만들기

다음 해야 할 일은 \n을 포함한 앞부분을 잘라낸 새로운 문자열을 만드는 것이었습니다.
로직 자체는 간단했습니다.
1. 만약 커스텀 구분자가 있다면
2. 구분자 리스트에 커스텀 구분자를 추가하고
3. 커스텀 구분자의 길이를 구한 뒤
4. 기존 문자열의 앞에서부터 커스텀 구분자의 길이+4만큼 잘라주기
5. 그리고 만들어진 해당 문자열에서 구분자 리스트의 구분자들로 split 하여 Number형으로 변환하기
로직의 흐름에 따라 우선 코드를 직렬적으로 작성된 전체 코드는 다음과 같습니다.
class App {
async run() {
try {
let seperatorList = [',', ':'];
const INPUT_STRING = await Console.readLineAsync(INFORMATION_MESSAGE.START);
const FIND_CUSTOM_SEPERATOR_REGEX = /^\/\/(.*?)\\n/;
const CUSTOM_SEPERATOR = INPUT_STRING.match(FIND_CUSTOM_SEPERATOR_REGEX)?.[1];
let originalString = INPUT_STRING;
if (CUSTOM_SEPERATOR) {
const CUSTOM_SEPERATOR_LENGTH = CUSTOM_SEPERATOR.length;
seperatorList.push(CUSTOM_SEPERATOR);
originalString = INPUT_STRING.slice(CUSTOM_SEPERATOR_LENGTH + 4);
}
const JOINED_SEPERATOR = seperatorList.join('');
const SEPERATOR_SPLIT_REGEX = new RegExp(`[${JOINED_SEPERATOR}]`);
const SPLIT_LIST = originalString.split(SEPERATOR_SPLIT_REGEX);
Console.print(SPLIT_LIST);
} catch (error) {
throw error;
}
}
}
적으면서 가장 힘들었던 건 변수명 생각하기.
힘들게 생각해서 작성했는데도 지저분하고 일관성이 없는 느낌이 들어 나중에 다시 다듬어야 할 것 같습니다.
그리고 new RegExp()를 사용해서 배열의 구분자들을 리터럴 문법으로 정규표현식을 완성했습니다.
테스트 결과 원하는 대로 동작은 하였으며 추후에 훨씬 더 다듬게 될 것 같지만 순조롭게 미션이 진행됨에 안도감을 느꼈습니다.
(당연함. 1주 차임.)
### 마무리
그렇게 오늘 할당된 프리코스 시간이 끝이 났습니다.
오늘도 일정 시간 동안 회고글을 작성하였고 커뮤니티 활동도 하며 시간을 알차게 쓸 수 있었습니다.

# 10/16 (목)
## 들어가기 앞서
저는 미션 활동 전에 먼저 디스코드 커뮤니티를 훑어봅니다.
어떤 소재에 대해 사람들이 얘기하고 있나, 혹은 도움이 될만한 정보가 있나 탐색하는 용도로 말이죠.
대부분이 백엔드 지원자라 그런지 백엔드에 대한 글이 상당히 많았습니다.
그중에서 한 분이 올려준 FE 관련 토론글을 봤습니다.

글을 읽어보니 우테코에서 제공하는 라이브러리에 오류가 있는 것 같다는 질문 글이었고
오랜만의 프론트 글이라 그런지, 완벽할 것만 같았던 우테코 라이브러리의 오류 지적 글이라 그런지 큰 흥미가 생겼습니다.
실제로 테스트해보니 작성자분의 말이 맞았고 '범위 내 랜덤 숫자를 골라주는 함수'에 소수점이 들어가게 되면 '에러'가 아닌 '의도하지 않는 결과'가 나오게 되었습니다.
요즘 미션을 할 수 없는 시간엔 '자바스크립트 딥다이브' 책(두께=둔기로 사용 가능)을 읽고 있는데
거기에서 '예외처리'에 대한 챕터에 비슷한 내용이 있었습니다.
JS가 '오류'를 알려주면 그나마 다행인 건데
만약 오류 없이 잘 돌아가지만 의도하지 않는 결과가 나오게 된다면 발견하기 힘들어 최악이라는 내용이었죠.
이 경우가 그런 경우인 것 같았습니다.
인자에 소수점을 넣었을 때 오류는 나지 않지만 의도하지 않은 결과가 나오니까요.
### 의견 제시
크게 흥미를 느낀 저는 해결법을 찾아보기 시작합니다.
작성자분의 말대로 end 인자에 대해선 내림 처리를 하는 게 맞을 것 같았습니다.
하지만 그렇게 되면 4.1, 4.9는 각각 5,4가 되어버리고 validateRange는 이미 지나왔기에 특수 case로서 별도의 예외 처리를 해줘야 했습니다.
따라서
if (startInclusive > endInclusive) {
throw new Error(
정수 범위의 시작 값 (${startInclusive})과 끝 값 (${endInclusive}) 사이에 해당하는 정수가 없습니다.
);
}
이 부분을 추가해 줬고 자체적으로 테스트를 해보니 원하는 대로 동작하는 것 같아 댓글로 의견을 남겼습니다.

지금 다시 보니 Error 메시지 앞에 'Invalid Value: '등의 에러 종류까지 붙였으면 더 좋았을 것 같네요.
## 미션을 진행하며 겪은 점
직전에 커스텀 구분자 분리와, 구분자를 기준으로 split 한 배열까지 추출했습니다.

그 후의 진행 단계는 Number형으로 변환 후 값을 더하면 되는데..
그전에 예외 처리나 값 검증을 할 시간이 왔습니다.
App 안에 수많은 예외 처리 코드를 전부 넣기엔 가독성이 떨어질 것이 확실했기에
예외 처리 로직을 위한 파일은 바로 따로 만들기로 결정했습니다.
### 코드를 짜며 마주친 새로운 이슈들
1. 정규표현식의 두 가지 사용법
JS에서 정규표현식을 사용할 수 있는 방법은 찾아보니 크게 두 가지였습니다.
- 정규식.test(문자열)
- 문자열.match(정규식)
- 가장 큰 차이점은
test는 반환 값이 Boolean이라 올바른지 여부 등의 검사에 쓰이고
match는 반환 값이 Array or null이라 특정 패턴을 추출할 때 쓰인다고 합니다.
따라서 is..로 시작하는 true/false 검출 목적의 정규식은 test로,
커스텀 구분자를 추출하는 것 같은 특정 값의 추출 목적의 정규식은 match로 작성하였습니다.
2. 변수(상수)는 어디에서 생성과 관리?
코드를 짜고 변수를 만들다 보니 어떤 때 class에서 쓰는 this.변수를 쓰고
어떤 때 const로 만들어서 쓸지에 대한 고민이 생겼습니다.
검색해 보니 보통 scope를 기준으로 해당 변수가 인스턴스 객체의 속성으로 사용될 것 같으면 this.변수로 쓰고
특정 로직에만 사용되는 (임시) 변수 느낌이면 그때마다 만들어서 범위 안에서만 사용하면 될 것 같았습니다.
3. 커스텀 구분자가 연속해서 나온다면?
//!\n12!12!!12 이 케이스는 어떻게 되어야 할까요?
커스텀 구분자인 !를 기준으로 split만 하면 배열은
[12, 12, '', 12]가 되고 에러가 안 뜨고 빈값을 무시하고 더하게 됩니다.
따라서 [자료형 예외 처리] 부분에 [구분자로 구분된 값이 비어있다면 ERROR] 항목을 추가했습니다.
4. 구분자 예외 처리 항목 중 커스텀 구분자의 여는 마크/닫는 마크만 있는지는 어떻게 구별할까
네.. 정규식밖에 답이 없는데 아무리 생각해도 제 머리로는 너무 어려워서
제미나이 선생님에게 도움을 요청했습니다.
그리고 꼭! 빼먹으면 안 되는 과정인 직접 머리로 디버깅 해보고 정규식 표현식을 이해하는 과정까지 마쳤습니다.
그렇지만,, 다시 생각해 보니
여는 태그는 있는데 닫는 태그가 없다? -> 별도의 오류로 처리해도 됨
여는 태그는 없는데 닫는 태그만 있다? -> 이건 커스텀 구분자 쪽의 오류가 아닌 애초에 문자열 입력에서의 오류라고 판단했습니다.
따라서 최종적으로 예외 처리 부분은 다음과 같이 수정되었습니다.

### 대체 커밋은 어느 타이밍에
과제 요구 사항에 '구현할 기능 목록'을 만들고 '기능 목록 단위'로 커밋을 하라고 합니다.

하지만,, 레고도 아니고 기능 목록 단위로 착착 순서대로 개발이 되는 것도 아니고
얽히고설키고 여기 수정하면 저기 수정하고 여기 없애면 저기 추가하고가 반복되면서
기능 목록대로 개발은 쉽지 않았습니다.
그래서 우선 1차적으로 동작하는 전체 코드를 전부 작성하고
그 후에 코드/파일 별로 나눠서 커밋을 하기로 했습니다. (실전에선 좋지 않은 방법이지만..ㅠㅠ)
## 일단락된 코드
그렇게 폭풍 같이 작성된 코드를 작성하여 일단락되었습니다.
먼저, 폴더 구조는 다음과 같습니다.
src
├── App.js
├── constants.js
├── index.js
├── utils.js
└── validation.js
- App.js : 메인 로직이 돌아가는 파일
- constants.js : 상수 변수, 출력 메시지 등이 저장되는 상수 파일
- index.js : App이 실행되는 파일
- utils.js : App.js에서 필요한 기능 함수를 모아둔 파일 (현재는 주로 정규식 생성 관련 함수)
- validation.js : 예외처리 로직에 대한 함수를 모아둔 파일
코드를 짜며 정말 최소한으로 나눠야 할 것 같은 코드들만 우선 별도 파일로 분리해 주었으며
아마 다듬다 보면 새로운 파일이 생길 수도 있을 것 같습니다.
전체 코드로 한 번 오늘의 성과를 회고해 보겠습니다.
### constants.js
export const INFORMATION_MESSAGE = {
START: `덧셈할 문자열을 입력해 주세요.\n`,
RESULT: `결과 : `,
};
export const ERROR_MESSAGE = {
CUSTOM_SEPERATOR_MUST_BE_CLOSED: '[ERROR] 커스텀 구분자의 닫는 문자열이 필요합니다.',
SEPERATOR_MUST_EXIST: '[ERROR] 구분자는 최소 한 개 이상 있어야 합니다.',
VALUE_MUST_BE_NUMBER: '[ERROR] 구분자를 제외하고 숫자가 아닌 값이 포함되어 있습니다.',
VALUE_MUST_BE_POSITIVE: '[ERROR] 숫자는 양수만 입력되어야 합니다.',
CUSTOM_SEPARATOR_MUST_EXIST: '[ERROR] 커스텀 구분자는 비어있을 수 없습니다.',
CUSTOM_SEPARATOR_MUST_BE_CHARACTER: '[ERROR] 커스텀 구분자는 문자만 입력되어야 합니다.',
};
export const DEFAULT_SEPRATOR = [',', ':'];
export const CONSTANT_CHAR = {
CUSTOM_SEPERATOR_START_MARK: '//',
CUSTOM_SEPARATOR_END_MARK: '\\n',
};
상수 파일을 한 번 보겠습니다.
출력 메시지들을 object로 저장해 놨으며
커스텀 구분자를 위한 START/END_MARK와 기본 구분자를 전역변수처럼 상수 파일에 같이 저장했습니다.
처음엔 메시지들만 저장을 하고 나머지 두 개는 App에서 관리했었는데
작성하다 보니 저 것들도 따로 뺐을 때 유지보수 관점에서 좋다고 생각하여 분리하게 되었습니다.
어려웠던 점은 에러 메시지 key값 작명이었습니다..
작명하는데 굉장히 오래 걸렸고, 나름 일관성 있게 만들었지만 보면 볼수록 지저분한 것 같기도 해서 보기를 관뒀습니다 ㅠ.ㅠ
그리고 나중 가서 '메시지만을 위한 파일'과 '구분자 관련 변수 관련 파일'은 성격이 달라 분리될 것 같습니다.
### utils.js
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function GENERATE_FIND_CUSTOM_SEPERATOR_REGEX(startMark, endMark) {
const ESCAPED_START_MARK = escapeRegExp(startMark);
const ESCAPED_END_MARK = escapeRegExp(endMark);
const REGEX = `^${ESCAPED_START_MARK}(.*?)${ESCAPED_END_MARK}`;
return new RegExp(REGEX);
}
export function GENERATE_FIND_CUSTOM_SEPERATOR_START_MARK_ONLY_REGEX(startMark, endMark) {
const ESCAPED_START_MARK = escapeRegExp(startMark);
const ESCAPED_END_MARK = escapeRegExp(endMark);
const REGEX = `^${ESCAPED_START_MARK}[^${ESCAPED_END_MARK}]*$`;
return new RegExp(REGEX);
}
export function GENERATE_SEPERATOR_SPLIT_REGEX(seperatorList) {
const JOINED_SEPERATOR = seperatorList.join('');
return new RegExp(`[${JOINED_SEPERATOR}]`);
}
utils.js엔 의도치 않게 정규식 관련 함수만 있게 되었습니다.
GENERATE로 시작하는 함수들은 각 기능에 필요한 정규식을 '생성'하여 RegExp 형태의 정규식을 return 해줍니다.
특히 //같이 특수기호는 정규식에서 쓰일 때 \ / \ / 와 같이 백슬래쉬를 붙여줘야 하기 때문에
상단에 escapeRegExp를 통해서 convert 해주었습니다.
(escapeRegExp의 정규식은 제미나이의 도움을 받았습니다.)
그리고 GENERATE_FIND_CUSTOM_SEPARATOR_REGEX 함수와 GENERATE_FIND_CUSTOM_SEPARATOR_START_MARK_ONLY_REGEX 함수가
받는 인자부터, 내부 로직까지 정규식 표현 부분을 제외한 모든 부분이 동일하여 중복 코드가 되었습니다.
지금 당장은 어떻게 깔끔하게 재사용할지 생각이 잘 안 나서 나중에 리팩토링 하기로 하고 넘기게 되었습니다.
### validation.js
import { GENERATE_FIND_CUSTOM_SEPERATOR_START_MARK_ONLY_REGEX } from './utils.js';
import { CONSTANT_CHAR } from './constants.js';
export function isInputExist(inputString) {
return inputString.length > 0;
}
export function isValueNumber(numberList) {
return numberList.every((value) => value !== '' && !isNaN(Number(value)));
}
export function isValuePositiveNumber(numberList) {
return numberList.every((value) => Number(value) > 0);
}
export function hasInputSeparator(inputString, seperatorList) {
return seperatorList.some((seperator) => inputString.includes(seperator));
}
export function isCustomSeperatorExist(customSeperator) {
return customSeperator?.length > 0;
}
export function isCustomSeperatorChar(customSeperator) {
return Number.isNaN(customSeperator);
}
export function hasInputCustomSeparatorStartMarkOnly(inputString) {
const REGEX = GENERATE_FIND_CUSTOM_SEPERATOR_START_MARK_ONLY_REGEX(
CONSTANT_CHAR.CUSTOM_SEPERATOR_START_MARK,
CONSTANT_CHAR.CUSTOM_SEPARATOR_END_MARK
);
return REGEX.test(inputString);
}
예외처리와 값 검증을 위해 주로 true/false를 반환하는 함수들이 있는 파일입니다.
특별한 건 없었고 해당 코드에서
export function isValueNumber(numberList) {
return numberList.every((value) => value !== '' && !isNaN(Number(value)));
}
이 부분의
!isNaN(Number(value))
코드는 추후에
!isNaN(value)
으로 바뀌었는데 이유는..
디스코드 커뮤니티에서 마침 김용주 님께서 올려주신 글이 참고가 되었습니다.


요약하자면 isNaN()은 인자값에 자체적으로 Number() 타입 캐스팅을 하기 때문에
제가 별도로 Number 타입 캐스팅을 해줄 필요가 없었습니다!
우테코가 말한 '경쟁자와 함께 성장하라고요?'가 실현된 경험이랄까요.
### App.js
import { Console } from '@woowacourse/mission-utils';
import { INFORMATION_MESSAGE, ERROR_MESSAGE, DEFAULT_SEPRATOR, CONSTANT_CHAR } from './constants.js';
import { GENERATE_FIND_CUSTOM_SEPERATOR_REGEX, GENERATE_SEPERATOR_SPLIT_REGEX } from './utils.js';
import {
isInputExist,
isValueNumber,
isValuePositiveNumber,
hasInputSeparator,
isCustomSeperatorExist,
isCustomSeperatorChar,
hasInputCustomSeparatorStartMarkOnly,
} from './validation.js';
class App {
constructor() {
this.input = '';
this.seperatorList = DEFAULT_SEPRATOR;
this.sum = 0;
}
async run() {
try {
await this.enterInput();
this.main();
this.printResult();
} catch (error) {
Console.print(error);
}
}
async enterInput() {
this.input = await Console.readLineAsync(INFORMATION_MESSAGE.START);
}
main() {
//공백만 입력했을 때도 0이 출력 되게
this.input = this.input.trim();
// 입력 문자열 예외처리 1
if (!isInputExist(this.input)) return;
this.findCustomSeperator();
this.validateInputString();
const NUMBER_LIST = this.getNumberList();
this.sum = NUMBER_LIST.reduce((acc, cur) => acc + cur, 0);
}
printResult() {
Console.print(`${INFORMATION_MESSAGE.RESULT}${this.sum}`);
}
findCustomSeperator() {
const CUSTOM_SEPERATOR_INFORMATION = this.findCustomSeperatorMark();
if (CUSTOM_SEPERATOR_INFORMATION) {
// 구분자 예외처리 3 (trim)
const CUSTOM_SEPERATOR = CUSTOM_SEPERATOR_INFORMATION[1]?.trim();
this.validateCustomSeperator(CUSTOM_SEPERATOR);
this.seperatorList.push(CUSTOM_SEPERATOR);
this.input = this.input.slice(CUSTOM_SEPERATOR.length + 4);
}
}
findCustomSeperatorMark() {
const FIND_CUSTOM_SEPERATOR_REGEX = GENERATE_FIND_CUSTOM_SEPERATOR_REGEX(
CONSTANT_CHAR.CUSTOM_SEPERATOR_START_MARK,
CONSTANT_CHAR.CUSTOM_SEPARATOR_END_MARK
);
return this.input.match(FIND_CUSTOM_SEPERATOR_REGEX);
}
getNumberList() {
const SPLIT_LIST = this.seperateInputString();
this.validateType(SPLIT_LIST);
return SPLIT_LIST.map(Number);
}
seperateInputString() {
const SEPERATOR_SPLIT_REGEX = GENERATE_SEPERATOR_SPLIT_REGEX(this.seperatorList);
const SPLIT_LIST = this.input.split(SEPERATOR_SPLIT_REGEX);
return SPLIT_LIST.map((value) => value.trim());
}
validateCustomSeperator(customSeperator) {
// 구분자 예외처리 1
if (!isCustomSeperatorExist(customSeperator)) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPARATOR_MUST_EXIST);
}
// 구분자 예외처리 2
if (!isCustomSeperatorChar(customSeperator)) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPARATOR_MUST_BE_CHARACTER);
}
}
validateInputString() {
// 입력 문자열 예외처리 2
if (
this.input.startsWith(CONSTANT_CHAR.CUSTOM_SEPERATOR_START_MARK) &&
hasInputCustomSeparatorStartMarkOnly(this.input)
) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPERATOR_MUST_BE_CLOSED);
}
// 입력 문자열 예외처리 3
if (!hasInputSeparator(this.input, this.seperatorList)) {
throw new Error(ERROR_MESSAGE.SEPERATOR_MUST_EXIST);
}
}
validateType(numberList) {
// 자료형 예외처리 1 && 2
if (!isValueNumber(numberList)) {
throw new Error(ERROR_MESSAGE.VALUE_MUST_BE_NUMBER);
}
// 자료형 예외처리 3
if (!isValuePositiveNumber(numberList)) {
throw new Error(ERROR_MESSAGE.VALUE_MUST_BE_POSITIVE);
}
}
}
export default App;
네.. 코드가 좀 길어졌습니다..
길어질 수밖에 없는 이유는 아직 코드 분리 없이 App.js에 전부 넣었기 때문입니다.
제가 눈으로 흐름을 따라가는데도 위아래로 계속 왔다 갔다 하게 되더라고요.
정리를 좀 하자면 큰 흐름은 다음과 같습니다.
App {
문자열 입력
main()
결과 출력
}
main() {
입력값이 없을 때 0 출력
커스텀 구분자 찾기()
문자열 검증하기()
숫자 배열 얻기()
sum에 숫자들 더하기
}
/* -- 커스텀 구분자 관련 -- */
커스텀 구분자 찾기() {
커스텀 구분자 마크 있는지 찾기()
커스텀 구분자 마크 있다면:
커스텀 구분자 추출
커스텀 구분자 검증(예외처리)
커스텀 구분자를 구분자 리스트에 추가
문자열을 \n 전까지 자르기
}
커스텀 구분자 마크 있는지 찾기() {
커스텀 구분자 찾는 정규식 가져오기
정규식 사용해서 커스텀 구분자 return (없으면 null)
}
/* -- 숫자 배열 얻기 관련 -- */
숫자 배열 얻기() {
구분자로 구분하여 배열 얻기()
얻은 배열값 검증(예외처리)
숫자로 변환해서 배열 return
}
구분자로 구분하여 배열 얻기() {
구분자로 구분하는 정규식 가져오기
정규식 사용해서 구분자로 구분된 배열 return
}
/* -- 예외처리 검증 관련 -- */
구분자 예외처리()
입력 문자열 예외처리()
자료형 예외처리()
어우.. 이렇게 정리해도 가독성이 최악이네요..
아무튼 코드를 돌렸을 때 대부분의 케이스에서 자체 error가 아닌
제가 지정한 [ERROR]가 잘 나오는 것을 확인할 수 있었습니다.
일단락.. 이죠..
## 마치며
본격적으로 코드를 작성해 본 날이었습니다.
1주 차 코드,, 심지어 처음엔 굉장히 쉬워 보였던 만 같았던 코드가
저의 성격 때문인지 아니면 우테코가 의도한 대로인지 생각보다 더 길어지고
더 꼼꼼하게 예외처리를 하게 되었습니다.

# 10/17 (금)
## 우테코 8기 프리코스 Helper
미션을 계속 진행하다 보니 불편한 점이 생겼습니다.
새로 접한 컨벤션과 우테코 미션에 필요한 여러 정보를 열람하기 위해 여러 창을 계속 왔다 갔다 하게 되는 것이었죠.
심지어는 우테코 깃헙 과제 가이드 창을 방금 다 보고 나서 닫은 후
다시 한참 그 가이드 창을 찾아서 연 적도 있었습니다.
그러다 보니 북마크의 역할도 하며 프리코스 진행 기간 동안 사용할 웹사이트를 만들어볼까 생각이 들었고
2시간 정도 걸려서 바이브 코딩으로 완성하였습니다.
그리고 간단하게 배포까지 하여 커뮤니티에 올렸더니 많은 분들이 유용하다고 칭찬해 주셔서 뿌듯했습니다.
이와 관련된 글은 이곳을 참고해 주세요!

## 스터디
커뮤니티에서 프론트엔드 스터디를 구한다는 글이 있어 연락을 드렸습니다.
총 5명의 멤버로 스터디 그룹이 결성되어 금일 짧게 온보딩 회의를 하였고
자기소개, 목표 등을 공유하면서 정기적인 스터디 날짜도 잡게 되었습니다.
이런 활동에서 다 같이 공유하고 같이 공부하는 것은 필수적이라고 생각합니다.
혼자 하다 보면 분명히 본인만의 세상에 빠지고, 자신의 생각과 로직을 검증해 줄 제삼자가 필요하다고 생각하기 때문이죠.
또한 본인이 아무리 공부를 하더라도 절대 얻을 수 없는, 다른 사람의 사고의 흐름이나 문법 활용, 코드 작성 구조 등을
공유하면서 공부할 수 있다는 점은 굉장히 큰 이점이라고 생각합니다.

팡일님께서 깔끔하게 노션 페이지도 만들어주셔서 눈물을 줄줄 흘리며 전보다 더더 적극적으로 참여해야겠다는 다짐을 했습니다!
## 그리고..
그리고 시간이 많이 없어 상수 파일을 더 자세하게 분리하고
변수 이름을 바꾸는 등 사소한 업데이트를 진행 후 마무리하였습니다.
아마 커뮤니티 활동을 지속적으로 하다 보니 과제를 할 시간이 부족했던 것 같은데
앞으로는 휴대폰으로 틈틈이 커뮤니티 활동을 하고 과제를 할 수 있는 시간엔 과제에 집중하도록 해야 할 것 같습니다.

# 10/18 (토) ~ 10/19 (일)
## 완성을 향해
주말엔 개인적인 하루 종일 일정이 있어 거의 코드를 작성하지 못했지만
일정 도중에 틈틈이 예외 테스트케이스를 휴대폰에 따로 적어놓으며 계속해서 과제를 수행했습니다.
그 외에도 validate 함수들이 메인 로직에 같이 있으면 안 될 것 같아
validate 폴더를 따로 만들어 검증 로직은 전부 모아두어 export 하도록 구조를 변경했습니다.
이렇게 리팩토링을 계속 진행하다 보니 프로젝트 구조도 계속 바뀌고 분리 기준이 명확하지 않아 조금 힘들었습니다.
MVC 패턴이니 무슨 패턴이니 이런 패턴법을 처음부터 고려해서 설계를 했다면 조금 더 수월했을 것 같지만
한 파일에 전부 적고 그 뒤에 나누려니까 계속해서 기준이 바뀌는 듯한 느낌이 들었죠.
2주 차부터는 새로운 패턴이나 구조들을 더 공부해 보고 처음부터 설계에 포함시켜 적용해 보도록 하면 좋을 것 같습니다.
그리고 테스팅에 관련된 개념도 학습했으니 TDD 주도 방식에 대해서도 2주 차에 적용해 보면 좋을 것 같습니다.
## ApplicationTest.js
처음에 우테코가 제공한 테스트 파일엔 2개의 테스트 케이스밖에 없었지만
실제 개발을 하면서 여러 테스트 케이스를 계속해서 검사할 필요가 있었습니다.
이번 미션의 중점은 과제에서 주어진 기본적인 요구사항을 달성했냐 보다는
예외처리를 얼마나 잘했냐, 어떤 기준에 따라서 했냐가 중점이라고 생각했으니까요.
따라서 여러 테스트케이스를 적다 보니 자명하게 중복코드가 많이 생겼고
저는 이것을 JS 문법을 통해서 배열화 시켰습니다.
const successCases = [
{ inputs: ['1,2,3'], output: '결과 : 6', name: '기본 구분자(,)로 합산' },
{ inputs: ['4:5:6'], output: '결과 : 15', name: '기본 구분자(:)로 합산' },
{ inputs: ['1,2:3'], output: '결과 : 6', name: '기본 구분자 혼합' },
{ inputs: [' '], output: '결과 : 0', name: '공백 입력시 0 반환' },
{ inputs: [''], output: '결과 : 0', name: '공백 입력시 0 반환' },
{ inputs: ['7'], output: '결과 : 7', name: '숫자 하나만 입력' },
{ inputs: ['0.4,0.6,0.25'], output: '결과 : 1.25', name: '소수점 입력' },
{ inputs: [' 1 , 2 : 3 '], output: '결과 : 6', name: '숫자 사이에 공백 포함' },
{ inputs: ['//;\\n1;2;3'], output: '결과 : 6', name: '커스텀 구분자 사용' },
{ inputs: ['//;\\n1;2,3:4'], output: '결과 : 10', name: '커스텀 구분자와 기본 구분자 혼합' },
{ inputs: ['//a\\n1a2a3'], output: '결과 : 6', name: '한 글자 커스텀 구분자(문자만)' },
{ inputs: ['//abc\\n1abc2abc3'], output: '결과 : 6', name: '여러 글자 커스텀 구분자(문자만)' },
{ inputs: ['//+=\!\\n1+=\!2+=\!3'], output: '결과 : 6', name: '커스텀 구분자 특수문자' },
{ inputs: ['//\\\\\\n1\\\\2\\\\3'], output: '결과 : 6', name: '커스텀 구분자 역슬래시' },
{ inputs: ['123'], output: '결과 : 123', name: '구분자 없이 여러 자리수' },
{ inputs: ['1.23'], output: '결과 : 1.23', name: '구분자 없이 소수점' },
];
successCases.forEach(({ inputs, output, name }) => {
test(name, async () => {
mockQuestions([...inputs]);
await app.run();
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});
이렇게 하니 훨씬 보기도 쉽고 테스트케이스 추가하기도 쉬웠습니다.
다만 지금까지 코드를 짠 방식대로라면 저 successCases라는 상수도 따로 파일에 관리하는 게 맞지만
테스트 파일은 하나로 유지하고 싶었기 때문에 따로 분리하지 않고 한 파일에서 관리하도록 진행했습니다.


# 10/20 (월)
## 최종 코드 완성
최종적으로 완성된 코드의 폴더 구조는 다음과 같습니다.

### __tests_
위에서 언급한 대로 test 파일은 더 상세하게 분리할 수도 있었지만 시간 관계상,
그리고 메인 로직에 더 초점을 맞추기 위해 한 파일로 작성하였습니다.
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 getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, 'print');
logSpy.mockClear();
return logSpy;
};
describe('문자열 계산기', () => {
let logSpy;
let app;
beforeEach(() => {
logSpy = getLogSpy();
app = new App();
});
const successCases = [
{ inputs: ['1,2,3'], output: '결과 : 6', name: '기본 구분자(,)로 합산' },
{ inputs: ['4:5:6'], output: '결과 : 15', name: '기본 구분자(:)로 합산' },
{ inputs: ['1,2:3'], output: '결과 : 6', name: '기본 구분자 혼합' },
{ inputs: [' '], output: '결과 : 0', name: '공백 입력시 0 반환' },
{ inputs: [''], output: '결과 : 0', name: '공백 입력시 0 반환' },
{ inputs: ['7'], output: '결과 : 7', name: '숫자 하나만 입력' },
{ inputs: ['0.4,0.6,0.25'], output: '결과 : 1.25', name: '소수점 입력' },
{ inputs: [' 1 , 2 : 3 '], output: '결과 : 6', name: '숫자 사이에 공백 포함' },
{ inputs: ['//;\\n1;2;3'], output: '결과 : 6', name: '커스텀 구분자 사용' },
{ inputs: ['//;\\n1;2,3:4'], output: '결과 : 10', name: '커스텀 구분자와 기본 구분자 혼합' },
{ inputs: ['//a\\n1a2a3'], output: '결과 : 6', name: '한 글자 커스텀 구분자(문자만)' },
{ inputs: ['//abc\\n1abc2abc3'], output: '결과 : 6', name: '여러 글자 커스텀 구분자(문자만)' },
{ inputs: ['//+=\!\\n1+=\!2+=\!3'], output: '결과 : 6', name: '커스텀 구분자 특수문자' },
{ inputs: ['//\\\\\\n1\\\\2\\\\3'], output: '결과 : 6', name: '커스텀 구분자 역슬래시' },
{ inputs: ['123'], output: '결과 : 123', name: '구분자 없이 여러 자리수' },
{ inputs: ['1.23'], output: '결과 : 1.23', name: '구분자 없이 소수점' },
];
successCases.forEach(({ inputs, output, name }) => {
test(name, async () => {
mockQuestions([...inputs]);
await app.run();
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});
const errorCases = [
{
inputs: ['//1\\n1,2,3'],
name: '커스텀 구분자에 숫자 사용시 예외',
message: '[ERROR] 커스텀 구분자는 문자만 입력되어야 합니다.',
},
{
inputs: ['//;1;2;3'],
name: '커스텀 구분자 미닫힘 예외',
message: '[ERROR] 커스텀 구분자의 닫는 문자열이 필요합니다.',
},
{
inputs: ['//\\n1,2,3'],
name: '커스텀 구분자 비어있음 예외',
message: '[ERROR] 커스텀 구분자는 비어있을 수 없습니다.',
},
{
inputs: ['// \\n'],
name: '커스텀 구분자 비어있음 예외',
message: '[ERROR] 커스텀 구분자는 비어있을 수 없습니다.',
},
{
inputs: ['//\\n\\n1\\n2\\n3'],
name: '커스텀 구분자에 이스케이프 문자(\\n) 포함',
message: '[ERROR] 커스텀 구분자는 비어있을 수 없습니다.',
},
{
inputs: ['1,2,a'],
name: '숫자가 아닌 값 포함 예외',
message: '[ERROR] 잘못된 입력값입니다.',
},
{ inputs: ['-1,2,3'], name: '음수 포함 예외', message: '[ERROR] 숫자는 양수만 입력되어야 합니다.' },
{ inputs: ['0,1,2'], name: '0 포함 예외', message: '[ERROR] 숫자는 양수만 입력되어야 합니다.' },
{
inputs: ['//a1\\n1a12a13'],
name: '커스텀 구분자에 숫자 포함',
message: '[ERROR] 커스텀 구분자는 문자만 입력되어야 합니다.',
},
{
inputs: ['1,,2::3'],
name: '구분자 연속',
message: '[ERROR] 잘못된 입력값입니다.',
},
{
inputs: [',,,'],
name: '구분자만 입력',
message: '[ERROR] 잘못된 입력값입니다.',
},
{
inputs: ['1,2,3,'],
name: '구분자로 끝남',
message: '[ERROR] 잘못된 입력값입니다.',
},
{
inputs: [',1,2,3'],
name: '구분자로 시작',
message: '[ERROR] 잘못된 입력값입니다.',
},
{
inputs: ['a,b,c'],
name: '숫자 아닌 문자만 입력',
message: '[ERROR] 잘못된 입력값입니다.',
},
];
errorCases.forEach(({ inputs, name, message }) => {
test(name, async () => {
mockQuestions([...inputs]);
await expect(app.run()).rejects.toThrow(message);
});
});
});
### src/calculator
1. Calculator.js
App에서 호출되는 Calculator class가 선언되는 계산기 메인 로직을 담당하는 파일입니다.
import CalculatorParser from './CalculatorParser.js';
import { validateEmptyInput } from '../validate/validators.js';
import { sumNumbers } from '../services/NumberService.js';
class Calculator {
calculate(inputString) {
const trimmedInput = inputString.trim();
if (!validateEmptyInput(trimmedInput)) return 0;
const numberList = CalculatorParser.parse(trimmedInput);
return sumNumbers(numberList);
}
}
export default Calculator;
- 입력값을 인자로 받는다.
- 공백 입력에 대한 edge case 예외처리로 0을 return 한다.
- CalculatorParser를 통해 number가 있는 Array를 추출한다.
- sumNumbers를 통해 최종적으로 숫자를 더한 결괏값을 return 한다.
이 네 가지 기능이 계산기 인스턴스에 꼭 필요하고 분리할 수 없는 로직이라고 생각했습니다.
2. CalculatorParser.js
커스텀 구분자 분리, 숫자 배열 분리 등 input에서 parse 로직을 담당하는 파일입니다.
import { parseCustomSeparator } from '../services/SeparatorService.js';
import { extractNumbers } from '../services/NumberService.js';
export function parse(inputString) {
const { separators, numberString } = parseCustomSeparator(inputString);
const numberList = extractNumbers(numberString, separators);
return numberList;
}
export default { parse };
계산기 로직에 필수적으로 필요하지만 수행하는 기능이 명확하기에 파일을 분리하였습니다.
### src/constants
1. calculatorConfig.js
말 그대로 계산기에 쓰이는 상수 설정 파일의 역할로 다음과 같은 것들로 구성되어 있습니다.
export const SEPARATORS = [',', ':'];
export const CONSTANT_CHAR = {
CUSTOM_SEPARATOR_START_MARK: '//',
CUSTOM_SEPARATOR_END_MARK: '\\n',
};
- 기본 구분자 배열
- 커스텀 문자열 구분을 위한 MARK 객체(//, \n)
2. messages.js
에러 메시지나 안내 메세지 등 Console로 출력되는 상수 메시지들이 담겨있습니다.
export const INFORMATION_MESSAGE = {
START: `덧셈할 문자열을 입력해 주세요.\n`,
RESULT: `결과 : `,
};
export const ERROR_MESSAGE = {
CUSTOM_SEPARATOR_MUST_BE_CLOSED: '[ERROR] 커스텀 구분자의 닫는 문자열이 필요합니다.',
VALUE_MUST_BE_NUMBER: '[ERROR] 잘못된 입력값입니다.',
VALUE_MUST_BE_POSITIVE: '[ERROR] 숫자는 양수만 입력되어야 합니다.',
CUSTOM_SEPARATOR_MUST_EXIST: '[ERROR] 커스텀 구분자는 비어있을 수 없습니다.',
CUSTOM_SEPARATOR_MUST_BE_CHARACTER: '[ERROR] 커스텀 구분자는 문자만 입력되어야 합니다.',
};
3. index.js
위의 두 개의 파일을 모아서 다른 파일에서 import 할 수 있게 해주는 bridge 역할로
calculatorConfig.js와 message.js를 export 해주는 파일입니다.
export * from './messages.js';
export * from './calculatorConfig.js';
다른 파일에서 상수 파일을 import 할 땐 constants/index.js만 공통적으로 import 하면 됩니다.
이렇게 하면 다른 파일에선 상수로 쓰이는 값들이 정확히 어느 파일에 있는지 구분하지 않아도 되고
consants 안에선 용도나 기능 별로 상수 파일을 분리해서 관리할 수 있다는 장점이 있습니다.
### src/services
계산기의 내부 로직을 크게 두 가지로 나누자면
- 커스텀 구분자 관련 추출 혹은 저장
- 더해질 숫자 값 추출 혹은 sum
두 개로 나누어지는데 각각을 수행하는 파일을 services 폴더로 분리하여 계산기에서 import 하여 사용할 수 있게 구조화했습니다.
1. NumberService.js
import { GENERATE_SEPARATOR_SPLIT_REGEX } from '../utils/regexUtils.js';
import { validateNumberTypes } from '../validate/validators.js';
export function extractNumbers(numberString, separators) {
const SEPARATOR_SPLIT_REGEX = GENERATE_SEPARATOR_SPLIT_REGEX(separators);
const splitList = numberString.split(SEPARATOR_SPLIT_REGEX).map((value) => value.trim());
validateNumberTypes(splitList);
return splitList.map(Number);
}
export function sumNumbers(numberList) {
return numberList.reduce((acc, cur) => acc + cur, 0);
}
- extractNumbers(문자열, 구분자): 문자열을 구분자로 분리하여 값을 추출하고 배열로 return 합니다.
- sumNumbers(숫자리스트): 숫자리스트의 값을 reduce를 이용해서 더한 결괏값을 return 합니다.
2. SeparatorService.js
import { SEPARATORS, CONSTANT_CHAR } from '../constants/index.js';
import { GENERATE_FIND_CUSTOM_SEPARATOR_REGEX } from '../utils/regexUtils.js';
import { validateInputString, validateCustomSeparator } from '../validate/validators.js';
export function parseCustomSeparator(input) {
validateInputString(input);
const customSeparatorInfo = findCustomSeparatorMark(input);
if (customSeparatorInfo) {
const fullMatchString = customSeparatorInfo[0];
const customSeparator = customSeparatorInfo[1]?.trim();
validateCustomSeparator(customSeparator);
const remainingInput = input.slice(fullMatchString.length);
return {
separators: [...SEPARATORS, customSeparator],
numberString: remainingInput,
};
}
return { separators: SEPARATORS, numberString: input };
}
export function findCustomSeparatorMark(input) {
const FIND_CUSTOM_SEPARATOR_REGEX = GENERATE_FIND_CUSTOM_SEPARATOR_REGEX(
CONSTANT_CHAR.CUSTOM_SEPARATOR_START_MARK,
CONSTANT_CHAR.CUSTOM_SEPARATOR_END_MARK
);
return input.match(FIND_CUSTOM_SEPARATOR_REGEX);
}
- parseCustomSeparator(입력값): 입력값에서 정규식을 이용해 커스텀 구분자를 분리하여 커스텀 구분자와 그 외의 문자열을 객체로 return 합니다.
- findCustomSeparatorMark(입력값): 입력값에서 커스텀 구분자 Mark(//, \n)를 탐색해 커스텀 구분자가 있는 문자열인지 판단합니다.
### src/utils
1. regexUtils.js
각종 정규식을 동적으로 만들어 반환하는 함수입니다.
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function GENERATE_FIND_CUSTOM_SEPARATOR_REGEX(startMark, endMark) {
const ESCAPED_START_MARK = escapeRegExp(startMark);
const ESCAPED_END_MARK = escapeRegExp(endMark);
const REGEX = `^${ESCAPED_START_MARK}(.*?)${ESCAPED_END_MARK}`;
return new RegExp(REGEX);
}
export function GENERATE_FIND_CUSTOM_SEPARATOR_START_MARK_ONLY_REGEX(startMark, endMark) {
const ESCAPED_START_MARK = escapeRegExp(startMark);
const ESCAPED_END_MARK = escapeRegExp(endMark);
const REGEX = `^${ESCAPED_START_MARK}[^${ESCAPED_END_MARK}]*$`;
return new RegExp(REGEX);
}
export function GENERATE_SEPARATOR_SPLIT_REGEX(SEPARATORList) {
const escapedSeparators = SEPARATORList.map(escapeRegExp);
const REGEX_STRING = escapedSeparators.join('|');
return new RegExp(REGEX_STRING);
}
export function GENERATE_CUSTOM_SEPARATOR_IS_CHAR_REGEX() {
return /^[^0-9]+$/;
}
동적으로 만들어지긴 하지만 정규식의 문법도 상수 파일로 따로 분리했다면 추후 유지보수에 용이했을 것 같습니다.
### src/validate
1. condition.js
주로 validators.js에 필요한 조건들을 boolean 형태로 return 해주는 함수를 모아놓은 파일입니다.
import { CONSTANT_CHAR } from '../constants/index.js';
import {
GENERATE_FIND_CUSTOM_SEPARATOR_START_MARK_ONLY_REGEX,
GENERATE_CUSTOM_SEPARATOR_IS_CHAR_REGEX,
} from '../utils/regexUtils.js';
// 자료형 관련 조건 확인
export function isAllNumber(list) {
return list.every((value) => value !== '' && !isNaN(value));
}
export function isAllPositiveNumber(list) {
return list.every((value) => Number(value) > 0);
}
// 커스텀 구분자 관련 조건 확인
export function hasInputCustomSeparatorStartMarkOnly(input) {
const REGEX = GENERATE_FIND_CUSTOM_SEPARATOR_START_MARK_ONLY_REGEX(
CONSTANT_CHAR.CUSTOM_SEPARATOR_START_MARK,
CONSTANT_CHAR.CUSTOM_SEPARATOR_END_MARK
);
return REGEX.test(input);
}
export function isCustomSeparatorExist(customSeparator) {
return customSeparator?.length > 0;
}
export function isCustomSeparatorChar(customSeparator) {
const REGEX = GENERATE_CUSTOM_SEPARATOR_IS_CHAR_REGEX();
return REGEX.test(customSeparator);
}
특이한 점이라면 정규식에 관련되어서 return 할 땐 이전에 언급한 것처럼 정규식.test(문자열) 함수를 이용해 true/false만 반환합니다.
2. validators.js
입력 문자열, 커스텀 구분자, 자료형 등 검증(예외 처리)에 대한 함수들이 모여있는 파일입니다.
주로 condition.js의 조건과 if문으로 Error를 반환하는 구조로 되어있습니다.
import { ERROR_MESSAGE, CONSTANT_CHAR } from '../constants/index.js';
import {
isAllNumber,
isAllPositiveNumber,
isCustomSeparatorExist,
isCustomSeparatorChar,
hasInputCustomSeparatorStartMarkOnly,
} from './conditions.js';
// 입력 문자열 검증
export function validateEmptyInput(input) {
return input.length > 0;
}
export function validateInputString(input) {
if (input.startsWith(CONSTANT_CHAR.CUSTOM_SEPARATOR_START_MARK) && hasInputCustomSeparatorStartMarkOnly(input)) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPARATOR_MUST_BE_CLOSED);
}
}
// 커스텀 구분자 검증
export function validateCustomSeparator(customSeparator) {
if (!isCustomSeparatorExist(customSeparator)) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPARATOR_MUST_EXIST);
}
if (!isCustomSeparatorChar(customSeparator)) {
throw new Error(ERROR_MESSAGE.CUSTOM_SEPARATOR_MUST_BE_CHARACTER);
}
}
// 자료형 검증
export function validateNumberTypes(list) {
if (!isAllNumber(list)) {
throw new Error(ERROR_MESSAGE.VALUE_MUST_BE_NUMBER);
}
if (!isAllPositiveNumber(list)) {
throw new Error(ERROR_MESSAGE.VALUE_MUST_BE_POSITIVE);
}
}
### App.js
index.js에서 인스턴스로 만들어져 프로그램이 가장 먼저 실행되는 파일입니다.
import { Console } from '@woowacourse/mission-utils';
import { INFORMATION_MESSAGE } from './constants/index.js';
import Calculator from './calculator/Calculator.js';
class App {
async run() {
try {
const input = await Console.readLineAsync(INFORMATION_MESSAGE.START);
const calculator = new Calculator();
const result = calculator.calculate(input);
Console.print(`${INFORMATION_MESSAGE.RESULT}${result}`);
} catch (error) {
Console.print(error.message);
throw error;
}
}
}
export default App;
크게 네 가지로 이루어집니다.
- 입력받기
- 계산기 인스턴스 만들기
- 계산하기
- 결과 출력하기
App.js는 이 최소한의 동작만 해야 한다고 생각했고 나머지 모든 기능은 파일로 분리한 형태가 되었습니다.
### README.md
최종적인 README는 처음과 달리 제 프로젝트에 대해, 그리고 예외처리에 대해 더 자세하게 작성하였습니다.
javascript-calculator-precourse
🐶 프로그램 큰 흐름
- 문자열 입력받기
- 문자열에 커스텀 구분자가 존재한다면 찾기
- 구분자(커스텀 구분자 포함)로 구분하여 숫자 배열 만들기
- 숫자 배열의 값을 모두 더하기
- 결과 출력하기
🐶 폴더 구조
- __tests__
- ApplicationTest.js # 통합 테스트
- src
- calculator
- Calculator.js # 핵심 로직 (계산기 본체)
- CalculatorParser.js # 입력 문자열 파싱 담당
- constants
- calculatorConfig.js # 구분자 등 설정 값
- index.js # constants 모듈 export
- messages.js # 안내 및 에러 메시지
- services
- NumberService.js # 숫자 추출 및 합산
- SeparatorService.js # 구분자 파싱 및 처리
- utils
- regexUtils.js # 정규식 생성 유틸
- validate
- conditions.js # 유효성 검증 조건 함수
- validators.js # 유효성 검증 실행 함수
- App.js # 애플리케이션 실행
- index.js # 어플리케이션 시작점
- calculator
- .gitignore
- .npmrc
- package-lock.json
- package.json
- README.md
🐶 구현할 기능 목록
1. 문자열 입력받기
- 시작 문구 출력 및 문자열 입력
- 문구 :
덧셈할 문자열을 입력해 주세요. - 함수 : Console.readLineAsync()
- 문구 :
2. 문자열에 커스텀 구분자가 존재한다면 찾기
- 정규표현식을 이용하여 커스텀 구분자 여부 찾기
- 커스텀 구분자가 있는 경우
- 커스텀 구분자를 구분자 리스트에 추가하기
- \n 앞부분을 잘라내기
- 커스텀 구분자가 있는 경우
3. 구분자(커스텀 구분자 포함)로 구분하여 숫자 배열 만들기
- 만들어진 숫자 배열의 숫자들을 Number형으로 변환하기
4. 숫자 배열의 값을 모두 더하기
- 함수 : arr.reduce()
5. 결과 출력하기
- 문구 :
결과 : ${sum} - 함수 : Console.print()
🐶 로직 흐름
- 1. 입력 및 실행
App.js:Console.readLineAsync를 사용해 사용자 입력을 받고Calculator를 실행
- 2. 핵심 계산 로직 (
Calculator.js)validateEmptyInput: 공백을 포함한 빈 문자열이 입력되면0을 returnCalculatorParser.parse: 문자열을 파싱 하여 최종 숫자 배열을 생성sumNumbers: 숫자 배열의 합계를reduce를 이용해 계산
- 3. 구분자 처리 (
SeparatorService.js)validateInputString: 입력값 형태가 올바른지 검증parseCustomSeparator: 정규식을 이용해 커스텀 구분자가 있는지 확인- 커스텀 구분자가 존재하면
validateCustomSeparator를 통해 유효성을 검증 - 기본 구분자(
,,:)와 커스텀 구분자를 포함한 전체 구분자 배열과, 구분자 선언부를 제외한 순수 숫자 문자열을 return
- 4. 숫자 추출 (
NumberService.js)extractNumbers: 전달받은 전체 구분자 배열을 기반으로split정규식을 동적으로 생성하여 문자열을 분리split으로 나뉜 각 값의 공백을 제거하고validateNumberTypes로 숫자 유효성을 검증- 유효성 검사를 통과한 배열을
Number형 배열로 변환하여 return
- 5. 유효성 검증 (
validators.js&conditions.js)- 입력값, 커스텀 구분자, 숫자(양수 여부, 숫자 여부)에 대한 유효성을 검사하고, 조건을 만족하지 못할 시
Errorthrow
- 입력값, 커스텀 구분자, 숫자(양수 여부, 숫자 여부)에 대한 유효성을 검사하고, 조건을 만족하지 못할 시
🐶 예외 처리 (자연어 기반)
입력 문자열 예외처리
- 1.입력 문자열이 없다면 0 출력
- 2.입력 문자열에 커스텀 구분자의 닫는 태그가 없는 경우 ERROR
- 3.입력 문자열에 구분자가 없다면 ERROR (-> 수정: 구분자 없이 숫자만 입력 시 해당 숫자 반환)
구분자 예외처리
- 1.커스텀 구분자에 아무것도 없다면 ERROR
- 2.커스텀 구분자가 문자가 아니라면 ERROR
- 3.커스텀 구분자가 공백이라면 ERROR
자료형 예외처리
- 1.구분자로 구분된 값이 비어있다면(구분자 연속 입력 시) ERROR
- 2.구분자로 구분된 값에 문자가 있다면 ERROR
- 3.구분자로 구분된 값에 양수가 아닌 숫자가 있다면 ERROR
🐶 예외 처리 (에러 메시지 기반)
[ERROR] 커스텀 구분자의 닫는 문자열이 필요합니다.
//로 시작했지만\n이 없는 경우 (e.g.,//;1;2;3)
[ERROR] 커스텀 구분자는 비어있을 수 없습니다.
//와\n사이에 아무 문자도 없거나 공백만 있는 경우 (e.g.,//\\n1,2,3또는// \\n)
[ERROR] 커스텀 구분자는 문자만 입력되어야 합니다.
- 커스텀 구분자에 숫자가 포함된 경우 (e.g.,
//1\\n...또는//a1\\n...)
[ERROR] 잘못된 입력값입니다.
- 구분자로 분리된 값 중 숫자가 아닌 값(문자, 특수문자 등)이 포함된 경우 (e.g.,
1,2,a또는a,b,c) - 구분자가 연속으로 사용된 경우 (e.g.,
1,,2::3) - 문자열이 구분자로 시작하거나 끝나는 경우 (e.g.,
,1,2,3또는1,2,3,)
[ERROR] 숫자는 양수만 입력되어야 합니다.
- 구분자로 분리된 값 중 음수(e.g.,
1,-2,3) 또는0(e.g.,0,1,2)이 포함된 경우
리드미도 지금까지 제대로 작성해 본 적이 없어
형식도 불완전하고 일관성도 부족하다고 생각해서
2주 차땐 더 개선된 README를 작성해야겠다고 생각했습니다.
전부 작성하고 나니 각 파일이 40줄이 넘지 않는다는 사실에 나름 만족했습니다.
기능 분리 자체도 (기준이 명확하진 않았지만) 나름 최선을 다했다고 생각했기 때문에 만족했습니다.
따라서 만족할 때까지 코드를 리팩토링하는 목표를..! 이루었습니다..!
# 마치며
프리코스 1주 차는 저에게 정말 귀중한 경험이었습니다.
소감문에도 작성하였지만 미션 수행 과정에서 얻은 경험뿐만 아니라
일주일 만에 사고력과 개발 지식이 엄청나게 늘었다는 점,
그리고 커뮤니티 활동을 하며 얻은 경험까지
단기간에 이렇게까지 성장했다는 사실이 놀라울 정도로 저에게 의미 있는 시간이었습니다.
2주 차땐 1주 차에서 적응한 우테코의 프리코스 진행 방식을 잘 활용해서
더 개선된 코드, 그리고 더 성장한 저를 마주하고 싶습니다.
이상, 긴 글 읽어주셔서 감사합니다.
🐶
'우아한테크코스 > 프리코스' 카테고리의 다른 글
| [2주차] 프리코스 2주 차 회고록 (0) | 2025.10.29 |
|---|---|
| [1주차] 스터디 회고록 (0) | 2025.10.23 |
| [1주차] 탐구 - 우테코 8기 프리코스 Helper 사이트 제작기 (6) | 2025.10.21 |
| [1주차] 탐구 - Airbnb JavaScript Style Guide (0) | 2025.10.14 |
| [0주차] 우테코 8기 지원과 준비 (12) | 2025.10.14 |