| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 알고리즘
- LG유플러스 유레카 3기 프론트엔드
- 재귀
- 프론트엔드
- 소수
- 프로세스
- Java
- 유레카 부트캠프
- 2775번 문제
- Do it! 자료구조와 함께 배우는 알고리즘 입문
- LG유플러스 유레카 프론트엔드 개발자
- LG유플러스 유레카 프론트엔드
- LG유플러스 유레카 부트캠프
- 코딩
- 브루트포스
- 정렬
- git branch 협업
- 프론트엔드 비대면반
- 멀티캠퍼스IT부트캠프
- 웹시큐리티
- 스레드
- 별찍기10
- zod
- 멀티캠퍼스IT부트캠프티
- tanstack query
- 부트캠프후기
- 백준
- 애자일
- 자바
- 시간 복잡도
- Today
- Total
개발 일기
20250902 JavaScript 비동기 본문
자바스크립트의 동기와 비동기
자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행할 수 있다. 즉, 이전 작업이 완료되어야 다음 작업을 수행할 수 있게 된다. 그런데 동기 방식은 간단하고 직관적이지만, 작업이 오래 걸리거나 응답이 늦어지는 경우에는 전체적인 성능과 사용자 경험에 영향을 줄 수 있다. 예를 들어 서버에 데이터를 요청하고 응답을 받아야 하는 작업이 있다면, 응답이 올 때까지 다른 작업을 하지 못하고 대기해야 한다.
비동기 함수 종류
1. setTimeout 및 콜백 함수
// 1. 데이터를 가져오는 비동기 함수 (3초 후 실행)
function getUserData(callback) {
console.log("사용자 데이터를 불러오는 중...");
setTimeout(() => {
const user = "홍길동"; // 3초 뒤 도착한 데이터
callback(user); // 콜백 실행
}, 3000);
}
function main() {
getUserData((user) => {
console.log("데이터를 받아왔습니다:", user);
});
console.log("다른 작업 실행 중...");
}
main();
🚩 실행 결과

- setTimeout은 비동기 함수라서 즉시 실행되는 게 아니라, 예약 후 시간이 지나면 실행된다.
- callback은 데이터를 다 가져온 시점에서 실행되어야 하기 때문에, 작업 완료 후 호출된다.
- 출력 순서가 작성한 코드 순서와 다를 수 있다. -> 비동기의 특징
위 처럼 콜백 함수는 비동기 함수에서 작업 결과를 전달받아 처리하는데 사용되어 작업 순서를 맞출 수 있게 된다. 하지만 너무 복잡하게 얽힌 비동기 처리 때문에 콜백 함수 방식은 코드 복잡도를 증가시켜, 개발자가 어플리케이션의 흐름을 읽기 어려워지는 등의 문제가 있을 수 있어 잘못하면 콜백 지옥 (callback hell)에 빠질 수 있다.

