오늘은 졸업을 위한... 토익 시험을 보고 일정이 있어 정신없는 날이었다. 하지만 하루도 빼먹을 수 없기 때문에 집에 들어오자마자 바로 앉아서 할 일을 정리해보았다. 지금까지 나는 스프링 부트로 백엔드(Backend)를 구현하였고 HTML/CSS를 사용해 프론트엔드(Frontend) 뼈대를 따로따로 만들었다. 이제는 자바 스크립트의 fetch를 사용해 이 둘을 연결해야 한다!
프론트엔드(HTML)와 백엔드(API) 연결
1. 왜 이 작업이 필요?
지금 나는 백엔드 API 서버'(Spring Boot)와 프론트엔드 뷰(HTML 파일)라는 두 개의 분리된 시스템을 만들었다.
- 프론트엔드 (HTML): index.html, random-lotto.html 등은 그 자체로 static 파일이다. 현재는 random-lotto.html의 로또 번호 생성하기 버튼을 아무리 눌러도 내가 예상한 결과는 물론 아무런 행동도 일어나지 않는다.
- 백엔드 (API): LottoController는 LottoApplication을 실행하면 8080 포트에서 요청이 오기 전까지 아무 일도 하지 않고 기다린다.
fetch 작업은 버튼 클릭같은 사용자 행동(Event)을 백엔드의 GET /im-minji/lotto/random API(Controller)에 연결해주는 역할을 할 것이다.
2. Fetch API (fetch())란 무엇이고 어떤 역할 담당하는지?
fetch()는 JavaScript에 내장된 'HTTP 요청' 전용 함수이다.
간단히 말해, random-lotto.html (프론트엔드)이 LottoController (백엔드)에게 요청을 걸게해주는 역할이다.
- fetch는 '비동기(asynchronous)' 방식으로 작동한다.
- '비동기'란 요청을 해두고, 응답이 올 때까지 다른 일을 계속 할 수 있다는 뜻이다.

3. fetch() 사용법 (간단한 예시)
fetch()는 어디로(URL) 요청할지, 그리고 어떻게(Options) 요청할지를 설정해야 한다.
1) GET 요청 (데이터 가져오기) GET 요청은 단순히 URL만 필요하며 가장 간단하다. async/await는 비동기 코드를 순서대로 실행되는 것처럼 편하게 쓰게 해주는 문법이다.
async function getSomeData() {
try {
const response = await fetch('/api/some-url');
const data = await response.json(); console.log(data);
} catch (error) {
console.error('호출 실패:', error); }
}
2) POST 요청 (데이터 저장하기, 옵션 필요) POST 요청은 저장할 데이터를 본문(Body)'에 실어 보내야 하므로 옵션이 필요할 때가 있다
async function saveSomeData(name, numbers) {
const dataToSend = { lottoName: name, numbers: numbers };
try {
const response = await fetch('/api/save-url',
{ method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataToSend) });
const savedData = await response.json();
console.log('저장 성공:', savedData.id);
} catch (error) { console.error('저장 실패:', error); } }
4. 이번 프로젝트에서 어떻게 사용?
지금까지 만든 각 HTML 파일(예: random-lotto.html)의 맨 아래에 script 태그를 추가하여 js 코드를 추가할 것이다.
- 먼저 HTML 문서가 전부 로드될 때까지 기다리기
- document.getElementById('generate-btn') 같은 코드로 '로또 번호 생성하기' 버튼을 찾기
- btn.addEventListener('click', ...) 코드로, "사용자가 이 버튼을 클릭하면, 내가 지정한 함수를 실행해줘"라고 EventListener 설정하기
- 사용자가 버튼을 클릭하면, 이 함수가 실행된다.
- 이 함수 안에서 fetch('/im-minji/lotto/random')를 호출하여 백엔드 API를 호출한다.
- API가 {"numbers": [1,2,3]} 같은 데이터를 돌려주면,
- document.getElementById('random-result-area').textContent = data.numbers.join(', ') 같은 코드로 결과 화면의 텍스트를 '1, 2, 3'으로 덮어쓰기(DOM 조작)한다.
모든 기능이 이런 '버튼 찾기' -> '클릭 감지' -> 'fetch 호출' -> '화면 덮어쓰기' 순서로 작동하도록 구현한다.
5. 연결해야 하는 API
1) random-lotto.html
- 로또 번호 생성하기 버튼은 GET /im-minji/lotto/random API를 호출
- 저장 확인 버튼은 POST /im-minji/my-lotto API를 호출하며, MyLottoRequestDTO에 맞는 JSON을 본문에 실어 보내야 한다.
2) my-lotto.html
- 파일이 열릴 때, GET /im-minji/my-lotto API를 호출하여 전체 목록 데이터를 받아온다.
- 각 항목의 구매 버튼은 POST /im-minji/purchase/from-my-lotto/{id} API를 호출한다. (id는 해당 항목의 ID입니다)
- 각 항목의 삭제 버튼은 DELETE /im-minji/my-lotto/{id} API를 호출한다.
- my-lotto-add.html 파일에서, 저장하기 버튼은 POST /im-minji/my-lotto API를 호출하며, 사용자가 입력한 MyLottoRequestDTO를 본문에 실어 보내야 한다.
3) purchased-lotto.html
- 파일이 열릴 때, GET /im-minji/purchase API를 호출하여 전체 목록 데이터를 받아온다.
- 각 항목의 삭제 버튼은 DELETE /im-minji/purchase/{id} API를 호출한다.
- purchased-lotto-add.html 파일에서, 저장하기 버튼은 POST /im-minji/purchase/manual API를 호출하며, 사용자가 입력한 PurchasedLottoRequestDTO(번호와 날짜)를 본문에 실어 보내야 한다.
1. random-lotto.html

