Todo 입력 이벤트와 로그인 체크 코드를 TypeScript로 바꾸는 과정

이번에는 Todo 화면에서 자주 보게 되는 코드를 TypeScript로 바꾸는 과정을 정리해보겠습니다.
예제로는 다음과 같은 흐름을 다룹니다.
- 입력창에서
Enter를 누르면 할 일을 등록하기 추가버튼을 누르면 할 일을 등록하기- 페이지가 열릴 때 로그인 상태를 확인하기
- 로그인되어 있으면 Todo 목록을 불러오기
기존 코드는 JavaScript로 작성되어 있고, 중간에 HTML 조각이 섞여 있거나 변수 선언이 불명확한 부분도 있습니다. TypeScript로 바꾸면 이런 부분을 더 안전하게 정리할 수 있습니다.
1. 기존 코드에서 아쉬운 점
기존 코드를 보면 다음과 같은 문제가 있습니다.
<script> todoTitleInput.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>
대표적으로 이런 문제가 있습니다.
todoTitleInput,token,loginInfo같은 변수가 어디서 선언되었는지 분명하지 않습니다.document.getElementById()의 결과는null일 수 있는데 바로 이벤트를 연결하고 있습니다.keydown이벤트의e타입이 명확하지 않습니다.Authorization헤더 값이 비어 있고, 코드 중간에 HTML 주석이 섞여 있습니다.checkLogin()이 끝나기 전에getTodoList()가 먼저 실행될 수 있습니다.
이럴 때 TypeScript를 쓰면 타입을 명확하게 하고, 흐름을 더 안전하게 만들 수 있습니다.
2. HTML 구조 준비하기
먼저 Todo 입력창과 버튼이 있다고 가정하겠습니다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Todo Web</title> </head> <body> <h1>나만의 Todo Web</h1> <input type="text" id="todoTitleInput" placeholder="할 일을 입력하세요" /> <button id="addBtn">추가</button> <ul id="todoList"></ul> <script type="module" src="./todo.ts"></script> </body> </html>
실제 브라우저에서 바로 todo.ts를 실행하는 구성은 개발 환경에 따라 달라질 수 있지만, 학습용으로는 파일 분리 흐름을 이해하는 데 좋습니다.
3. TypeScript에서 필요한 타입 정의하기
먼저 로그인 사용자 정보와 API 응답 형태를 간단히 정의해봅니다.
interface UserInfo { id: number; username: string; nickname: string; } interface MeResponse { data: UserInfo; }
이렇게 하면 response.json()으로 받은 값이 어떤 구조인지 더 분명하게 다룰 수 있습니다.
4. DOM 요소를 안전하게 가져오기
JavaScript에서는 아래처럼 바로 쓰는 경우가 많습니다.
const addBtn = document.getElementById("addBtn"); addBtn.addEventListener("click", addTodo);
하지만 TypeScript에서는 getElementById() 결과가 HTMLElement | null이기 때문에 그대로 쓰면 에러가 날 수 있습니다.
그래서 다음처럼 작성합니다.
const todoTitleInput = document.getElementById("todoTitleInput") as HTMLInputElement | null; const addBtn = document.getElementById("addBtn") as HTMLButtonElement | null; const todoList = document.getElementById("todoList") as HTMLUListElement | null;
그리고 null 여부를 꼭 확인합니다.
if (!todoTitleInput || !addBtn || !todoList) { throw new Error("필요한 HTML 요소를 찾을 수 없습니다."); }
이렇게 하면 요소가 없는 상태에서 이벤트를 붙이려다 발생하는 오류를 줄일 수 있습니다.
5. addTodo 함수 먼저 정리하기
할 일을 등록하는 함수도 TypeScript로 작성해봅니다.
async function addTodo(): Promise<void> { const title = todoTitleInput.value.trim(); if (!title) { alert("할 일을 입력해주세요."); todoTitleInput.focus(); return; } console.log("등록할 제목:", title); // 실제 API 호출은 이후에 연결 todoTitleInput.value = ""; todoTitleInput.focus(); }
좋아진 점은 다음과 같습니다.
- 반환 타입을
Promise<void>로 명시했습니다. - 입력값이 비어 있는 경우를 먼저 처리했습니다.
- 등록 후 입력창을 비우고 다시 포커스를 주도록 했습니다.
사용자 입장에서는 연속 입력이 쉬워집니다.
6. Enter 키 입력 이벤트를 TypeScript로 바꾸기
기존 JavaScript 코드는 다음과 같았습니다.
todoTitleInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ addTodo(); } });
TypeScript에서는 이벤트 타입을 명확하게 적어줄 수 있습니다.
todoTitleInput.addEventListener("keydown", (e: KeyboardEvent) => { if (e.key === "Enter") { addTodo(); } });
이렇게 하면 e.key를 사용할 때 TypeScript가 키보드 이벤트라는 사실을 알고 도와줍니다.
7. 버튼 클릭 이벤트 연결하기
버튼 클릭도 그대로 연결하면 됩니다.
addBtn.addEventListener("click", addTodo);
즉,
- Enter 키를 눌러도 등록
- 버튼을 클릭해도 등록
두 방식 모두 같은 addTodo() 함수를 사용하게 됩니다.
이렇게 작성하면 중복 코드를 줄일 수 있습니다.
8. 로그인 체크 함수를 TypeScript로 바꾸기
이제 가장 중요한 로그인 확인 부분을 정리해보겠습니다.
기존 코드는 토큰 확인과 API 호출 흐름이 섞여 있었고, 일부 코드가 깨져 있었습니다. TypeScript에서는 아래처럼 다시 구성할 수 있습니다.
let loginInfo: UserInfo | null = null; const token: string | null = localStorage.getItem("token"); async function checkLogin(): Promise<void> { console.log("함수호출시작"); if (!token) { location.href = "login.html"; return; } 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 result: MeResponse = await response.json(); loginInfo = result.data; console.log(loginInfo); console.log("함수호출끝"); }
여기서 중요한 포인트는 다음과 같습니다.
1) token의 타입을 명확히 함
const token: string | null = localStorage.getItem("token");
로컬스토리지에서 값을 읽으면 문자열이거나 null입니다. 이걸 타입으로 명확히 표현했습니다.
2) Authorization 헤더를 올바르게 작성함
Authorization: `Bearer ${token}`
기존 코드에는 이 부분이 깨져 있었는데, 보통 JWT 토큰 인증은 이렇게 작성합니다.
3) 응답 타입을 지정함
const result: MeResponse = await response.json(); loginInfo = result.data;
result.data 안에 어떤 값이 들어오는지 구조를 알고 다룰 수 있게 됩니다.
9. 로그인 확인 후 목록을 불러오는 순서 고치기
기존 코드에서는 이런 흐름이 있었습니다.
if(!token){ location.href="login.html"; }else{ checkLogin(); getTodoList(); }
문제는 checkLogin()이 비동기 함수인데 await 없이 바로 호출되고 있다는 점입니다. 그러면 로그인 확인이 끝나기 전에 getTodoList()가 먼저 실행될 수 있습니다.
TypeScript에서는 아래처럼 init() 함수를 만들어 순서를 맞추는 것이 좋습니다.
async function getTodoList(): Promise<void> { console.log("Todo 목록 조회"); // 이후 목록 조회 API 연결 } async function init(): Promise<void> { if (!token) { location.href = "login.html"; return; } await checkLogin(); await getTodoList(); } init();
이렇게 하면
- 토큰 확인
- 로그인 여부 확인 API 호출
- Todo 목록 조회
순서가 보장됩니다.
10. 최종 TypeScript 예제
위 내용을 합쳐서 정리하면 다음과 같습니다.
interface UserInfo { id: number; username: string; nickname: string; } interface MeResponse { data: UserInfo; } const todoTitleInput = document.getElementById("todoTitleInput") as HTMLInputElement | null; const addBtn = document.getElementById("addBtn") as HTMLButtonElement | null; const todoList = document.getElementById("todoList") as HTMLUListElement | null; if (!todoTitleInput || !addBtn || !todoList) { throw new Error("필요한 HTML 요소를 찾을 수 없습니다."); } let loginInfo: UserInfo | null = null; const token: string | null = localStorage.getItem("token"); async function addTodo(): Promise<void> { const title = todoTitleInput.value.trim(); if (!title) { alert("할 일을 입력해주세요."); todoTitleInput.focus(); return; } console.log("등록할 제목:", title); todoTitleInput.value = ""; todoTitleInput.focus(); } async function checkLogin(): Promise<void> { console.log("함수호출시작"); if (!token) { location.href = "login.html"; return; } 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 result: MeResponse = await response.json(); loginInfo = result.data; console.log(loginInfo); console.log("함수호출끝"); } async function getTodoList(): Promise<void> { console.log("Todo 목록 조회"); } todoTitleInput.addEventListener("keydown", (e: KeyboardEvent) => { if (e.key === "Enter") { addTodo(); } }); addBtn.addEventListener("click", addTodo); async function init(): Promise<void> { if (!token) { location.href = "login.html"; return; } await checkLogin(); await getTodoList(); } init();
11. TypeScript로 바꾸면서 얻는 장점
이 과정을 통해 다음과 같은 이점을 얻을 수 있습니다.
DOM 요소를 더 안전하게 다룰 수 있다
null 가능성을 체크하므로 런타임 오류를 줄일 수 있습니다.
이벤트 객체의 타입이 분명해진다
KeyboardEvent를 사용해서 e.key 같은 속성을 안전하게 사용할 수 있습니다.
로컬스토리지 값의 타입을 정확히 알 수 있다
string | null 타입 덕분에 토큰이 없는 경우를 명확하게 처리할 수 있습니다.
API 응답 구조를 코드로 설명할 수 있다
interface를 사용하면 응답 데이터 형태를 문서처럼 표현할 수 있습니다.
비동기 순서를 더 명확하게 제어할 수 있다
await를 사용해 로그인 확인 후 목록 조회가 실행되도록 만들 수 있습니다.
마무리
Todo 프로그램처럼 화면 입력, 버튼 이벤트, 로그인 체크, API 호출이 함께 들어가는 코드는 처음에는 JavaScript로도 만들 수 있습니다. 하지만 기능이 조금만 늘어나도 변수 타입, 응답 구조, 비동기 순서 때문에 실수가 생기기 쉽습니다.
이럴 때 TypeScript를 도입하면
- 어떤 값이 들어오는지
- 어떤 DOM 요소를 다루는지
- 어떤 순서로 함수가 실행되어야 하는지
를 더 분명하게 표현할 수 있습니다.
다음 글에서는 이 코드에 이어서 실제로 Todo 등록 API 호출, 목록 출력, 삭제 기능까지 TypeScript로 확장해볼 수 있습니다.
#태그
- typescript
- todo
- vite
- javascript
- frontend

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