애플리케이션의 비동기 처리는 빈번하게 발생하므로 비동기 요청의 응답을 기다리는 동안 사용자에게 로딩을 표시해줘야한다. 재사용이 가능한 컴포넌트로 로딩 컴포넌트를 만들어보자.

✏️ 접근성 고려

접근성을 고려하였을 때, 스크린 리더가 로딩중이 시작할 때와 로딩이 종료되었을 때를 읽을 수 있도록 하기 위해서는 public 폴더에 index.html 파일에 다음과 같이 기재가 되어있어야한다.

1
2
3
4
5
6
// public/index.html
<body>
<!-- 로딩 스피너 접근성을 위한 DOM 요소를 추가하세요. -->
<div id="loading-start" aria-live="assertive"></div>
<div id="loading-end" aria-live="assertive"></div>
</body>
  • aria-live="assertive"속성을 주어 다른 것보다 우선적으로 스크린 리더가 읽도록 설정해준다.

예제

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// Spinner.js
const loadingElements = {
start: document.getElementById("loading-start"),
end: document.getElementById("loading-end"),
};

export class Spinner extends React.Component {
static defaultProps = {
type: "connect", // 'grow', 'learn', 'connect'*
message: "로딩 중...",
showMessage: true,
timeToDisappear: {
start: 1500,
end: 2500,
},
};

render() {
const { type, message, showMessage } = this.props;
const spinnerImagePath = getAsset(`spinner/spinner-${type}.gif`);

return (
<figure className={styles.container}>
<img className={styles.image} src={spinnerImagePath} alt="" />
{showMessage ? (
<figcaption>{message}</figcaption>
) : (
<A11yHidden as="figcaption">{message}</A11yHidden>
)}
</figure>
);
}

componentDidMount() {
const { start } = loadingElements;
start.setAttribute("role", "alert");
start.insertAdjacentHTML(
"beforeend",
`<span class="a11yHidden">${this.props.message}</span>`
);
}

componentWillUnmount() {
const { start, end } = loadingElements;
const { timeToDisappear } = this.props;

setTimeout(() => {
start.removeAttribute("role");
start.innerHTML = "";
end.insertAdjacentHTML(
"afterbegin",
`<span class="a11yHidden">로딩이 종료되었습니다.</span>`
);
}, timeToDisappear.start);
setTimeout(() => {
end.innerHTML = "";
}, timeToDisappear.end);
}
}
  1. loading 요소를 반복적으로 사용할 것이기 때문에 최상단에 객체의 프로퍼티로 등록시켜주었다.
  2. Spinner 컴포넌트의 기본 props값을 설정해주었다.
    • 이는 컴포넌트를 만든 사람만 알 수 있기때문에 문서화를 하거나 TypeScript를 사용하여 개발자 경험(DX)를 높일 수 있다.
  3. 로딩중이라는 메시지를 보여주는 경우와 그렇지 않는 경우를 나누었다. 보여주지 않는 경우에는 접근성 컴포넌트로 생성하여 스크린 리더에는 읽히도록 설정해주었다.
  4. 컴포넌트가 mounted 될 때, role="alert"속성을 주어 스크린 리더가 읽고 있는 것을 중지하고 로딩중을 읽도록 설정하였다.
  5. StrictMode 에서는 mounted - unmounted - mounted 되는 특징때문에 2번 작동할 수 있으므로 성능을 고려하여 clean Up을 해줘야한다.

댓글 공유

Portal(포털)

카테고리 React

📌 Portal이란?

포털을 사용하면 애플리케이션 영역을 벗어나 특정 위치에 컴포넌트를 렌더링할 수 있다. 즉, 지금껏 root 컨테이너에만 렌더링을 해왔다면 포털을 사용하여 root 컨테이너 외부에다가도 컴포넌트를 렌더링할 수 있게된다.

포털을 통해 렌더링된 컴포넌트는 DOM 트리 위치와 상관없이 React 컴포넌트 트리에 포함되기 때문이다.

🔨 사용방법

1
ReactDOM.createPortal(child, container);
  • child는 렌더링할 수 있는 자식
  • container는 DOM 요소이다.