이 화면이 load될 때는 화면 상의 모든 정보들이 불러져 와야한다. 그리고 생성하기 버튼을 누르면 GET으로 랜덤 생성을 받아와야 하고 생성하기 버튼을 한 번 누르면 등장하는 재생성을 누를 때마다 받아와야 한다. 그리고 MyLotto에 저장하기 버튼을 누르면 현재 있는 랜덤 번호와 이름을 입력받아 2가지의 정보를 requestDTO로 myLotto한테 보낸다.
<script>
// 1. HTML 문서가 모두 로드되면, function() 안의 코드를 실행
document.addEventListener('DOMContentLoaded', () => {
// 2. HTML에서 필요한 요소들을 ID로 찾아서 변수에 저장
const generateBtn = document.getElementById('generate-btn');
const saveBtn = document.getElementById('save-btn');
const resultArea = document.getElementById('random-result-area');
const saveForm = document.getElementById('save-form');
const lottoNameInput = document.getElementById('lotto-name');
const saveConfirmBtn = document.getElementById('save-confirm-btn');
// 생성된 로또 번호를 임시로 저장할 변수
let currentNumbers = [];
// 3. 로또 번호 생성하기(generate-btn) 버튼 클릭 이벤트
generateBtn.addEventListener('click', async () => {
try {
// API 1: 랜덤 번호 생성 API 호출 (LottoController의 getRandomLotto() 실행)
const response = await fetch('/im-minji/lotto/random');
if (!response.ok) {
throw new Error('[ERROR] API 호출에 실패했습니다.');
}
// 서버가 돌려준 JSON 데이터를 객체로 변환 data = { "numbers": [1, 2, 3, 4, 5, 6] }
const data = await response.json();
// 전역 변수에 번호 저장 (저장하기 버튼을 위해)
currentNumbers = data.numbers;
// 4. '결과 화면'의 HTML 내용을 API에서 받은 번호로 덮어쓰기
resultArea.innerHTML = `<p class="lotto-numbers">${currentNumbers.join(', ')}</p>`;
// 5. 버튼 상태 변경
generateBtn.textContent = '재생성'; // 버튼 텍스트 변경
saveBtn.disabled = false; // 저장하기 버튼 활성화
saveForm.classList.add('hidden'); // 이름 입력창 숨기기 (초기화)
} catch (error) {
resultArea.innerHTML = `<p class="error-text">${error.message}</p>`;
}
});
// 6. MyLotto에 저장하기 (save-btn) 버튼 클릭 이벤트
saveBtn.addEventListener('click', () => {
// 숨겨져 있던 '이름 입력 폼'을 보여줌
saveForm.classList.remove('hidden');
lottoNameInput.focus(); // 입력창에 바로 타이핑할 수 있게 포커스
});
// 7. 저장 확인 (save-confirm-btn) 버튼 클릭 이벤트
saveConfirmBtn.addEventListener('click', async () => {
const lottoName = lottoNameInput.value;
// 유효성 검사
if (!lottoName) {
alert('로또 이름을 입력해주세요.');
return;
}
if (currentNumbers.length === 0) {
alert('먼저 로또를 생성해주세요.');
return;
}
// API 2: 나만의 로또 저장 API에 보낼 Request DTO
const requestBody = {
numbers: currentNumbers,
lottoName: lottoName
};
try {
// API 2: 나만의 로또 저장 API 호출 (LottoController의 saveMyLotto() 실행)
const response = await fetch('/im-minji/my-lotto', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody) // JS 객체를 JSON 문자열로 변환
});
if (!response.ok) {
throw new Error('저장에 실패했습니다.');
}
const savedLotto = await response.json(); // 저장된 Response DTO 받기
alert(`'${savedLotto.lottoName}' 이름으로 저장되었습니다! (ID: ${savedLotto.id})`);
// 8. 성공 후, 화면 초기화
lottoNameInput.value = '';
currentNumbers = [];
saveForm.classList.add('hidden');
saveBtn.disabled = true;
generateBtn.textContent = '로또 번호 생성하기';
resultArea.innerHTML = `<p>생성하기 버튼을 눌러주세요.</p>`;
} catch (error) {
alert(error.message);
}
});
});
</script>
이렇게 하고 항상 하던 것처럼 라이브서버로 들어가서 테스트하니까 실패하는 거다!! ㅠㅠ 그래서 급하게 다시 포스트맨으로 테스트 해봤는데 문제가 없었다.

