addEventListener와 로그인 체크 코드를 TypeScript로 변환하는 과정

이번 글에서는 기존 JavaScript 코드 일부를 TypeScript로 바꾸는 과정을 단계별로 정리해보겠습니다.
예제로 사용할 코드는 다음과 같은 역할을 합니다.
- Enter 키를 누르면
addTodo()실행 - 버튼을 클릭하면
addTodo()실행 /auth/meAPI를 호출해서 로그인 여부 확인- 토큰이 없거나 로그인 실패 시
login.html로 이동 - 로그인 상태라면
getTodoList()호출
다만 원본 코드에는 HTML이 섞여 깨진 부분이 있고, 타입 정보가 없기 때문에 TypeScript로 옮길 때 몇 가지를 정리해야 합니다.
1. 원본 코드에서 확인할 점
기존 코드는 대략 이런 형태입니다.
<script> addEventListener("keydown", function(e){ if(e.key === "Enter"){ addTodo(); } }); const addBtn = document.getElementById("addBtn"); addBtn.addEventListener("click", addTodo); async function checkLogin() { const response = await fetch('https://api.fullstackfamily.com/api/edu/ws-283fc1/auth/me', { method : 'GET', headers : { "Authorization" : ... } }); if(!response.ok){ location.href="login.html"; return; } loginInfo = await response.json(); console.log(loginInfo.data); } if(!token){ location.href="login.html"; }else{ checkLogin(); getTodoList(); } </script>
이 코드에서 TypeScript로 바꾸기 전에 먼저 생각해야 할 점은 다음과 같습니다.
e의 타입이 없다.document.getElementById()는HTMLElement | null을 반환한다.token은string | null일 수 있다.loginInfo의 구조가 불명확하다.checkLogin()이 비동기 함수인데, 호출 순서를 명확하게 관리하는 것이 좋다.- 깨진 HTML 조각은 제거하고 JavaScript 로직만 정리해야 한다.
2. 먼저 변수와 함수의 역할을 정리하기
TypeScript에서는 코드를 그냥 옮기기보다, 먼저 어떤 값이 어떤 타입인지 정리하는 것이 중요합니다.
예를 들면 다음과 같은 값들이 있습니다.
token: 로그인 토큰addBtn: 할일 추가 버튼todoTitleInput: 할일 입력창loginInfo: 로그인 사용자 정보
그리고 함수는 대략 이렇게 볼 수 있습니다.
addTodo(): 할일 등록checkLogin(): 로그인 여부 확인getTodoList(): 할일 목록 조회
3. 이벤트 객체에 타입 지정하기
원본 코드에서는 function(e)처럼 작성되어 있습니다.
addEventListener("keydown", function(e){ if(e.key === "Enter"){ addTodo(); } });
TypeScript에서는 e가 어떤 이벤트인지 알려주는 것이 좋습니다. 키보드 입력 이벤트이므로 KeyboardEvent를 사용할 수 있습니다.
window.addEventListener("keydown", function (e: KeyboardEvent) { if (e.key === "Enter") { addTodo(); } });
여기서 addEventListener() 앞에 window를 붙여주면, 전역 이벤트라는 점도 더 분명해집니다.
4. getElementById()의 null 처리하기
기존 코드:
const addBtn = document.getElementById("addBtn"); addBtn.addEventListener("click", addTodo);
이 코드는 JavaScript에서는 흔히 쓰이지만, TypeScript에서는 에러가 날 수 있습니다.
그 이유는 document.getElementById("addBtn")의 결과가 없을 수도 있기 때문입니다.
즉 반환 타입은 단순한 버튼이 아니라 다음처럼 볼 수 있습니다.
HTMLElement | null
그래서 null 체크가 필요합니다.
const addBtn = document.getElementById("addBtn"); if (addBtn) { addBtn.addEventListener("click", addTodo); }
조금 더 구체적으로 버튼 요소라고 가정한다면 타입 단언을 사용할 수도 있습니다.
const addBtn = document.getElementById("addBtn") as HTMLButtonElement | null; if (addBtn) { addBtn.addEventListener("click", addTodo); }
5. 입력창도 정확한 타입으로 가져오기
Enter 키를 입력하는 대상이 할일 입력창이라면, 입력창도 타입을 분명히 하는 것이 좋습니다.
const todoTitleInput = document.getElementById("todoTitleInput") as HTMLInputElement | null; if (todoTitleInput) { todoTitleInput.addEventListener("keydown", function (e: KeyboardEvent) { if (e.key === "Enter") { addTodo(); } }); }
이렇게 하면 나중에 todoTitleInput.value를 사용할 때도 더 안전하게 다룰 수 있습니다.
6. token 타입 처리하기
로그인 토큰은 보통 localStorage.getItem()으로 읽어옵니다.
const token = localStorage.getItem("token");
여기서 중요한 점은 반환값이 항상 문자열이 아니라는 것입니다.
- 값이 있으면
string - 값이 없으면
null
즉 타입은 다음과 같습니다.
string | null
그래서 조건문으로 먼저 검사해야 합니다.
const token: string | null = localStorage.getItem("token"); if (!token) { location.href = "login.html"; }
이 과정을 거치면 이후 코드에서는 토큰이 존재하는 경우만 처리할 수 있습니다.
7. 로그인 응답 타입 정의하기
원본 코드에서는 loginInfo = await response.json();처럼 쓰고 있습니다.
하지만 TypeScript에서는 응답 데이터 구조를 어느 정도라도 적어주는 것이 좋습니다.
예를 들어 /auth/me 응답이 아래처럼 온다고 가정해보겠습니다.
{ "data": { "id": 1, "username": "nicole", "nickname": "Nicole" } }
그렇다면 interface를 정의할 수 있습니다.
interface UserData { id: number; username: string; nickname: string; } interface MeResponse { data: UserData; }
이제 response.json() 결과를 더 명확하게 다룰 수 있습니다.
const loginInfo: MeResponse = await response.json(); console.log(loginInfo.data);
8. checkLogin() 함수에 반환 타입 쓰기
원본 코드의 checkLogin()은 비동기 함수입니다.
async function checkLogin() { ... }
TypeScript에서는 반환 타입도 써주는 습관이 좋습니다.
이 함수는 특별한 값을 반환하지 않으므로 Promise<void>로 작성할 수 있습니다.
async function checkLogin(token: string): Promise<void> { console.log("함수호출시작"); const response = await fetch("https://api.fullstackfamily.com/api/edu/ws-283fc1/auth/me", { method: "GET", headers: { Authorization: `Bearer ${token}`, }, }); console.log(`response.ok : ${response.ok}`); if (!response.ok) { location.href = "login.html"; return; } const loginInfo: MeResponse = await response.json(); console.log(loginInfo.data); console.log("함수호출끝"); }
여기서 한 가지 중요한 수정이 있습니다.
원본 코드에는 Authorization 헤더 부분이 깨져 있었는데, 실제로는 보통 다음처럼 작성해야 합니다.
Authorization: `Bearer ${token}`
9. 로그인 체크 후 목록 조회 순서 개선하기
원본 코드에서는 이렇게 되어 있습니다.
if(!token){ location.href="login.html"; }else{ checkLogin(); getTodoList(); }
이렇게 쓰면 checkLogin()이 끝나기 전에 getTodoList()가 먼저 실행될 수도 있습니다.
왜냐하면 checkLogin()은 비동기 함수이기 때문입니다.
그래서 TypeScript로 바꾸면서 await를 이용해 흐름을 더 명확히 만들면 좋습니다.
async function init(): Promise<void> { const token: string | null = localStorage.getItem("token"); if (!token) { location.href = "login.html"; return; } await checkLogin(token); await getTodoList(); }
그리고 마지막에 초기화 함수를 호출합니다.
init();
10. 최종 TypeScript 코드 예제
정리한 내용을 바탕으로 전체 코드를 예제로 써보면 다음과 같습니다.
interface UserData { id: number; username: string; nickname: string; } interface MeResponse { data: UserData; } const todoTitleInput = document.getElementById("todoTitleInput") as HTMLInputElement | null; const addBtn = document.getElementById("addBtn") as HTMLButtonElement | null; function addTodo(): void { console.log("할일 등록"); } async function getTodoList(): Promise<void> { console.log("할일 목록 조회"); } if (todoTitleInput) { todoTitleInput.addEventListener("keydown", function (e: KeyboardEvent) { if (e.key === "Enter") { addTodo(); } }); } if (addBtn) { addBtn.addEventListener("click", addTodo); } async function checkLogin(token: string): Promise<void> { console.log("함수호출시작"); const response = await fetch("https://api.fullstackfamily.com/api/edu/ws-283fc1/auth/me", { method: "GET", headers: { Authorization: `Bearer ${token}`, }, }); console.log(`response.ok : ${response.ok}`); if (!response.ok) { location.href = "login.html"; return; } const loginInfo: MeResponse = await response.json(); console.log(loginInfo.data); console.log("함수호출끝"); } async function init(): Promise<void> { const token: string | null = localStorage.getItem("token"); if (!token) { location.href = "login.html"; return; } await checkLogin(token); await getTodoList(); } init();
11. TypeScript로 바꾸면서 좋아지는 점
이번 변환 과정의 핵심은 단순히 확장자를 .js에서 .ts로 바꾸는 것이 아닙니다.
TypeScript를 적용하면 다음과 같은 장점이 있습니다.
1) 이벤트 객체를 더 안전하게 다룰 수 있다
KeyboardEvent 타입 덕분에 e.key 같은 속성을 믿고 사용할 수 있습니다.
2) DOM 요소가 없을 가능성을 체크하게 된다
getElementById() 결과가 null일 수 있다는 점을 코드에서 직접 다루게 됩니다.
3) token 값이 없을 수 있음을 명확히 표현한다
string | null 타입을 통해 로그인 체크 로직이 더 분명해집니다.
4) API 응답 구조를 예측 가능하게 만든다
interface를 사용하면 loginInfo.data에 어떤 값이 들어있는지 파악하기 쉬워집니다.
5) 비동기 흐름을 더 안정적으로 만들 수 있다
await checkLogin(token) 이후에 getTodoList()를 호출하도록 순서를 보장할 수 있습니다.
마무리
기존 JavaScript 코드를 TypeScript로 바꿀 때는 다음 순서로 접근하면 편합니다.
- 변수와 함수 역할 파악
- DOM 요소 타입 지정
- 이벤트 타입 지정
null가능성 처리- API 응답 타입 정의
- 비동기 흐름 정리
이 과정을 반복하면 단순한 예제 코드도 점점 더 읽기 쉽고, 유지보수하기 쉬운 코드로 바뀝니다.
다음 글에서는 이 코드에 이어서 할일 등록 API 호출, 입력창 비우기, 포커스 이동, Todo 목록 출력까지 TypeScript로 확장해보겠습니다.

댓글
댓글을 작성하려면 이 필요합니다.