1
2
3
4
5
6
render() {
return ReactDOM.createPortal(
this.props.children,
domNode
)
}
  • 위 경우 React는 새로운 div를 생성하지 않고 domNode 안에 자식을 렌더링한다.
  • domNode는 DOM 내부의 어디에 있던지 간에 상관없다.

시각적으로 자식을 튀어나오도록 보여야하는 다이얼로그, 호버카드, 툴팁에 사용된다. 이 때, 키보드 포커스 관리와 접근성을 고려해줘야한다.

🌈 다이얼로그 예시

다이얼로그 특징

  1. 다이얼로그가 띄워진 상태면 그 아래 위치한 내용은 비활성화 상태여야한다.
  2. 다이얼로그 바깥으로 초점이동되면 안된다.
  3. dialog 역할(role)을 부여해야한다.
  4. 모달 기능일 경우 aria-modal=true 이여야한다.
  5. 다이얼로그 제목은 aria-label, aria-labelledby로 설정한다.

1. 부모 컴포넌트

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
40
41
42
43
44
45
46
47
export class DemoDialog extends React.Component {
#opennerRef = React.createRef(null);

state = {
show: false,
};

handleShowDialog = () => {
this.setState({ show: true });
};

handleHideDialog = () => {
this.setState({ show: false });
};

render() {
return (
<div className={styles.box}>
<button
ref={this.#opennerRef}
type="button"
className={styles.openDialogButton}
aria-haspopup="dialog"
aria-label="모달 다이얼로그 열기"
title="모달 다이얼로그 열기"
onClick={this.handleShowDialog}
>
모달열기
</button>
{this.state.show && (
<Dialog
modal
onClose={this.handleHideDialog}
openner={this.#opennerRef.current}
>
<Dialog.Header>
<h3>불금 다이얼로그</h3>
</Dialog.Header>
<Dialog.Body>
<ul>...</ul>
</Dialog.Body>
</Dialog>
)}
</div>
);
}
}
  • 부모 컴포넌트에서는 버튼 클릭이벤트로 모달을 조건부 렌더링해주고 있다.

2. 모달 컴포넌트(자식)

root 요소가 아닌 곳에 자식 컴포넌트를 그려주기 위해서 public 폴더의 index.html에 다음과 같이 div 요소를 추가해줘야한다.

1
2
3
4
5
6
7
// public/index.html
<body>
<div id="root"></div>

<!-- 다이얼로그를 렌더링 할 컨테이너 요소 -->
<div id="dialogZone"></div>
</boyd>
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
// Dialog.jsx
const { documentElement: htmlElement } = document;
const reactDomContainer = document.getElementById("root");

export class Dialog extends React.Component {
#containerRef = React.createRef(null);

handleClose = () => {
this.props.onClose?.();
this.props.openner.focus();
};

render() {
const { modal = false } = this.props;

return createPortal(
<>
<article
ref={this.#containerRef}
tabIndex={-1}
role="dialog"
aria-modal={modal}
className={styles.container}
>
{this.props.children}
<Dialog.Footer onClose={this.handleClose} />
</article>
{modal && <div className={styles.dim} onClick={this.handleClose} />}
</>,
document.getElementById("dialogZone")
);
}
}
  • 우선 해당 모달 컴포넌트를 그릴 domNode를 htmlElement로 지정해주었다.
  • 컨테이너 DOM 요소 가져오기 위해서 ref를 생성해주었다.
  • createPortal()에 렌더링할 자식 컴포넌트와 자식 컴포넌트를 렌더링할 컨테이너를 전달하였다.
  • 모달이 켜져있을 때, Dialog.footer 자식 요소에게 onClose를 props로 전달하였다.

3. 자식 컴포넌트 접근성 고려

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export class Dialog extends React.Component {
...

#tabbableElements = [];

#bindEscKeyEvents() {
const handler = (e) => {
if (e.key.toLowerCase().includes("escape")) {
console.log("pressed esc key");
this.handleClose();
}
};

document.addEventListener("keyup", handler);

// cleanup function
return () => document.removeEventListener("keyup", handler);
}

#unbindEscKeyEvents = null;

componentDidMount() {
this.#containerRef.current.focus();
this.#tabbableElements = getTabbableElements(this.#containerRef.current);
this.settingKeyboardTrap();

htmlElement.style.overflowY = "hidden";
reactDomContainer.setAttribute("aria-hidden", true);

this.#unbindEscKeyEvents = this.#bindEscKeyEvents();
}

componentWillUnmount() {
htmlElement.style.overflowY = "visible";
reactDomContainer.setAttribute("aria-hidden", false);

this.#unbindEscKeyEvents?.();
}

settingKeyboardTrap() {
const tabbles = this.#tabbableElements;
const firstElement = tabbles[0];
const lastElement = tabbles[tabbles.length - 1];

firstElement.addEventListener("keydown", (e) => {
if (e.shiftKey && e.key.toLowerCase().includes("tab")) {
e.preventDefault();
lastElement.focus();
}
});

lastElement.addEventListener("keydown", (e) => {
if (!e.shiftKey && e.key.toLowerCase().includes("tab")) {
e.preventDefault();
firstElement.focus();
}
});
}
}
  • key 이벤트를 등록해주고 제거해줄 때, 클로저를 사용하여 이벤트 핸들러를 동일한 참조값으로 일치시켜주면 편하다.
    • keydown 이벤트를 사용해야지만 e.preventDefault()기본동작을 막을 수 있다.
  • 모달 컴포넌트가 생성되었을 때, 해당 컨테이너(article)에 초점을 가게하기 위해 ref를 전달해준 것이다.
  • htmlElement.style.overflowY = "hidden"로 모달 컴포넌트가 띄워져있을 때, 다른 요소는 스크롤이 비활성화시켜주었다.
  • 모달이 켜져있으면 root 컨테이너는 aria-hidden을 스크린 리더기에서도 모달만 읽히도록 해줘야한다.
  • 컴포넌트가 소멸되기 직전에 등록했던 key 이벤트를 제거해줘야하는 것을 잊지 말자. (고려하지 않는다면 성능에 문제가 생길 것이다.)

4. 모달 slot 구분

어떤 컴포넌트들은 어떤 자식 요소가 들어올지 예상할 수 없는 경우가 있다. 이럴 경우 children prop을 사용하여 자식 요소를 출력에 그대로 전달하는 것이 좋다.

합성(composition)을 사용하여 컴포넌트 간에 코드를 재사용하도록 하자.

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
Dialog.Header = function DialogHeader({ children }) {
return <header className={styles.header}>{children}</header>;
};

Dialog.Header.defaultProps = {
children: <h2>React 포털로 연 다이얼로그(with 모달)</h2>,
};

Dialog.Body = function DialogBody({ children }) {
return <div className={styles.body}>{children}</div>;
};

Dialog.Footer = function DialogFooter({ children, onClose }) {
return (
<footer className={styles.footer}>
<button
type="button"
className={styles.closeButton}
aria-label="모달 다이얼로그 닫기"
title="모달 다이얼로그 닫기"
onClick={onClose}
>
모달닫기
</button>
{children}
</footer>
);
};

댓글 공유

DOM 컨트롤(ref)

카테고리 React

🕹 리액트 DOM 컨트롤

리액트 앱은 대부분 리액트에 의해 컨트롤되지만 예외인 상황이 있다. 리액트는 virtualDOM을 가지고 동작하기 때문에 RealDOM 요소에 접근하거나 조작해야할 경우 이는 리액트가 할 수 없는 일이다.

이러한 일을 리액트 사이드 이펙트라고 부른다. 흔히들 사이드 이펙트를 부작용이라고 오인하는 경우가 있지만 여기서는 순수하지 않거나 역할에 맞는 일을 하지 않는 경우를 말한다.

사이드 이펙트를 처리하기 위해서는 다음과 같은 React API를 사용하여야한다.

  • ref(참조 설정)
    • 값이 변경되어도 리액트가 재렌더링하지 않는다.
  • callback ref(ref 속성에 연결된 함수)
  • React.createRef(참조 객체 생성)
  • React.forwardRef(참조 객체 전달)
    • 범용적으로 누구나 사용할 수 있는 컴포넌트를 만들기 위해서 꼭 필요하다.
    • 외부 컴포넌트가 내부 컴포넌트를 전달받아서 컴포넌트를 내보낸다.
    • domRef를 통해서 전달할 수도 있다.

💭 예시

컴포넌트 생성 시점에 이벤트를 구독한 경우, 컴포넌트 제거 시점에 구독한 이벤트를 취소해야한다.

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
40
41
export class TiltCard extends React.Component {
// DOM 요소 참조를 목적으로 Ref를 생성합니다.
tiltRef = React.createRef(null); // { current:null }

// DOM 요소 참조를 위한 목적의 클래스 인스턴스 멤버
#cardDomElement = null;

// 최초 마운트 시점 이후 처리할 로직을 작성합니다.
componentDidMount() {
this.#cardDomElement = this.tiltRef.current;
VanillaTilt.init(this.#cardDomElement);

// 이벤트 구독
this.#cardDomElement.addEventListener(
"tiltChange",
this.handleChangeTilt.bind(this)
);
}

// 마운트 해제 직전에 처리할 로직을 작성합니다.
componentWillUnmount() {
this.#cardDomElement.vanillaTilt.destroy();

// 이벤트 구독 해지
this.#cardDomElement.removeEventListener(
"tiltChange",
this.handleChangeTilt.bind(this)
);
}

render() {
const { children } = this.props;

return (
// 생성된 Ref를 참조하도록 설정합니다.
<div ref={this.tiltRef} className="tiltCard">
{children}
</div>
);
}
}
  • tiltRef에 DOM 요소를 저장하기 위해 ref를 생성하였다.
  • tiltRef DOM 요소 얻기 위해 “current” 프로퍼티에 접근하였다.
  • tiltRef DOM 요소에 컴포넌트 생성주기에 맞게 이벤트를 등록하고 제거해주었고 ref를 참조하도록 설정해주었다.

댓글 공유

🥁 CORS

카테고리 CS

📌 CORS란?

교차 출처 리소스 공유(Cross-Origin Resource Sharing)는 추가 HTTP Header를 사용하여 한 출처에서 실행 중인 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에게 알려준다.

웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때, 교차 출처 HTTP 요청을 실행한다.

ex) https://domain-a.com의 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우 보안 상의 이유로 브라우저는 HTTP 요청을 제한한다.

  • CORS 체제는 브라우저와 서버 간의 안전한 교차 출처 요청 및 데이터 전송을 지원한다.
  • CORS 표준에 맞춘다는 것은 서버에서도 새로운 요청과 응답 Header를 처리해야한다.