알고보니 내가 주소 설정을 http://localhost:8080/lotto/random 이런 식으로 어디서 보내겠다는 것을 확실하게 정해뒀으니까 라이브 서버에서는 호출을 보내도 인식을 못하는 것이었다. 휴
다시 http://localhost:8080/ 여기서 해보니까

얏호!!! 성공했다 너무 신기하다. 내가 처음에 생각한대로 코드들이 작동해서 원하는 결과를 만들어냈다는 것이 너무너무 뿌듯하다.

MyLotto에 저장하기 버튼을 누르면 위와 같이 이름을 입력하는 폼이 등장하고

저장까지 성공적으로 된 것 같다. 아직 MyLotto쪽을 구현하지 않아서 웹페이지에서 확인은 못하지만

포스트맨으로 성공적으로 저장되었다는 것을 확인했다. 얏호
2. my-lotto.html

어제 만들어둘 때 항상 데이터가 보이도록 목업 데이터를 넣어두었으나 이제는 mylotto 페이지를 불러올때마다 목록에 어떤 정보를 가지고 와야 하는지를 가져와야 한다. 가져올 때는 GET 요청으로 불러오고 해당 목록에서 구매 버튼을 누르면 나만의 로또에다가 날짜를 추가해서 구매로또로 복사하는 POST 요청을 보내야 한다. 이때 구매 버튼을 누를 때는 날짜를 오늘 날짜를 설정하는 것으로 했었다. 마지막으로 삭제 버튼을 클릭하면 DELETE 요청을 보내서 삭제해야 하고 목록도 새로 고침해서 삭제된 결과가 바로 목록에 적용되어서 보이도록 해야 한다!
<script>
// 1. HTML 문서가 모두 로드되면, function() 안의 코드를 실행
document.addEventListener('DOMContentLoaded', () => {
// 2. <tbody> 요소를 찾기 (표 내용 요소)
const tableBody = document.getElementById('my-lotto-list-body');
// API 1: 나만의 로또 목록 전체를 조회하고 화면에 그리는 함수: GET으로 내용 받아오기
async function loadMyLottoList() {
try {
// (LottoController의 readAllMyLotto() 실행)
const response = await fetch('/im-minji/my-lotto');
if (!response.ok) {
throw new Error('목록을 불러오는데 실패했습니다.');
}
// data = [ { "id": 1, "lottoName": "...", "numbers": [...] }, ... ]
const lottoList = await response.json();
// <tbody>의 HTML 내용을 비우기
tableBody.innerHTML = '';
if (lottoList.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5">저장된 로또가 없습니다.</td></tr>';
return;
}
// 3. 받아온 데이터(lottoList)로 테이블 행(<tr>)을 만들기
lottoList.forEach(lotto => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${lotto.id}</td>
<td>${lotto.lottoName}</td>
<td>${lotto.numbers.join(', ')}</td>
<td>
<button class="btn btn-small btn-purchase" data-id="${lotto.id}">구매</button>
</td>
<td>
<button class="btn btn-small btn-delete" data-id="${lotto.id}">삭제</button>
</td>
`;
// 4. 완성된 행(row)을 <tbody>에 추가하기
tableBody.appendChild(row);
});
} catch (error) {
tableBody.innerHTML = `<tr><td colspan="5" class="error-text">${error.message}</td></tr>`;
}
}
// API 2 & 3: '구매' 또는 '삭제' 버튼 클릭 처리 (C, D)
tableBody.addEventListener('click', async (event) => {
const clickedButton = event.target;
const lottoId = clickedButton.dataset.id; // data-id 속성 값 (예: "1")
if (!lottoId) {
return; // 버튼이 아닌 다른 곳을 클릭하면 무시
}
// --- API 2: '구매' 버튼 클릭 시 ---
if (clickedButton.classList.contains('btn-purchase')) {
if (!confirm(`'${lottoId}'번 로또를 '구매 내역'으로 복사하시겠습니까?\n(오늘 날짜로 저장됩니다)`)) {
return;
}
try {
// (LottoController의 purchasedMyLotto() 실행)
const response = await fetch(`/im-minji/purchase/from-my-lotto/${lottoId}`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('[ERROR] 구매에 실패했습니다.');
}
const savedPurchase = await response.json();
alert(`구매 완료! (구매 ID: ${savedPurchase.id})`);
// (구매 성공 시, 구매 목록 페이지로 이동시키는 것도 좋습니다)
// window.location.href = 'purchased-lotto.html';
} catch (error) {
alert(error.message);
}
}
// --- API 3: '삭제' 버튼 클릭 시 ---
if (clickedButton.classList.contains('btn-delete')) {
if (!confirm(`'${lottoId}'번 로또를 정말 삭제하시겠습니까?`)) {
return;
}
try {
// (LottoController의 deleteMyLotto() 실행)
const response = await fetch(`/im-minji/my-lotto/${lottoId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('[ERROR] 삭제에 실패했습니다.');
}
alert('삭제 완료!');
// 5. 삭제 성공 시, 목록을 새로고침 (화면에 적용되어야 하기 때문에)
loadMyLottoList();
} catch (error) {
alert(error.message);
}
}
});
// 6. 페이지가 처음 열리면, 목록을 자동으로 불러오기
loadMyLottoList();
});
</script>

