이번에 페이지 기본 틀을 어느 정도 만든 후, 바로 로또 6/45의 랜덤 뽑기 기능을 구현하기로 했다. 사실 GPT랑 구글링으로 개발 공부를 계속해왔지만, 이번에는 혼자 학습하는 게 유독 힘들었다. 배운 내용이 효율적인지 확신할 수 없었고, 강의로 배우지 못한 부분을 익히는데 시간이 너무 많이 걸렸다.
이 기본 틀을 만드는 동안 가장 큰 어려움은 최신 로또 회차를 자동으로 업데이트하는 기능을 구현하는 거였다. 최신 회차는 일주일에 한 번 갱신되는데, 동행복권 페이지를 크롤링해 DB에 저장하고, 이 값을 이용해 최신 회차를 표시하는 기능을 만들고 싶었다. 이 과정에서 axios, cheerio, puppeteer, cron 같은 라이브러리들을 알게 되었고, 크롤링하는 것까지는 성공했다. (디자인은 나중에.. ㅎㅎ)
내가 원하는 목표는 매주 한 번씩 자동으로 크롤링해서 DB에 저장하고, 이 값을 바탕으로 최신 회차 정보를 갱신하는 것이었다. 하지만 내 부족한 지식으로는 크론 작업을 원하는 주기대로 실행하는 방법을 제대로 구현하지 못했다. 결국 특정 이벤트마다 크롤링하고, DB에 저장된 값과 비교한 후 최신 회차로 갱신되면 저장하는 방식으로 구현했다. 이 방식은 내 기준에서 봤을 때, '쓰레기 같은 코드'였지만 당시로서는 어쩔 수 없었다. 그런데, 이 방법조차도 크롤링하는 과정에서 로딩 시간이 너무 길어지는 문제가 생겼고, 결국 버리기로 했다.
최신 로또 회차는 사실 일주일에 한 번만 갱신되면 되기 때문에, 일단 하드코딩된 값을 모든 사용자에게 보여주기로 했다. 그나마 나은 해결책이었지만, 여전히 일주일에 한 번만 갱신되도록 구현할 방법을 찾지 못하고 있다. 이런 부분이 참 어렵다. 누구한테 물어보고 싶었지만, 딱히 물어볼 곳도 없었다. GPT를 사용해도 매번 해결되지 않은 답을 줬고, 그래서 이 부분은 일단 미뤄두고 다음 기능으로 넘어갔다.
Fisher-Yates 알고리즘을 사용한 랜덤 번호 뽑기
이제 내가 처음 만든 기능은 각 종류의 뽑기에 따라 다른 페이지를 보여주는 것이었다. 그중에서도 제일 먼저 만든 건, 말 그대로 1부터 45까지의 숫자 중에서 6개의 무작위 난수를 뽑아주는 기능이었다.
처음에는 단순하게 Math.random을 사용해서 6개의 난수를 뽑았다.
function randomPickFunction() {
const numbers = [];
while (numbers.length < 6) {
const num = Math.floor(Math.random() * 45) + 1;
if (!numbers.includes(num)) {
numbers.push(num);
}
}
return numbers.sort((a, b) => a - b);
}
그런데 내가 알기로는 Math.random이 완전한 무작위성을 보장하지 않는다고 알고 있었다. 그래서 이번에 새로나온 GPT-o1 preview 에게 코드를 보완할 방법을 물어봤더니, Fisher-Yates 알고리즘을 이용하라고 했다. 이 알고리즘은 배열을 무작위로 섞는 데 자주 사용되는 방법이라고 한다.
그렇게 해서 나온 코드가 바로 이거다.
function randomPickFunction() {
const numbers = Array.from({ length: 45 }, (_, i) => i + 1);
for (let i = numbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
}
return numbers.slice(0, 6).sort((a, b) => a - b);
}
사실 처음 이 코드를 봤을 때 알고리즘의 '알'도 모르는 나는 뭔 소리인지 하나도 몰랐다. 그래서 이 코드를 차근차근 분석하면서 공부해 봤다.
Fisher-Yates 알고리즘 이해하기
우선 1부터 45까지의 숫자가 들어간 배열을 만들어준다.
const numbers = Array.from({ length: 45 }, (_, i) => i + 1);
이렇게 하면 [1, 2, 3, 4, ..., 45]가 들어간 배열이 생성된다.
이제 배열을 섞어준다. 여기서 핵심이 되는 부분이 이 루프다.
for (let i = numbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
}
처음에는 이게 이해가 잘 안 됐다. 그래서 간단한 예시를 들어 봤다. 배열의 길이가 5인 경우, 초기 배열은 [1, 2, 3, 4, 5]가 된다.
- 첫 번째 교환: i = 4 (배열의 마지막 인덱스), j = 2 (무작위로 선택된 인덱스)
배열 상태: [1, 2, 5, 4, 3] - 두 번째 교환: i = 3, j = 0
배열 상태: [4, 2, 5, 1, 3]
이런 식으로 뒤에서부터 배열을 섞으면서 무작위성을 높이는 것이다.
그러면 숫자가 무작위로 섞인 배열이 생기게 된다.
여기서 궁금했던 게, 왜 하필 뒤에서부터 섞는지였다. 결론부터 말하자면, 뒤에서부터 섞으면 점점 섞을 범위가 줄어들어 이미 섞인 요소를 다시 선택할 가능성이 없기 때문이다. 반면, 앞에서부터 섞으면 이미 섞인 요소들이 다시 선택될 수 있어 무작위성이 깨질 위험이 커진다. 간단한 예제를 아래 사진으로 두겠으니 혹시 이해가 안가는 분들은 참고하시길!
예시 초기배열 : [1, 2, 3, 4, 5]
이렇게 i 인덱스로 이미 섞인 부분이 계속해서 섞일 수 있는 문제가 발생하기 때문에 뒤에서 부터 섞는 방법이 옳다.
무작위 뽑기 기능 구현
return numbers.slice(0, 6).sort((a, b) => a - b);
위에서 열심히 만든 무작위 배열의 앞에서 부터 원하는 만큼 가져오가만 하면 중복 검사하는 과정 없이 빠르게 난수 추출이 가능하다!
이렇게 뽑기 버튼을 누르면 6개의 무작위 숫자가 잘 뽑혀 나온다. 참고로 나는 한 번에 최대 5개까지만 뽑을 수 있게 설정했다. 이유는 너무 많이 뽑으면 뽑힌 숫자의 '소중함'이 떨어진다고 생각했기 때문이다. 뽑은 숫자를 더 신중하게 선택하도록 유도하는 것이 목표다.
그리고 삭제 버튼도 간단하게 구현했다.
onClick={() => {
setNumArrayList(
numArrayList.filter((_, i) => i !== index)
);
}}
이 코드는 뽑은 숫자 배열에서 내가 선택한 요소만 제거하고 나머지를 유지하는 방식이다. 너무 쉽게 해결돼서 허탈할 정도였다. ㅎㅎ
AI 코딩 보조 도구에 대한 생각
이번에 Cursor라는 코딩 보조 AI를 처음 써봤다. 말 그대로 '신세계'였다. GPT도 편리했지만, 이건 내가 생각한 코드를 알아서 예상해서 빠르게 써주는 기능까지 있었다. 게다가 "로또 번호 추출 함수 만들어줘"라고 쓰면, 내가 30분 동안 고민해서 쓴 코드보다 훨씬 깔끔한 코드가 10초 만에 나왔다. 너무 신기하면서도, 내 코딩 실력은 늘고 있는게 맞나? 라는 의문점과 걱정이 들었다.
결국 내가 내린 결론은, AI 보조 도구를 적극 활용하되, 기능 구현은 내가 먼저 고민해서 해결하고 나서 AI의 도움을 받는 식으로 하기로 했다. 기본적인 기능이나 로직은 내가 직접 짜고, 디자인이나 반복적인 작업은 AI를 통해 빠르게 처리하는 방식이 내가 개발자로서 성장하는 데 더 도움이 될 거라 생각했다.
다음은 확률 픽과 언더독 픽 기능을 만들어야겠다. 확률에 따라 숫자를 뽑거나, 그동안 나오지 않은 숫자들을 더 많이 뽑도록 하는 기능이다. 이제부터 진짜 재미있는 작업이 될 것 같다!
'나만의 로또 번호 추첨 사이트' 카테고리의 다른 글
#5 : 언더독 뽑기 기능 구현하기 (1) | 2024.10.09 |
---|---|
#4 : 확률을 반영한 로또 번호 추출 기능 만들기 (0) | 2024.10.06 |
#2 : 기본적인 폴더 생성 및 메인 페이지 틀 제작 (2) | 2024.09.24 |
#1 : 생에 첫 프로젝트 : 로또 번호 랜덤 추출 사이트 (1) | 2024.09.19 |