🤿 DeepDive

CORS 표준은 웹 브라우저에서 허용된 출처를 서버에서 새로운 HTTP Header에 추가함으로써 동작한다.

📮 Simple Request

HTTP 요청 메서드가 GET, POST 일 때 사용한다.

  1. 다른 출처끼리 요청을 보낼 때, 요청에 Origin이라는 Header를 추가한다.

    1
    https://loco9939.com:5000
    • Origin은 요청하는 쪽의 Scheme, 도메인, 포트가 담겨있다.
    • scheme : https
    • 도메인 : loco9939.com
    • 포트 : :5000
  2. 요청 받은 서버는 응답 Header에 지정된 ACAO(Access Control Allow Origin) 정보를 실어서 보낸다.

  3. 브라우저가 ACAO 정보가 담긴 응답과 요청의 Origin을 비교하여 동일하면 허락한다.

✈️ Preflight

브라우저가 사전요청을 먼저 보낸 후 서버의 응답을 보고 안전한지 확인한 후 본 요청을 보내는 방식이다. 본 요청은 Simple Request 방식과 동일하다.

🌈 CORS 오류 해결 방법

🔨 서버 측

  • ACAO(Access Control Allow Origin) 설정하기
    • 구체적인 출처 명시하기
    • Credentials: include 옵션 사용한 경우 * 사용한 경우 CORS 에러발생
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var http = require("http");

