js의 타이머는 실행시간을 보장하지 못한다

자바스크립트에서 타이머가 동작하는 방식을 정리해보았다.
자바스크립트의 싱글스레드 이해해도 많은 도움이 되는 것 같다.

타이머 함수

자바스크립트 타이머 관련 함수는 3 가지가 있다.
하지만 자바스크립트는 해당 delay의 정확한 시간을 보장하지 않는다.

또한 타이머는 외부의 웹 api로 브라우저에서 제공한다.

1
2
3
4
5
6
let id = setTimeout(fn, delay);
// delay 만큼의 지연 후 fn을 호출한다. 나중에 취소할 수 있는 고유 ID를 반환한다.
let id = setInteral(fn, delay);
// setTimeout과 유사하지만 delay 마다 fn을 계속 호출한다.
clearInterval(id);
// id에 해당하는 타이머의 콜백을 발생하지 않도록 한다.

싱글스레드로 인한 실행예시

이는 자바스크립트가 싱글스레드로 동작하기 때문이다.
이는 한 번에 하나의 코드블럭만 실행할 수 있다는 것을 의미한다.

이는 실행 가능할 때(빈 자리가 있을 때)만 실행된다.

아래의 그림은 이를 설명하는 그림이다.

call stack

위에서부터 실행된다.
첫 번째 자바스크립트 블록은 18ms동안 실행되며 마우스 클릭 블록은 11ms동안 실행된다.

첫 번째 자바스크립트 블럭의 실행순서는 다음과 같다.

  1. setTimeout 타이머 설정
  2. 마우스 클릭 이벤트 발생
  3. setInterval 타이머 설정
  4. setTimeout 실행

하지만 1,2는 비동기 이벤트이고 현재 자바스크립트 블럭의 실행이 끝나지 않았으므로 즉시 실행되지 않고 큐에 넣어서 대기한다.

자바스크립트 블록의 실행이 끝났을 경우 내부에선 대기중인 큐를 확인한다.
이 과정이 what's waiting이다.
확인에도 시간이 소요된다.

그 다음 큐에서 클릭 핸들러를 가져와 실행하여 Mouse Click Callback 블럭이 생긴다.

Mouse Click Callback 실행중에
setInterval이 발생하여 대기중인 큐에 넣는다.

Mouse Click Callback의 실행이 끝나게 되면,
내부에서 다시 큐를 확인하여 setTimeout을 실행한다.

setTimeout의 실행 중에 setInterval이 발생하지만 이미 큐에는 setInterval이 대기중이므로 이는 무시된다.

그 후 setTimeout의 실행이 끝난 뒤에 발생한 setInterval은 what's Waiting? 없이 즉시 실행된다.(실행중인 블록과 대기중인 큐에 아무 것도 없기 때문이다.)

정리

  1. 자바스크립트 엔진은 싱글스레드로 동작하여 비동기 이벤트를 큐에 넣는다.
  2. 타이머 함수는 웹 api로 브라우저에서 제공하여 외부에서 delay마다 큐에 넣는다.
  3. setInterval은 마지막 콜백에 상관없이 delay마다 웹API에서 실행을 시도한다. 이미 큐에 있을 경우 무시된다.
  4. setTimeout은 웹 API에서 delay마다 실행을 시도하지만 실행되고 있는 콜백이 있을 경우 해당 콜백의 실행이 끝날 때까지 큐에서 대기한 후 실행을 시도한다. 때문에 delay만큼의 시간을 보장할 수 없다.

ref

How JavaScript Timers Work
자바스크립트 타이머는 지각쟁이? 그 이유는 싱글 콜 스택?

ts+react에서 이벤트 객체의 타입 지정하기

ts+react를 사용할 때에 HTMLEvent의 타입을 지정하는 방법에 대해서 간단하게 정리해보았다.

React의 Event의 타입

Event의 타입은 React안에 정의되어있다.

onchange 이벤트의 타입은 React.ChangeEvent<T>이다.
onclick 이벤트의 경우 React.MouseEvent<T>이다.
제네릭에는 이벤트를 발생시키는 요소를 전달하면 된다.

예시

자주 쓰이는 useInput hooks이다.

이벤트의 타입을 React.ChangeEvent<HTMLInputElement>라고 명시해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useCallback } from "react";