2. Promise
콜백 함수는 엄연히 말하면 비동기를 순차적으로 처리하기 위한 '편법' 같은 것이지 정식으로 지원하는 비동기 전용 함수가 아니다. 그래서 이러한 한계점을 극복하기 위해 비동기 처리를 위한 전용 객체로 Promise 객체가 탄생한다.
// 1. 데이터를 가져오는 비동기 함수 (3초 후 실행)
function getUserData() {
console.log("사용자 데이터를 불러오는 중...");
return new Promise((resolve) => {
setTimeout(() => {
const user = "홍길동"; // 3초 뒤 도착한 데이터
resolve(user); // resolve를 호출하면 then이 실행됨
}, 3000);
});
}
function main() {
getUserData()
.then((user) => {
console.log("데이터를 받아왔습니다:", user);
});
console.log("다른 작업 실행 중...");
}
main();
콜백 대신 resolve를 통해 값을 전달하고, 호출하는 쪽에서는 then으로 처리한다.
코드의 중첩이 줄어들고 가독성이 좋아지게 된다.
3. async/await
Promise도 완벽한 해결책은 아니다. Callback Hell이 있듯이 지나친 then 핸들러 함수의 남용으로 인한 Promise Hell이 존재하기 때문이다. 그래서 나오게 된 문법이 async/await이다.
function getUserData() {
console.log("사용자 데이터를 불러오는 중...");
return new Promise((resolve) => {
setTimeout(() => {
const user = "홍길동";
resolve(user);
}, 3000);
});
}
async function main() {
const user = await getUserData(); // 마치 동기 코드처럼 작성 가능
console.log("데이터를 받아왔습니다:", user);
console.log("다른 작업 실행 중...");
}
main();
위 처럼 동기 코드처럼 직관적으로 작성할 수 있게 된다.
결론
비동기를 처리함에 있어 async/await 문법이 훨씬 좋아 보이지만, 사용 방법에 따라서는 코드가 복잡해질 수도 있다. 따라서 이 비동기 처리에 대한 3가지 방식은 용도에 맞춰서 적절히 사용해야 한다.
callback hell을 맞이할 정도의 복잡한 상황이 아닐 때면 오히려 가독성이 좋아질 때도 있기 때문이다. 비교적 복잡한 비동기 작업을 처리할 때는 Promise 객체를 사용하면 코드를 보다 간결하게 작성할 수 있다.
Spring Boot를 이용한 간단한 실습
로컬 환경에서 vscode로 만든 웹페이지의 로그인 정보를 전송하고 Spring Boot 서버와 통신하며 데이터를 주고 받는 실습을 진행했다.
Spring Boot 코드
package com.ureca.web.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ureca.web.model.Member;
@RestController
@CrossOrigin({"http://127.0.0.1:5501/"})
public class MemberController {
@GetMapping("/")
public String index() {
return "index";
}
@PostMapping("/login")
public String login(@RequestBody Member m) {
// login 처리
System.out.println(m);
return "login ok";
}
}
사용 기술
- @RestController
이 클래스 안의 메서드가 REST API 엔드포인트로 동작함을 의미한다.
반환값은 HTML이 아닌 데이터(JSON, 문자열 등)로 바로 전송된다.
- @CrossOrigin
다른 도메인/포트에서 AJAX 요청이 가능하도록 허용한다.
실습에서는 http://127.0.0.1:5501에서 요청을 허용했다.
- @PostMapping
GET 요청이 들어오면 index 문자열을 반환한다.
- @RequestBody
POST 요청 처리
클라이언트가 보낸 JSON 데이터를 @RequestBody Member m로 받음.
Member 클래스 정의가 필요하다는 것을 배웠고 프론트엔드에서 JSON 형태로 /login 호출 가능하다는 것을 배웠다.
@CrossOrigin을 통해 포트가 다른 클라이언트에서도 CORS 에러 없이 요청 가능하다는 것을 알았다. form 태그를 통해 통신은 CORS의 영향을 받지 않는다.
JavaScript 코드
loginForm.addEventListener("submit", async (event) => {
event.preventDefault(); // 기본 제출 막기
const loginId = document.getElementById("id");
const loginPw = document.getElementById("password");
let id = loginId.value;
let pw = loginPw.value;
let body = JSON.stringify({
id,
pw,
});
let responseData = await fetch("http://localhost:8080/login", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body,
});
responseData = await responseData.text();
alert(responseData);
loginForm.reset();
modal.style.display = "none";
});
서버 통신에 async/await를 사용해봤다. id, pw 데이터를 받아 JSON 문자열 형태로 서버에 전송하고 잘 전송되면 alert창에 login ok가 뜨게 된다.
JavaScript 잘 사용하는 방법
1. 전역 변수 사용 금지
2. 모든 선언은 각 스크립트나 함수의 맨 위에 두는 것
3. 변수를 선언할 때 초기화하는 것
4. const로 객체, 배열 선언하기
5. new Object()를 사용하지 않기
let x1 = ""; // new primitive string
let x2 = 0; // new primitive number
let x3 = false; // new primitive boolean
const x4 = {}; // new object
const x5 = []; // new array object
const x6 = /()/; // new regexp object
const x7 = function(){}; // new function object
위의 코드처럼 방식을 리터럴이라고 한다. 리터럴은 자바스크립트 엔진에서 더 최적화되어 빠르게 처리된다. 또한 타입 혼동을 방지할 수 있다.
생성자는 내부적으로 객체를 생성하고 초기화하는 추가 과정을 거친다.
6. 자동 유형 변환에 주의하기
let x = 5 + 7; // x.valueOf() is 12, typeof x is a number
let x = 5 + "7"; // x.valueOf() is 57, typeof x is a string
let x = "5" + 7; // x.valueOf() is 57, typeof x is a string
let x = 5 - 7; // x.valueOf() is -2, typeof x is a number
let x = 5 - "7"; // x.valueOf() is -2, typeof x is a number
let x = "5" - 7; // x.valueOf() is -2, typeof x is a number
let x = 5 - "x"; // x.valueOf() is NaN, typeof x is a number
+ 연산자는 하나라도 문자열이면 문자열 연결 연산으로 동작한다.
- 연산자는 숫자형 연산만 가능하다. 만약 문자열이 있으면 숫자로 강제 변환한다.
"x"의 경우 숫자가 아니라서 변환 실패로 NaN이 된다.
7. === 비교를 사용
0 == ""; // true
1 == "1"; // true
1 == true; // true
0 === ""; // false
1 === "1"; // false
1 === true; // false
비교 연산자 == 는 항상 비교 전에 (일치하는 유형으로) 변환한다.
연산자 === 는 값과 유형을 비교한다.
8. 매개변수 기본값 사용하기
function myFunction(x, y) {
if (y === undefined) {
y = 0;
}
}
// ECMAScript 2015에서는 함수 정의에 기본 매개변수를 허용한다.
function (a=1, b=1) { /*function code*/ }
9. switch 문에 꼭 default를 사용하기
10. 숫자, 문자열, 부울을 객체로 사용하지 말 것
11. eval() 사용을 피할 것
eval() 함수는 텍스트를 코드로 실행하는 데 사용된다. 임의의 코드가 실행될 수 있으므로 보안 문제가 발생할 수도 있다.
JavaScript를 사용하며 일어나는 실수
1. 실수로 할당 연산자를 사용함
비교 연산자 대신 할당 연산자를 사용하면 예상치 못한 결과가 발생할 수 있다.
if ( 조건식 ) 의 조건식에 x = 10같은 식이 들어가면 대입된 값이 평가가 이루어져 true가 나오게 된다.
===를 사용할 것!
2. switch 문은 엄격한 비교를 사용한다.
let x = 10;
switch(x) {
case 10: alert("Hello");
}
// 실행되지 않음
let x = 10;
switch(x) {
case "10": alert("Hello");
}
3. 덧셈과 연결 혼동
let x = 10;
x = 10 + 5; // Now x is 15
let y = 10;
y += "5"; // Now y is "105"
4. Float에 대한 오해
JavaScript의 모든 숫자는 64비트 부동 소수점 숫자(Float)로 저장된다.
JavaScript를 포함한 모든 프로그래밍 언어는 정확한 부동 소수점 값을 다루는 데 어려움이 있다.
곱하고 나눌 것!
let x = 0.1;
let y = 0.2;
let z = x + y // 0.3의 결과가 나오지 않는다.
// 해결 방법
let z = (x * 10 + y * 10) / 10;
5. JavaScript 문자열 끊기
// =을 기준으로 나누면 정상적으로 할당 됨
let x =
"Hello World!";
// 효과가 없음
let x = "Hello
World!";
// 문자열에서 문장을 나눠야 하는 경우엔 백슬래시를 사용해야 한다.
let x = "Hello \
World!";
6. 세미콜론을 잘못 배치
// 세미콜론이 잘못 배치되어서 이 코드 블록은 x의 값에 관계없이 실행된다.
if (x == 19);
{
// code block
}
7. return 문
function myFunction(a) {
let power = 10
return a * power
}
function myFunction(a) {
let power = 10;
return a * power;
}
function myFunction(a) {
let
power = 10;
return a * power;
}
// 위의 코드들은 다 동일하게 작동한다. 아래 코드는 undefined를 반환한다.
function myFunction(a) {
let
power = 10;
return
a * power;
}
let만 있는 코드 줄처럼 불완전한 경우 다음 줄을 읽어 문장을 완성하려고 시도하지만 return의 경우 완전하다고 판단하기 때문에 return 이후 a * power는 무시된다.
8. 자바스크립트의 배열은 인덱스를 숫자로만 사용가능하다.
const person = [];
person["firstName"] = "John";
person["lastName"] = "Doe";
person["age"] = 46;
person.length; // person.length will return 0
person[0]; // person[0] will return undefined
9. 쉼표로 정의 끝내기
ECMAScript 5에서는 객체와 배열 정의에 끝에 쉼표를 붙이는 것이 허용된다. 하지만 JSON에서는 끝에 쉼표를 사용할 수 없다.
10. 정의되지 않음은 Null이 아니다.
undefined는 JS에서 변수, 객체, 속성, 함수 등 값이 정의되지 않은 상태이다. 선언만 하고 값을 넣지 않으면 기본적으로 undefined이다.
Null은 의도적으로 값이 없음을 나타내는 상태이다. 빈 객체나 변수에 null을 할당할 수 있다.
undefined와 null은 다르지만, 둘 다 값이 없는 상태를 나타내므로 빈 객체인 지 확인할 때 주의가 필요하다.
자바스크립트 성능 올리는 방법
1. 루프에서 활동 줄이기
for (let i = 0; i < arr.length; i++) {
// 개선한 코드
let l = arr.length;
for (let i = 0; i < l; i++) {
루프가 반복될 때마다 배열의 length 속성에 액세스해야 되기 때문에 한번만 접근하는 방식으로 개선할 수 있다.
2. DOM 접근 줄이기
const obj = document.getElementById("demo");
obj.innerHTML = "Hello";
HTML DOM에 접근하는 것은 다른 javascript 문에 비해 매우 느리다. DOM 요소에 여러 번 액세스할 것으로 예상되는 경우 한 번 액세스하여 로컬 변수로 사용하는 것이 좋다.
3. DOM 크기 줄이기
DOM 요소 수를 적게 유지하면 페이지 로딩이 개선되고 렌더링 속도가 빨라진다. 특히 작은 기기에서는 더욱 그렇다.
DOM을 검색하려고 할 때마다(예: getElementsByTagName 같은 메서드), DOM이 작으면 작을수록 훨씬 빨라진다.
4. 불필요한 변수 피하기
값을 저장할 계획이 없다면 새로운 변수를 만들지 않아야 된다.
let fullName = firstName + " " + lastName;
document.getElementById("demo").innerHTML = fullName;
// 개선한 코드
document.getElementById("demo").innerHTML = firstName + " " + lastName;
5. script를 페이지 본문의 맨 아래에 넣기
스크립트를 페이지 body 맨 아래에 넣으면 브라우저가 페이지 내용을 먼저 불러올 수 있다. 즉, 페이지 펜더링이 먼저 되고 스크립트는 나중에 실행되는 구조이다.
스크립트를 다운받는 동안 브라우저는 다른 다운로드를 시작하지 못할 수 있다. 게다가 HTML 파싱과 화면 펜더링도 잠시 막힐 수 있다.
HTTP 규격상 브라우저는 한 번에 두 개 이상의 외부 컴포넌트를 동시에 다운로드 하지 않는다. 그래서 스크립트 위치와 로딩 순서가 페이지 속도에 영향을 준다.
<script defer>를 사용하면 스크립트를 HTML 파싱이 끝난 뒤 실행하도록 할 수 있다. 단, 외부 스크립트에만 적용 가능.
😵💫 느낀 점
오늘 학습한 내용의 양 자체가 많은 편이었다. 그래서 실습하면서 진행이 되어 재미가 있었고 이해도 쉽게 했다.
비동기 자체에 대한 이해도 늘었고 자바스크립트의 비동기 처리 방식에 대해서도 Spring Boot를 이용해 데이터 교환을 하며 쉽게 접근할 수 있었다.
참고 자료 :
'TIL' 카테고리의 다른 글
| 20250904 TypeScript (0) | 2025.09.04 |
|---|---|
| 20250903 TypeScript (0) | 2025.09.04 |
| 20250901 JavaScript 화살표 함수와 this (0) | 2025.09.01 |
| 20250829 JavaScript (1) | 2025.08.31 |
| 20250828 자바스크립트 기본을 배웠다. (0) | 2025.08.28 |