const PORT = process.env.PORT || 3000;

var httpServer = http.createServer(function (request, response) {
// Setting up Headers
response.setHeader("Access-Control-Allow-origin", "*"); // 모든 출처(orogin)을 허용
response.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS, PUT, PATCH, DELETE"
); // 모든 HTTP 메서드 허용
response.setHeader("Access-Control-Allow-Credentials", "true"); // 클라이언트와 서버 간에 쿠키 주고받기 허용

// ...

response.writeHead(200, { "Content-Type": "text/plain" });
response.end("ok");
});

httpServer.listen(PORT, () => {
console.log("Server is running at port 3000...");
});

Access-Control-Allow-origin 헤더 값으로 * 을 사용하면 모든 Origin에서 오는 요청을 허용한다는 의미이므로 당장은 편할 수 있겠지만, 바꿔서 생각하면 정체도 모르는 이상한 출처에서 오는 요청까지 모두 허용하기 때문에 보안은 더 허술해진다. 그러니 가급적이면 귀찮더라도 다음과 같이 출처를 직접 명시해주도록 하자.

👑 클라이언트 측

  • Proxy 설정
    • 브라우저 요청을 Proxy에서 설정한 주소로 우회하여 전송하는 방법
    • 라이브러리 사용한다
    • 개발단계에서만 사용하고 Production 환경에서는 Proxy 처리가 되지 않는다.