type UseInputTypes = [
string,
(e: React.ChangeEvent<HTMLInputElement>) => void,
React.Dispatch<React.SetStateAction<string>>
];
const useInput = (initialValue: string): UseInputTypes => {
const [value, setValue] = useState<string>(initialValue);

const onChangeValue = useCallback((e) => {
setValue(e.target.value);
}, []);
return [value, onChangeValue, setValue];
};

export default useInput;

next.config.js 여러 플러그인 연결하기

다음 플러그인 사용하기
https://www.npmjs.com/package/next-compose-plugins

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
const withTM = require("next-transpile-modules")([
"@fullcalendar/common",
"@babel/preset-react",
"@fullcalendar/common",
"@fullcalendar/daygrid",
"@fullcalendar/interaction",
"@fullcalendar/react",
"@fullcalendar/timegrid",
]);

const withPlugins = require("next-compose-plugins");

const nextConfig = {
async rewrites() {
return [
{
source: "/:path*",
destination: `https://dev.aistudios.com/:path*`,
},
];
},
};

module.exports = withPlugins([withTM({})], nextConfig);
// withPlugins([plugins],nextConfig);

nextjs css-npm 에러

next.js의 경우 node_modules내부의 패키지에서 전역으로 css를 import하면 에러를 발생시킨다.
구글링을 통해 해결하였다.

1
./node_modules/@fullcalendar/common/main.css Global CSS cannot be imported from within node_modules. Read more: https://err.sh/next.js/css-npm Location: node_modules/@fullcalendar/common/main.js

해결방법

1
npm i next-transpile-modules @babel/preset-react

next-transpile-modules 는 node_modules 내부의 패키지에 css/scss파일이 포함 될 수 있도록 transpile 하는 플러그인이다.

이후 next.config.js 파일을 다음과같이 작성해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('next').NextConfig} */
const withTM = require("next-transpile-modules")([
"@fullcalendar/common",
"@babel/preset-react",
"@fullcalendar/common",
"@fullcalendar/daygrid",
"@fullcalendar/interaction",
"@fullcalendar/react",
"@fullcalendar/timegrid",
]);

module.exports = withTM({
// your custom config goes here
});

개선방안

공부할 예정인 next-compose-plugins를 통해 간편하게 여러 플러그인을 import 할 수 있다.

ref

next css-npm
How To Use Fullcalendar With Next.js (v10 or higher)

typescript ReactElement ReactNode JSX.Element 차이

render

render() 함수는 다음 중 하나를 반환하여야한다.

  1. React Element
  • JSX를 사용하여 생성된다.
  1. 배열과 Fragment
  • render()를 통하여 여러개의 Element를 반환한다.
  1. Portal
  • 별도의 DOM 하위트리에 자식 엘리먼트를 렌더링한다.
  1. 문자열과 숫자
  • 이 값들은 DOM상에 텍스트 노드로서 렌더링된다.
  1. Boolean 또는 null
  • 아무것도 랜더링 하지 않는다.

render()는 순수한 함수여야 한다.
컴포넌트의 state를 변경하지 않고 호출될 때마다 동일한 결과를 반환해야한다.
또한 브라우저와 직접적으로 상호작용해서는 안된다.

ReactNode

ReactNode는 ReactChild 등등 많은 것을 포함한다.
제일 범용적으로 사용할 수 있는 것 같다..!(다른 관점에선 리펙토링 할 수 있는 부분)

클래스형 컴포넌트의 반환값이다!

1
2
3
4
5
6
7
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;

ReactElement

ReactElement는 type, props, 그리고 key를 가진 객체이다.

예전 게시글에서 알아본 것 처럼 함수형 컴포넌트의 반환값이다!

1
2
3
4
5
6
7
8
9
10
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T;
props: P;
key: Key | null;
}

JSX.Element

JSX.Element는 any type의 generic type인 ReactElement이다.
전역으로 선언되어있어 라이브러리들이 구현하여 사용할 수 있다.

1
2
declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { }

++추가 React.FC

React.FCchildren이 포함되어있어 컴포넌트의 props의 타입 처리가 어렵다.

차라리 위의 ReactElementReactNode를 적용 후 children의 타입을 명시하는 것이 좋다..!

git push시 none-fast-forward 문제 해결하기

단위별로 push하던 중에 다음과 같은 에러가 발생하였다.

1
2
3
4
5
6
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/Deep-brain-Academy-2
hint: Updates were rejected because the tip of your current branch is behin
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