그리고 수동으로 저장하는 myLotto에 대해서도 구현해야 하는데 이는 random이랑 비슷하게 모든 요소를 찾고 입력된 값에 대해서 유효성 검사를 한 후 입력된 값을 데이터로 넣어서 requestDTO로 POST 하면 된다!
<script>
document.addEventListener('DOMContentLoaded', () => {
// 1. 폼(form)과 입력창(input) 요소를 찾기
const saveForm = document.getElementById('my-lotto-save-form');
const lottoNameInput = document.getElementById('lotto-name');
const lottoNumbersInput = document.getElementById('lotto-numbers');
// 2. 폼(form)에서 submit 이벤트가 발생했을 때(저장하기 버튼 클릭 시)
saveForm.addEventListener('submit', async (event) => {
// 3. 'submit'의 기본 동작(페이지 새로고침)을 막기
event.preventDefault();
// 4. 입력된 값을 가져오기
const lottoName = lottoNameInput.value;
const numbersString = lottoNumbersInput.value; // "1, 2, 3, 4, 5, 6"
// 5. 쉼표로 구분된 문자열을 -> 숫자 배열로 변환
let numbersArray = [];
try {
numbersArray = numbersString.split(',') // ["1", " 2", " 3", ...]
.map(s => parseInt(s.trim())) // [1, 2, 3, ...]
.filter(n => !isNaN(n)); // NaN(숫자 아님) 값 제거
// 간단한 유효성 검사
if (numbersArray.length !== 6) {
throw new Error('로또 번호는 6개여야 합니다.');
}
if (!lottoName) {
throw new Error('로또 이름을 입력해야 합니다.');
}
} catch (error) {
alert('입력 값 오류: ' + (error.message || '번호를 쉼표(,)로 구분하여 6개 입력해주세요.'));
return;
}
// 6. API에 보낼 Request DTO 객체
const requestBody = {
numbers: numbersArray,
lottoName: lottoName
};
try {
// 7. API 2: '나만의 로또' 저장 API 호출 (LottoController의 saveMyLotto() 실행)
const response = await fetch('/im-minji/my-lotto', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
// 서버가 4xx, 5xx 에러를 보낸 경우
const errorData = await response.json(); // 서버의 에러 메시지
throw new Error(errorData.message || '저장에 실패했습니다.');
}
const savedLotto = await response.json();
alert(`'${savedLotto.lottoName}' 이름으로 저장되었습니다! (ID: ${savedLotto.id})`);
// 8. 저장 성공 시, '나만의 로또' 목록 페이지로 이동
window.location.href = 'my-lotto.html';
} catch (error) {
alert('저장 중 오류 발생: ' + error.message);
}
});
});
</script>
이렇게 해서 결과는!!




삭제도 잘 작동하고 구매를 했을 시에도 복사가 잘 되어졌다는 것을 포스트맨으로 모두 확인하였다! 이제 마지막으로 구매 로또만 구현하면 된다. 나만의 로또 페이지랑 동작 페이지가 비슷해 동일한 로직으로 진행할 것 같당
3. purchased-lotto.html



이렇게 해서 모든 API 연결이 끝났다!!
오늘로서야 2주간 해온 프로젝트가 눈으로 보인느 결과로 마무리 된 것 같아서 너무 뿌듯하다. 이제 내일부터는 제출 기한이 시작되는데 조금의 시간이 더 주어졌기 때문에 중간에 뒤로 뺐었던 통계 기능을 도전해볼 것이다. 그리고 나서 이 프로젝트를 어떻게 이용해야 하는건지? 에 대한 것들을 리드미에 정리하고 기록한 다음에 마무리할 것이다. 이제 정말 얼마 남지 않았다. 힘내서 마무리하자 아자자!!