댓글 공유

⛳️ MVC, MVP, MVVM 패턴

카테고리 CS

📌 디자인 패턴이란?

디자인 패턴은 애플리케이션내에서 각자 역할에 맞는 코드끼리 분리하여 유지보수성을 높이고 탄탄한 구조를 가진 애플리케이션을 설계할 수 있다.

✏️ 디자인 패턴 장점

  1. 검증된 해결책

디자인 패턴은 소프트웨어 개발의 문제를 해결하는 확실한 접근 방식을 제공한다.

  1. 쉬운 재사용

일반적으로 필요에 따라 조정가능하며 즉시 사용가능한 솔루션을 반영한다.

다만, 디자인 패턴이 모든 문제의 해결책은 아니다. 패턴의 역할은 솔루션 체계를 제공하고 이를 지원하는 역할을 한다.

이외의 사소한 문제를 예방할 수 있고 반복을 피하여 파일 크기를 줄일 수 있으며, 개발자간의 소통을 원할하게 하는 장점이 있다.

🛤 MVC 패턴

Model + View + Controller를 합친 용어이다.

mvc

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • Controller : 사용자의 입력을 받고 처리하는 부분

동작

  1. 사용자 액션이 들어오면 컨트롤러가 액션 확인하고 모델을 업데이트한다.
  2. 컨트롤러는 모델을 나타낼 뷰를 선택한다.
  3. 뷰는 모델을 이용하여 화면에 나타낸다.
1
2
3
4
참고 - MVC에서 View가 업데이트 되는 방법
- 뷰가 모델을 이용하여 직접 업데이트
- 모델이 뷰에게 알림을 주어 업데이트
- 뷰가 Polling으로 모델의 변경을 주기적으로 감지하여 업데이트

polling이란?

하나의 장치가 충돌 회피 또는 동기화를 목적으로 다른 장치의 상태를 주기적으로 검사하여 일정 조건을 만족할 때, 송수신 자료를 처리하는 방식


특징

  • 컨트롤러는 여러 개의 뷰를 선택할 수 있는 1:n 구조
  • 보편적이며 단순하다.
  • 뷰와 모델 사이의 의존성이 높다. (bad~👎)

🏸 MVP 패턴

Model + View + Presenter를 합친 용어이다.

mvp

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • Presenter : 뷰에서 요청한 정보로 모델을 가공하여 뷰에게 전달해주는 부분(View, Model을 붙여주는 접착제역할)

동작

  1. 사용자 액션은 뷰를 통해 들어온다.
  2. 뷰는 프레젠터에게 데이터를 요청한다.
  3. 프레젠터는 모델에게 데이터 요청한다.
  4. 모델은 프레젠터에게 요청받은 데이터를 응답한다.
  5. 프레젠터는 뷰에게 데이터를 응답한다.
  6. 뷰가 응답받은 데이터를 화면에 나타낸다.

특징

  • 프레젠터는 뷰와 모델의 인스턴스를 가지고 있어 둘을 연결하는 접착제 역할
  • 프레젠터와 뷰는 1:1
  • 뷰와 모델간의 의존성이 없다. (Good~👍)
  • 하지만 뷰와 프레젠터의 의존성 높다.

👑 MVVM 패턴

Model + View + View Model을 합친 용어이다.

mvvm

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • View Model : 뷰를 표현하기 위해 만든 뷰를 위한 모델