기존에 게시글로 작성하였던 git stash를 사용하여 해결하였다.

1
2
3
4
5
git add .
git commit -m "commit msg"
git stash
git pull origin main
git push origin

Mongoose 사용하기

MongoDB의 ODM인 mongoose를 express.js와 함께 사용해보았다.

폴더구조

1
2
3
4
5
6
7
8
9
10
11
12
13
back
├── models
│ └── user.js
├── router
│ └── user.js
├── node_modules
├── app.js
├── mongo.js
├── nodemon.json
├── package.json
├── package-lock.json
├── .env
└── .gitignore

model을 정의하기 위한 models폴더를 만들었다.
또한 루트 디렉터리에 MongoDB와 연결하기 위한 mongo.js파일을 만들었다.

설치

mongoose를 사용하기 위해 mongoose를 설치하였다.
또한 .env파일을 작성하기 위해 dotenv를 설치해주었다.

1
2
npm i mongoose
npm i dotenv

dotenv

내 데이터베이스의 주소인 MONGO_URI,
개발인지 배포인지 나타낼 NODE_ENV
그리고 express에서 사용할 포트넘버인 PORT를 작성해주었다.

.env

1
2
3
MONGO_URI=mongodb+srv://<username>:<password>@<clustername>.zdhw0.mongodb.net/<databasename>?retryWrites=true&w=majority
NODE_ENV="development"
PORT=8000

Mongoose를 express와 연결하기

MongoDB와 연결하는 함수를 모듈화 하였다.

./mongo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mongoose = require("mongoose"); //모듈을 불러옴
require("dotenv").config(); //.env를 사용하기 위해 불러옴

function dbConnect() {
if (process.env.NODE_ENV !== "production") {
mongoose.set("debug", true);
}
// production이 아닐 경우 debug가 true가 되도록 하였다.
mongoose.Promise = global.Promise;
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("Successfully connected to mongodb!"))
.catch((e) => console.error(e));
}
// 연결 후 콘솔에 출력하도록 하였다.

module.exports = { dbConnect };
// app.js에서 사용할 수 있도록 하였다.

그 후 app.js에서 호출하여 데이터베이스와 연결 해주었다.

./app.js

1
2
3
4
5
6
7
...

const { dbConnect } = require("./mongo");
dbConnect();

...

Model과 Schema

Mongoose는 nosql인 MongoDB에 스키마를 도입하여 데이터를 DB에 넣기 전에 검사한다. 때문에 제약은 있지만 안정성을 더하였다.

다음과 같이 UserSchema를 작성하였다.

./models/user.js

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
const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
maxlength: 60,
},
username: {
type: String,
require: true,
maxlength: 20,
},
password: {
type: String,
require: true,
maxlength: 150,
},
},
{ timestamps: true }
);

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);
// model함수를 호출하면 Schema가 등록된다.
// 어플리케이션에서 사용하기 위한 모델을 반환한다.

사용자 정의 메소드 정의하기

Mongoose에선 Schema혹은 모델의 인스턴스인 Document 객체 내부에 다양한 메서드를 정의하고 사용할 수 있다.

methods

mongoose는 스키마나 다큐먼트에 사용자 정의 메소드를 추가할 수 있다.

Schema 객체의 methods에 접근하여 함수를 추가해주었다.

내부의 this를 통해 Document에 접근할 수 있다.
때문에 화살표 함수를 사용하면 안 된다.

./models/user.js

1
2
3
4
5
UserSchema.methods.methodName = function () {
const document = this; //document 하나
...
return result;
};

호출시엔 새로운 Document를 만들어서 호출할 수 있다.

./models/user.js

1
2
const user=new User({...});
user.methodName();

statics

mongoose는 모델이나 쿼리에도 사용자 정의 메소드를 추가할 수 있다.

Schema 객체의 statics에 접근하여 함수를 추가해주었다.

내부의 this를 통해 Schema Collection에 접근할 수 있다.
마찬가지로 화살표 함수를 사용하면 안 된다.

나는 유효성 검사 메소드를 statics메서드로 정의해주었다.
이럴 경우 코드가 훨씬 깔끔해지는 것 같다.

1
2
3
4
5
UserSchema.statics.staticMethodName = function () {
const SchemaCollection = this; //Schema 전체
// ...
return result;
};

호출시엔 새로운 Document를 만들지 않고 User Colloection에 접근할 수 있다.

