JavaScript TypeScript
3줄 요약
- 백엔드 개발자에게 JS/TS의 핵심은 문법 암기가 아니라 브라우저 런타임, 비동기 실행 순서, 타입 경계, API 응답 모델링을 정확히 이해하는 것이다.
Promise,AbortController,Date, input value, nullable, union type은 개인 프로젝트에서 바로 버그를 만들기 쉬운 지점이다.- TypeScript는 런타임 검증 도구가 아니므로 외부 입력은
unknown으로 받고 Zod 같은 schema로 좁히는 습관이 필요하다.
핵심 정리
- JavaScript는 브라우저 이벤트 루프 위에서 동작한다. 네트워크 요청, timer, 사용자 이벤트는 코드가 위에서 아래로 읽히더라도 완료 순서와 취소 가능성을 함께 봐야 한다.
async/await는 Promise를 읽기 쉽게 만드는 문법이지 병렬 처리와 취소를 자동으로 해결하지 않는다.- TypeScript 타입은 컴파일 타임 계약이다. 서버 JSON, URL query, storage 값은 런타임에서 여전히 검증되지 않은 값이다.
- API 타입은 DTO와 view model을 나누면 nullable 처리, 날짜 포맷, 표시 label, fallback image를 한곳에서 정리하기 좋다.
.ts와.tsx는 의미 있는 구분이다. JSX가 없는 API 함수, 타입, 유틸은.ts, 컴포넌트는.tsx로 둔다.
백엔드 개발자가 헷갈리는 지점
- TypeScript 타입을 붙이면 API 응답도 자동으로 안전해진다고 오해하기 쉽다.
- 판단 기준은 외부 입력이 런타임 검증을 통과했는지다.
as User는 검증이 아니라 컴파일러에게 믿으라고 말하는 것이다.
Date가 Java의LocalDate,Instant,ZonedDateTime처럼 명확히 나뉜다고 생각하기 쉽다.- 판단 기준은 date-only와 timestamp를 API 계약에서 구분하는 것이다.
- 생일과 마감일은 문자열 date-only로, 생성 시각은 ISO timestamp로 다룬다.
forEach(async ...)가 모든 요청을 기다린다고 착각하기 쉽다.- 판단 기준은 병렬이면
Promise.all, 순차면for...of, 일부 실패 허용이면Promise.allSettled다.
- 판단 기준은 병렬이면
개인 프로젝트 기준
- API client는
fetchwrapper 하나로 시작하고, 반복되는 인증/에러/취소 정책이 생기면 계층을 나눈다. - 외부 응답은
unknown으로 받은 뒤 Zod schema나 타입 생성 도구로 좁힌다. - URL, input, storage에서 온 값은 string으로 보고 명시적으로 변환한다.
- 문법 함정은 02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅을 기준 문서로 둔다.
확인 질문
- TypeScript 타입만으로 서버 응답이 안전해지는가?
- 아니다. TypeScript는 컴파일 타임 타입이고, 서버 응답은 런타임 검증이나 신뢰 가능한 생성 타입으로 확인해야 한다.
- 독립 API 요청 여러 개를 동시에 처리하려면 무엇을 쓰는가?
Promise.all을 기본으로 쓰고, 일부 실패를 허용해야 하면Promise.allSettled를 쓴다.
.ts와.tsx를 구분하는 실전 기준은 무엇인가?- JSX가 있으면
.tsx, 타입, API, 유틸, schema처럼 JSX가 없으면.ts를 쓴다.
- JSX가 있으면
01. 비동기 타입 설계 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
async/await를 쓰는데도 race condition과 취소 문제가 생기는 이유는 무엇인가?- TypeScript에서 API 응답 타입을 어떻게 안전하게 다룰 수 있는가?
.ts와.tsx를 구분하는 기준은 단순 취향인가, 실전 협업 신호인가?
개요
백엔드 개발자가 프론트엔드 JavaScript를 배울 때 가장 조심해야 할 지점은 문법 자체보다 실행 환경이다. 브라우저는 사용자 입력, 네트워크, timer, 렌더링을 동시에 다룬다. 따라서 “요청을 보냈다”와 “현재 화면에 반영해도 된다” 사이에는 취소, 순서 역전, 중복 요청, stale data 문제가 끼어든다.
TypeScript도 마찬가지다. 타입을 많이 붙이는 것보다 어디까지가 신뢰 가능한 값인지 경계를 나누는 것이 중요하다. API 응답, URL query, Web Storage 값은 외부 입력이다. 이 값에 as를 붙이는 것은 검증이 아니라 주장이다.
원리
async/await는 Promise 기반 비동기 흐름을 동기 코드처럼 읽게 해준다. 그러나 병렬 실행, 취소, 일부 실패 허용, timeout은 직접 설계해야 한다. 예를 들어 검색어가 바뀔 때마다 요청을 보내면 먼저 보낸 요청이 나중에 도착해 최신 검색 결과를 덮을 수 있다. 이런 문제는 React의 useEffect cleanup, AbortController, TanStack Query의 query key로 다룬다.
TypeScript는 코드를 실행하기 전에 타입 오류를 줄여 주지만, 런타임 JSON의 모양을 검사하지 않는다. 서버가 문서와 다른 값을 내려주거나 nullable 필드가 빠지면 타입만으로는 막을 수 없다. 신뢰 경계에서는 unknown으로 받은 뒤 schema 검증 또는 OpenAPI 기반 타입 생성을 사용한다.
코드로 보는 핵심 예시
검색 요청은 취소와 최신성 문제가 자주 생긴다. 예시는 직접 effect를 작성할 때 필요한 방어를 보여주지만, 실제 목록/검색 화면은 TanStack Query로 옮기는 편이 더 단순할 수 있다.
function SearchResult({ keyword }: { keyword: string }) {
const [items, setItems] = useState<Item[]>([]);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const response = await fetch(
`/api/items?keyword=${encodeURIComponent(keyword)}`,
{ signal: controller.signal },
);
if (!response.ok) {
throw new Error("검색 요청에 실패했습니다.");
}
const body: unknown = await response.json();
const items = itemListSchema.parse(body);
setItems(items);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
return;
}
setError(error instanceof Error ? error : new Error("알 수 없는 오류"));
}
}
load();
return () => controller.abort();
}, [keyword]);
if (error) return <p role="alert">{error.message}</p>;
return <ItemList items={items} />;
}외부 입력은 타입 assertion보다 schema 검증으로 좁히는 편이 안전하다.
import { z } from "zod";
const itemSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number().nonnegative(),
thumbnailUrl: z.string().url().nullable(),
});
const itemListSchema = z.array(itemSchema);
type Item = z.infer<typeof itemSchema>;.ts와 .tsx는 다음처럼 구분한다.
src/
api/items.ts
schemas/item.ts
hooks/useItems.ts
components/ItemCard.tsx기존 메모에 있던 핵심처럼, 모든 파일을 .tsx로 만들 수는 있지만 JSX 파싱 모드 때문에 generic 문법 충돌이 생기고, 파일만 보고 UI 계층과 순수 로직 계층을 구분하기 어려워진다.
백엔드와 맞춰야 할 지점
- API 응답의 nullable 필드와 optional 필드를 구분한다.
- date-only와 timestamp를 구분하고 timezone 기준을 문서에 남긴다.
- 숫자처럼 보이지만 문자열로 오는 ID, 금액, 전화번호의 타입을 합의한다.
- 에러 응답 schema를 성공 응답만큼 구체적으로 맞춘다.
- pagination과 search parameter 타입을 OpenAPI 또는 계약 테스트로 고정한다.
개인 프로젝트 기준
fetchwrapper는 작게 시작한다. base URL, JSON parse,response.ok처리, 공통 에러 변환만 넣는다.- 인증 refresh, retry, cache가 복잡해지면 API wrapper보다 TanStack Query와 auth client 책임을 나눈다.
- API 응답 타입은 서버 계약 타입, 화면 표시 타입은 view model로 나눈다.
.ts는 로직과 타입,.tsx는 JSX가 있는 컴포넌트 파일로 유지한다.- 숫자, 날짜, enum은 “서버가 어떤 문자열을 주는가”와 “화면이 어떤 타입으로 쓰는가”를 변환 함수로 분리한다.
실전 팁
- 검색 입력은 debounce와 요청 취소를 함께 고려한다.
Promise.all은 하나 실패하면 전체 실패한다. 부가 정보 요청은Promise.allSettled가 맞을 수 있다.Date객체를 만들기 전 그 값이 date-only인지 timestamp인지 먼저 판단한다.as를 쓰기 전에 “이 값은 어디에서 왔고 누가 검증했는가”를 묻는다.AbortController는 브라우저가 응답 사용을 중단하게 해주지만, 서버 작업이 실제로 취소되는지는 백엔드 구현에 따라 다르다.- 이 문서의 함정은 [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#asyncawait와-반복문을-조합할-때-무엇을-조심해야-할까|
async/await와 반복문을 조합할 때 무엇을 조심해야 할까?]], JavaScript Date와 timezone은 왜 개인 프로젝트에서도 터질까?, [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#typescript의-any-unknown-as-non-null-assertion은-언제-위험할까|TypeScript의any,unknown,as, non-null assertion은 언제 위험할까?]]에서 다시 확인한다.
위험 신호!
- API 응답을 곧바로
as SomeType으로 단언한다. forEach(async ...)뒤에 완료 처리를 둔다.- number input 값을 number라고 믿고 바로 계산한다.
- 모든 nullable 필드에 optional chaining만 붙이고 로딩, 에러, empty 경계를 만들지 않는다.
- 모든 파일을
.tsx로 만들어 generic 파싱 문제와 계층 혼란이 반복된다.
확인 질문
확인 질문
async/await가 자동으로 해결하지 않는 비동기 문제는 무엇인가?
- 병렬 처리, 취소, race condition, 일부 실패 허용, retry 정책은 별도로 설계해야 한다.
- API 응답을 안전하게 타입화하려면 어떻게 해야 하는가?
- OpenAPI 기반 타입 생성이나 Zod 같은 schema 검증으로 런타임 값을 좁힌다.
- date-only와 timestamp를 구분해야 하는 이유는 무엇인가?
- timezone 적용 여부가 달라 화면 날짜가 하루 밀리거나 잘못된 시각으로 표시될 수 있기 때문이다.