동작

  1. 사용자 액션이 뷰를 통해 들어온다.
  2. 뷰에 액션이 들어오면 Command 패턴으로 뷰 모델에 액션을 전달한다.
  3. 뷰 모델은 모델에게 데이터를 요청한다.
  4. 모델은 뷰 모델에게 요청받은 데이터를 응답한다.
  5. 뷰 모델은 응답받은 데이터를 가공하여 저장한다.
  6. 뷰는 뷰 모델과 데이터 바인딩하여 화면에 나타낸다.

특징

  • Command 패턴과 데이터 바인딩 두가지 패턴을 사용(뷰와 뷰 모델 사이의 의존성 제거)
  • 뷰 모델과 뷰는 1:n
  • 뷰와 모델사이의 의존성도 없으므로 각 부분은 독립적으로 모듈화하여 개발할 수 있다.
  • 뷰 모델 설계가 어렵다.

Command패턴이란?

요청을 객체 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 사용할 수 있도록 메서드명, 매개변수 등 요청에 필요한 정보를 저장,로깅, 취소하는 패턴이다.


참고

[디자인패턴]MVC,MVP,MVVM 패턴 비교

댓글 공유

🎡 Flux 패턴

카테고리 CS

📌 Flux 패턴

페이스북이 전통적인 MVC 디자인 패턴이 복잡한 애플리케이션에서 적합하지 않다고 판단하여 새로운 디자인 패턴을 소개한 것이 바로 단방향 데이터 흐름을 갖는 Flux 패턴이다.

🔥 문제점

Complicated MVC

애플리케이션 규모가 커짐에 따라 양방향 데이터 흐름을 그림처럼 복잡한 구조를 가지게 된다.

사용자와 상호작용하는 여러 View가 연결된 여러 Model을 업데이트하고 Model 또한 연결된 View를 업데이트하는 매우 복잡한 상황이 발생하기에 데이터 흐름을 예측하기란 매우 어려워진다.

사실 위 그림은 전통적인 MVC 패턴이고 Apple의 MVC 패턴은 사진처럼 Model과 View의 양방향 데이터 흐름이 발생하지 않는다. 문제는 양방향 데이터 흐름이다.

AppleMVC

🦖 Flux 아키텍처

flux

Flux 패턴은 앱의 단방향 데이터 흐름을 촉진하는 시스템 아키텍처를 말한다.

Action

사용자의 요청을 정의한 객체를 말한다.

  • 사용자 요청 타입(Type)과 데이터(Payload)를 가진다.
  • 액션 크리에이터(Action Creator) 함수를 사용하여 디스패처로 전달한다.

Dispatcher

디스패처는 중앙 데이터 흐름 관리한다. 스토어에 등록된 액션 타입마다 콜백되는 함수가 존재한다.

  • 사용자 액션으로부터 받은 액션 타입에 맞는 콜백함수를 실행한다.
  • 스토어 데이터 변경은 디스패처를 통해서만 가능하다.

Store

스토어는 상태(Model)를 관리하는 저장소이며 상태를 변경할 수 있는 콜백을 가진다. 각 액션을 처리하는 콜백함수는 디스패처에 등록된다.

  • 상태와 상태를 변경하는 함수를 가진다.
  • 디스패처에 등록된 콜백함수가 실행되면 스토어의 상태가 변경되며 View에 데이터가 변경되었음을 알린다.(옵저버 패턴 활용)

View

뷰는 사용자와 상호작용하는 컴포넌트이다. 스토어에서 상태가 변경되었다는 알림이 오면 뷰는 리렌더링이 발생한다.

  • 사용자는 View를 조작할 수 있고 상태 변경을 요청하는 액션을 생성할 수 있다.
  • 생성된 액션은 디스패처에게 전달되고 디스패처는 해당 액션에 연결된 콜백을 실행하여 스토어의 상태를 변경한다.
  • 상태가 변경되었음을 뷰에게 알리고 뷰는 리렌더링이 발생한다.

댓글 공유

LifeCycle(라이프사이클)

카테고리 React

📌 LifeCycle이란?

라이프 사이클은 함수 컴포넌트와 달리 클래스 컴포넌트에서만 라이프 사이클이 제공된다. 컴포넌트 생성, 마운트, 업데이트, 언마운트 등 특정 시점에서 실행되는 콜백함수를 말한다.

👑 LifeCycle 필요성

라이프 사이클은 React의 성능을 최적화하고 React가 컨트롤할 수 없는 Side Effect(사이드이펙트)를 처리하기 위함이다.