1
2
const user=new User({...});
user.staticMethodName();

느낀점

자질구레한 오타로 에를 좀 먹어서 다음엔 typescript도 같이 적용해보아야겠다.

ref

Mongoose(몽구스) 스키마(Schema)
Mongoose 스키마& 쿼리 기본
Mongoose 공식문서

React DOM의 render와 hydrate

next를 사용하던 도중 SSR(Server Side Rendering)에서 사용되는 hydrate라는 키워드에 대해 공부하고 싶어져서 공부해보았다.

react-dom은 앱의 최상위 레벨에서 사용하는 DOM과 관련한 메서드와 React 모델 외부로 나갈 수 있는 해결책을 제공한다.

cra로 만든 프로젝트 기준 최상위 레벨인 index.js에서 사용된다.
render 메소드를 제공해주는 그 패키지이다.

1
2
3
4
5
6
7
8
9
import React from "react";
import ReactDOM from "react-dom";
import App from "./APP";

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

render

1
ReactDOM.render(element:React$Element<any>,container:Container,callback:?Function,)

renderelementcontainer DOM에 렌더링하고 컴포넌트에 대한 참조(Ref)를 반환한다.

callback이 제공되면 렌더링되거나 업데이트 된 후에 실행된다.

element가 이전에 container내부에 렌더링 되었다면 해당 엘리먼트는 업데이트하고 최신의 React 엘리먼트를 반영하는 데 필요한 DOM만 변경한다.

일전에 공부하였던 diff 알고리즘React fiber를 통하여 DOM에 변경점만 업데이트 하거나 렌더링하는 함수이다.

코드

ReactDOM에서 render 함수를 볼 수 있다.

동작방식은 다음과 같다.

  1. container가 DOM 엘리먼트인지 확인하고 아닐경우 에러를 발생시킨다.

  2. root 엘리먼트는 _reactRootContainer 속성을 가지고 있다.
    또한 container는 __reactContainere+$랜덤 문자열 속성도 가지고 있다.
    다음 경우 에러를 발생시킨다.

    1. container__reactRootContainer 속성이 undefined일 경우
    2. container 내부에 __reactContainerer+$랜덤 문자열 속성이 존재하지 않을 경우
  3. 2까지 에러가 발생하지 않았을 경우 DOM 엘리먼트인 containersub tree로 React 엘리먼트인 element를 render한다.

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
31
32
33
34
35
36
37
38
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function
) {
if (__DEV__) {
console.error(
"ReactDOM.render is no longer supported in React 18. Use createRoot " +
"instead. Until you switch to the new API, your app will behave as " +
"if it's running React 17. Learn " +
"more: https://reactjs.org/link/switch-to-createroot"
);
}

if (!isValidContainerLegacy(container)) {
throw new Error("Target container is not a DOM element.");
}

if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
"You are calling ReactDOM.render() on a container that was previously " +
"passed to ReactDOM.createRoot(). This is not supported. " +
"Did you mean to call root.render(element)?"
);
}
}
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback
);
}

주의사항

공식문서에 나와있는 주의사항은 다음과 같다.

  1. ReactDOM.render()는 처음 호출할 때 기존의 DOM 엘리먼트를 교체하며 이후의 호출은 React의 DOM diffing 알고리즘을 사용하여 더욱 효율적으로 업데이트한다.

  2. ReactDOM.render()는 컨테이너 노드를 수정하지 않고 컨테이너의 하위 노드만 수정하여 자식 노드를 덮어쓸 필요 없이 기존의 DOM 노드에 컴포넌트를 추가할 수 있다.

  3. ReactDOM.render()는 현재 ReactComponent 루트(root) 인스턴스에 대한 참조를 반환한다. 그러나 이 반환 값을 사용하는 것은 피해야한다.
    ReactComponent 인스턴스의 참조가 필요하다면 권장하는 해결책은 루트 엘리먼트에 콜백 ref를 붙여야한다.

  4. ReactDOM.render()를 사용해 서버에서 렌더링한 컨테이너에 이벤트를 보충할 때는 hydrate()를 사용해야 한다.

hydrate

1
ReactDOM.hydrate(element:React$Element<any>,container:Container,callback:?Function,)

위의 render와 동일하지만 HTML이 ReactDOMServer로 렌더링 된 후 컨테이너에 이벤트를 보충하기 위해 사용된다.
React는 기존 마크업에 이벤트 리스너를 연결한다.

서버에서 온 정적인 HTML코드에 React 코드들을 렌더링하여 리액트가 관리할 수 있도록 컴포넌트화 하는 함수이다.

SSR을 하는 경우에는 hydrate로 콜백만 붙여야 한다.(고한다.)

코드

마찬가지로 ReactDOM에서 hydrate 코드를 볼 수 있다.

render와 동작 방식이 똑같다.

다만 render와 달리legacyRenderSubtreeIntoContainer 함수에서 forceHydrate가 true이다.

1
2
3
4
5
6
7
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback
);
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
31
32
33
34
35
36
37
38
39
export function hydrate(
element: React$Node,
container: Container,
callback: ?Function
) {
if (__DEV__) {
console.error(
"ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot " +
"instead. Until you switch to the new API, your app will behave as " +
"if it's running React 17. Learn " +
"more: https://reactjs.org/link/switch-to-createroot"
);
}

if (!isValidContainerLegacy(container)) {
throw new Error("Target container is not a DOM element.");
}

if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
"You are calling ReactDOM.hydrate() on a container that was previously " +
"passed to ReactDOM.createRoot(). This is not supported. " +
"Did you mean to call hydrateRoot(container, element)?"
);
}
}
// TODO: throw or warn if we couldn't hydrate?
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback
);
}

ref

리액트 공식 문서
리액트의 hydration이란?

Forwading Refs

next.js를 사용하던 중 next/link의 Link 에서 forwarding Ref를 사용하라는 에러를 받았다.

그래서 알아보기로 하였다.

DOM 에 ref 전달하기

아래 코드는 button DOM 요소를 렌더링하는 FancyButton 컴포넌트이다.

1
2
3
function FancyButton(props) {
return <button className="FancyButton">{props.children}</button>;
}

FancyButton 컴포넌트의 경우 button이나 input같은 DOM요소와 같이 어플리케이션 전체에 걸쳐서 재사용될 것이다.

그리고 포커스, 선택, 애니메이션을 관리하기 위해서는 이런 DOM 노드에 접근하는 것이 불가피하다.

React.forwardRef

React.forwardRef를 이용하면 ref를 button DOM 요소로 직접 전달할 수 있다.

1
2
3
4
5
6
7
8
9
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

const ref = React.createRef();
// 또는 const ref= useRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

요약

ref를 전달하기 위해선 ref라는 속성명 외의 다른 속성명을 사용해서 전달해야 한다.
하지만 React.forwardRef를 사용하면 ref를 props로 전달할 수 있다!

주의사항

컴포넌트 라이브러리에서 fowardRef를 사용하기 시작할 때 변경사항으로 간주하고 새로운 중요 버전을 릴리즈해야한다.

또한 forwardRef가 존재할 때 조건부로 적용하는 것도 권장되지 않는다.

js Error

과거 자바스크립트가 아닌 다른 언어로 프로그래밍 할 때도 에러를 주로 caller에게 넘겨서 처리하곤 했었다.

자바스크립트로 프로그래밍 하다보면 에러를 발생시킨 caller를 차례대로 콘솔에 찍어준다.
이에 대한 정확한 로직이 궁금하여 공부 해보았다.

Error

Error객체는 런타임 오류가 발생했을 떄 던져진다.
Error 객체를 사용자 지정 예외의 기반 객체로 사용할 수 있다.

Error() 생성자를 통해 Error 객체를 생성할 수 있다.

Error 생성하기

throw 문을 통해 예외를 발생시킬 수 있다.

1
2
3
4
5
6
try {
throw "new Error";
} catch (e) {
console.error(e);
}
//new Error

다음과 같이 catch문을 작성하지 않아도 예외는 발생한다.

1
2
3
4
5
6
7
8
9
function sum(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
throw "숫자가 아닙니다.";
}
return x + y;
}

console.log(sum(1, 2)); //3
console.log(sum("a", 2)); // Uncaught 숫자가 아닙니다.

콜스택과 에러

throw로 예외를 발생 시켰을 때는 현재 함수의 실행이 중지된다.
그 후 콜스택의 첫번째 catch 블록으로 전달된다.
만약 catch 블록이 존재하지 않을 경우 프로그램이 종료된다.

예외처리를 해주지 않았을 경우