대표적인 사이드 이펙트는 다음과 같다.

  1. 네트워크 통신 (비동기 통신 요청 및 응답)
  2. DOM 컨트롤 (RealDOM 접근 및 조작)
  3. 구독/취소 (이벤트 핸들링 등)
  4. 오류 감지 (ErrorBoundary 컴포넌트 등)

✏️ 대표적인 LifeCycle 메서드

  • constructor()
    • 마운트 되기 전에 실행된다.
  • render()
    • 컴포넌트 렌더링 시점에 호출
  • componentDidMount()
    • DOM에 마운트 된 이후 실행된다.
  • componentDidUpdate()
    • 최초 렌더링 시 실행 ❌
    • 업데이트 이후 실행
  • componentWillunmount()
    • 컴포넌트 소멸하기 직전에 실행

LifeCycle

🌈 Render 단계

  • render 단계는 virtualDOM을 조작하는 단계이다.
    • 성능 최적화를 위한 행동은 render 이전에 해줘야한다.
    • render가 끝났다고 해서 실제DOM에 바로 반영되는 것이 아니다.
  • 리액트 업데이트가 되는 경우는 다음 3가지 상황이다.
    1. 새로운 props가 전달되었을 때,
    2. 해당 컴포넌트의 state가 변경되었을 때,
    3. 강제로 변경시켰을 때만 업데이트가 발생한다.

🔨 Commit 단계

commit 단계는 RealDOM이므로 명령형이 가능하다.

  • DOM을 사용하여 componentDidMount, DidUpdate, WillUnmount 에서 명령형 코드 작성이 가능하다.
  • 사이드 이펙트 관련 함수는 여기서 수행해야한다.

댓글 공유

🐍 setState

카테고리 React, Hooks

📌 setState

setState() 메서드는 컴포넌트의 state 객체에 대한 업데이트를 실행합니다. state가 변경되면, 컴포넌트는 리렌더링됩니다.

1
setState(updater[, callback])

updater에 컴포넌트의 state 변경사항을 인자로 넘겨주고 React에게 해당 컴포넌트를 재렌더링해야한다고 요청을 보내는 메서드이다.

  • setState() 호출은 비동기적으로 이뤄진다.
  • 성능 향상을 위해 즉각적으로 재렌더링을 발생시키지 않는다.
    • 여러 변경 사항을 일괄적으로 갱신하거나 나중에 미룰 수 있다. 때문에 setState() 호출하자마자 this.state에 접근하는 것은 문제가 될 수 있다.
    • 대신 componentDidUpdate() 또는 setState(updater, callback)처럼 2번째 인자로 콜백함수를 전달하는 방법을 사용한다.
1
2
3
4
5
6
7
8
9
10
this.state = {
message: "",
};

this.setState(
{ message: "상태 변경" },
() => console.log("callback: ", this.state.message) // '상태 변경'
);

console.log(this.state.message); // ''
  • 아직 렌더링이 발생하지 않았기 때문에 this.state.message의 값이 갱신되지 않았다.

🎒 2022.11.12 추가

setState() 첫번째 인수로 콜백함수 올 때

1
2
3
4
5
6
7
8
9
10
11
12
13
export class BinaryCalcurator extends Component {
state = {
buttonStates: Array(this.props.numberOfButtons).fill(false),
};

handleToggleButtonContent = (index) => {
this.setState(({ buttonStates }) => ({
buttonStates: buttonStates.map((buttonState, idx) =>
idx === index ? !buttonState : buttonState
),
}));
};
}
  • 콜백함수는 이전 상태를 받아서 상태 변경을 요청한다.

댓글 공유

🍪 Cookie

카테고리 CS

📌 Cookie란?

서버가 클라이언트에게 전송하는 데이터이다. 서버가 HTTP Response Header의 Set-Cookie에 내용을 넣어 전달하면 브라우저가 이 내용을 자체적으로 브라우저에 저장한다.

1
2
3
4
5
6
<!-- Reponse Header -->
HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookies: <cookie-name>=<cookie-value>