아래의 코드는 예외처리를 해주지 않은 코드이다.
이 경우엔 콜스택의 첫 번째 요소부터 catch 블록을 탐색하고 처리될 때 까지 콜 스택을 차례대로 비우게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f2() {
console.log("f2 start");
throw "에러발생";
console.log("f2 end");
}

function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}

console.log("will:f1");
f1();
console.log("did: f1");

실행 순서는 다음과 같다.

  1. 콜스택이 비어있기 때문에 익명함수 호출
  2. will:f1
  3. 익명함수가 f1호출
  4. f1 start
  5. f1이 f2 호출
  6. f2 start
  7. Uncaught 에러발생
  8. 에러가 발생하며 처리될 떄 까지 호출스택을 비움

예외처리를 해주었을 경우

아래의 코드는 try catch 구문을 통해 예외처리를 해주었다.
때문에 아래의 부분은 파괴되지 않고 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f2() {
console.log("f2 start");
throw "에러발생";
console.log("f2 end");
}

function f1() {
console.log("f1 start");
try {
f2();
} catch (e) {
console.log(e);
}
console.log("f1 end");
}

console.log("will:f1");
f1();
console.log("did: f1");
  1. 익명함수 호출
  2. will:f1
  3. 익명함수가 f1호출
  4. f1 start
  5. f1이 f2 호출
  6. f2 start
  7. 에러발생
  8. f1 end
  9. did: f1

Error 객체

아래 코드는 Error 객체를 이용하여 예외를 발생시켰을 경우이다.
이럴 경우 에러가 발생했을 당시의 콜스택 상황을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f2() {
console.log("f2 start");
throw new Error("에러");
console.log("f2 end");
}

function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}

console.log("will:f1");
try {
f1();
} catch (e) {
console.log(e);
}
console.log("did: f1");
  1. 익명함수 호출
  2. 익명함수가 f1호출
  3. f1 start
  4. f1이 f2 호출
  5. f2 start
  6. 에러 + 콜스택 정보( f2에서의 콜스택 상황을 반환함)
  7. did: f1

Promise의 에러처리

Promise 내부에선 두 번쨰 인자인 reject를 이용하여 에러를 발생시킬 수 있다.

하지만 예외를 발생시킨 타이밍이 try catch 블럭이 아니다.
비동기 호출이기 때문에 콜스택이 비었을 때 타이머가 큐에 넣고 해당 큐의 내용이 콜스택이 쌓인 것이라 try 구문에서 catch가 안 된다.

따라서 실행 결과는 Uncaught (in Promise) error!이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}

try {
wait(3);
} catch (e) {
console.error(e);
}

Promise의 catch 함수

Promise의 호출뒤에 .catch(callback)을 통해 에러가 발생했을 떄 실행시킬 함수를 작성할 수 있다.

callback의 인자는 Promise내부에서 reject된 값이다.

아래 코드의 실행결과는 1st catch error!이다.

1
2
3
4
5
6
7
8
9
10
11
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}

wait(3).catch((e) => {
console.log("1st catch", e);
});

async/ await

async 함수의 리턴은 Promise이다.
따라서 Promise와 같은 방법으로 예외처리를 할 수 있다.

차이점은 await 키워드이다.

awaitasync함수 안에서만 사용할 수 있다.
Promisefullfill되거나 reject될 때 까지 async함수의 실행을 일시 정지한다.

await을 쓰지 않을 경우

아래의 코드는 await을 사용하지 않은 코드로 Promise와 같이 예외처리 해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function myAsyncFunc() {
throw "myAsyncError!";
}

function myPromiseFun() {
return new Promise((resolve, reject) => {
reject("myError");
});
}

const result = await myAsyncFunc().catch((e) => {
console.log(e);
});
// myAsyncError!
const result2 = myPromiseFun().catch((e) => {
console.log(e);
});
// MyError!

await을 쓸 경우

아래의 코드는 await을 사용한 코드로 myAsyncFun 함수는 wait 함수의 Promisefullfill 될 때까지 함수의 실행을 일시정지한다.
때문에 try catch 구문을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function wait(sec) {
return new Promise((resolve) => {
setTimeout(() => {
reject("wait Error");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
try {
await wait(3);
} catch (e) {
console.error(e);
}
console.log(new Date());
}
const result = myAsyncFunc();
//현재시간, 3초뒤 에러

느낀점

비동기 함수의 예외처리는 Promise Chaining을 고려하면 된다.

ref

throw
코드종(설명이 직관적이고 친절하시다..!)
await