// Page Content...
  • 쿠키는 key,value를 쌍으로 브라우저에 문자열로 저장된다.
  • 주로 만료 기간을 설정하거나 자동으로 서버로 전송되어야할 작은 용량의 정보를 담는다.
    • Ex) n일 동안 보지 않기, 비로그인 장바구니 기능
  • 쿠키는 주로 인증을 위해 사용된다.
  • 쿠키는 세션쿠키와 퍼머넌트 쿠키가 있다.

세션 쿠키는 클라이언트가 셧다운 되면 사라지는 쿠키이다. 왜냐하면 Expires, Max-Age를 지정해주지 않았기 때문이다.

일부 브라우저는 Session Resorting을 사용하여 세션쿠키를 영구적으로 만들 수 있다.

  • Expires, Max-Age 설정으로 만료 기한을 정해주지 않은 쿠키

클라이언트가 브라우저를 열고 닫거나 새로고침을 하여도 쿠키의 데이터가 보존되는 쿠키이다. Set-Cookie에 몇가지 조건을 추가하여 Permanent Cookie를 설정할 수 있다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT;
  • 만료시간은 클라이언트 시간을 따른다. (서버시간과 상이할 수 있다.)

✏️ 쿠키 접근

1
2
3
4
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// logs "yummy_cookie=choco; tasty_cookie=strawberry"

🔨 쿠키 보안

🙅‍♂️ 민감한 정보는 HTTP 쿠키 내에 저장하면 안된다.

XSS 공격으로부터 사용자의 쿠키 정보가 유출되는 것을 방지하기 위해 등장하였다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT; HttpOnly
  • document.cookie API로 접근할 수 없다.

HTTPS 전송을 보증해주는 역할을 하는 쿠키이다. Secure로 설정된 쿠키는 안전한 전송 프로토콜에만 쿠키 정보를 서버로 보낸다.

1
Set-Cookie: id=a3fWa; Expires=Thu, 10 Nov 2022 15:30:00 GMT; Secure; HttpOnly

👿 Cross-site 요청 위조(CSRF) 공격

현재 사용자가 은행 사이트에 로그인 되어있다. 송금과 같은 은행 업무를 보고 로그아웃 하지 않고 새 탭을 열어 어떤 사이트를 열었다. 그런데 이 사이트에는 해커에게 송금을 요청하는 form이 있고 이 form은 자동으로 제출되도록 설정되었다.

form이 해당 사이트를 거쳐 은행 사이트로 전송될 때 인증 쿠키도 함께 전송된다. 은행은 사용자로 착각하여 해커에게 돈을 송금하는 일이 발생한다.

⇒ 이런 공격을 크로스 사이트 요청 위조 라고 부른다.

🚪 방어 방법

  • XSRF 토큰이라는 특수 필드 넣어줘야한다.
  • 민감한 동작에 필수로 요구되는 확인 절차가 항상 수행하도록 한다.
  • 민감한 동작에 사용되는 쿠키는 짧은 수명만 갖도록 한다.

댓글 공유

HTTP 요청 메서드 GET vs POST

카테고리 CS

📌 GET

GET 방식은 전송 URL에 입력 데이터를 쿼리 스트링으로 보내는 방식
ex) http://jsonplaceholder.typicode.com/posts?userId=1&id=1

  • 전송 URL 바로 뒤에 ?를 통해 데이터의 시작을 알려주고, Key-value 형태의 데이터 추가한다.
  • 캐시가 가능하다.
  • 1개 이상의 전송 데이터는 &로 구분한다.
  • URL에 전송 데이터가 모두 노출되어 보안에 문제가 있어 민감한 정보는 사용하지 않는다. 전송할 수 있는 데이터의 한계가 있다. (최대 255자)
  • REST API에서 GET 메소드는 모든 또는 특정 리소스의 조회를 요청한다.
    GET 요청 보내면 ~ 이거이거 조회할게요~

📌 POST

POST 방식은 사용자의 요청을 Request Body에 담아 보내는 방식
ex) http://jsonplaceholder.typicode.com/posts

  • URL에 전송 데이터가 모두 노출되지 않지만 GET에 비해 속도가 느리다.
  • 캐시가 불가능하다.
  • 데이터 길이와 타입의 제한이 없다.
  • REST API에서 POST 메소드는 특정 리소스의 생성을 요청한다.
    POST 요청 보내면 ~ 이거 생성해주세요~

댓글 공유

loco9939

author.bio


author.job