0%

React UI Library 사용법

Ant Design

Material UI


CORS

Same-Origin Policy

웹 브라우저 보안을 위해 프로토콜, 도메인, 포트가 동일한 서버로만 요청을 주고 받을 수 있는 정책

👇

Cross-Origin Resource Sharing

도메인이나 포트가 달라도 서버에 자원을 요청할 수 있는 메커니즘

![image-20210529152418689](/Users/JungHyunLah/Library/Application Support/typora-user-images/image-20210529152418689.png)

아래와 같은 문제 발생 시; 서버에서 Access-Control-Allow-Origin을 수정해야 함!

![image-20210529152436081](/Users/JungHyunLah/Library/Application Support/typora-user-images/image-20210529152436081.png)


Redux 기초

  1. 예측 가능한
  2. 중앙화된
  3. 디버깅이 쉬운
  4. 유연한
  • 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너 (오픈소스 JS 라이브러리)
  • React, Vue 등 JS 라이브러리와 함께 사용된다.

언제, 왜 사용할까?

  • 기존에는 useState를 통해 정의한 상태를 props를 통해 자식 컴포넌트로 전달하는 구조
  • 프로젝트가 복잡해지면 거쳐야 하는 컴포넌트가 많아지므로 상태 전달이 비효율적일 수 있음

👇

  1. 상태 관리를 한 곳에서 하고 싶을 때
  2. 원하는 컴포넌트에 상태값과 함수를 직접 전달하고 싶을 때

전체적인 구조

  1. store
  2. action
  3. reducer
  4. store 내장 함수

store

  • 어플리케이션의 상태는 하나의 저장소 store 안에 있는 객체 트리(상태 트리)에 저장된다.
  • 상태 트리는 불변 상태를 가져야 한다.
  • 상태가 업데이트되면 관련된 뷰가 다시 그려진다.
  • 내장 메소드
    • getState() : store에 등록된 상태 정보를 불러옴
    • dispatch() : action을 발생시킴
    • subscribe() : 인자로 특정 함수를 전달하면, action이 발생할 때마다 해당 함수를 호출함

action

  • 상태 변경을 나타내는 객체
  • type 필드는 필수이며, 상수로 관리하는 것이 좋다.

reducer

  • 실질적인 함수
  • action이 상태 트리를 어떻게 변경할 지 작성한 순수한 함수
  • 인자로 현재 stateaction 을 받아 액션에 따라 교체된 새로운 상태를 반환한다.

Redux는 꼭 필요할 때 사용하자!

  1. 지속적으로 상태가 변화하는 데이터가 많을 때
  2. 모든 상태를 한 곳에서 관리해야 할 때
  3. 최상위 컴포넌트 에서 모든 상태를 가지고 있기 힘들 때

Closures


클로저란?

함수 번들과 이들이 참조하는 주변 상태 ( lexical environment 라 칭한다) 의 조합을 의미한다.

쉽게 말하자면, 클로저는 독립적인 (자유) 변수를 가리키는 함수로, 클로저 안에 정의된 함수는 만들어진 환경을 기억한다. 즉, 클로저를 통해 inner function scope 에서 outer function scope에 접근할 수 있으며, JavaScript 에서는 함수가 생성되는 시점에 클로저도 생성된다.

흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다.

이해를 돕기 위해 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
function getClosure() {
var text = 'variable 1';
return function() {
return text;
};
}

var closure = getClosure();
console.log(closure()); // 'variable 1'

위에서 정의한 getClosure()는 함수를 반환하고, 반환된 함수는 getClosure() 내부에서 선언된 변수를 참조하고 있다. 또한 이렇게 참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 제대로 된 값을 반환하고 있는 걸 알 수 있다. 여기서 반환된 함수가 클로저이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var base = 'Hello, ';
function sayHelloTo(name) {
var text = base + name;
return function() {
console.log(text);
};
}

var hello1 = sayHelloTo('승민');
var hello2 = sayHelloTo('현섭');
var hello3 = sayHelloTo('유근');
hello1(); // 'Hello, 승민'
hello2(); // 'Hello, 현섭'
hello3(); // 'Hello, 유근'

출력된 결과를 보면 text 변수가 동적으로 변화하고 있는 것처럼 보인다. 실제로는 text라는 변수 자체가 여러 번 생성된 것이다. 즉, hello1()hello2(), hello3()은 서로 다른 환경을 가지고 있다.


Lexical Scoping

Lexical Scoping 은 함수들이 중첩되어 있을 때, parser가 변수 이름을 어떻게 해석하는지에 대해 설명한다. 즉, lexical scope는 중첩 함수가 연속적으로 존재하는 그룹에서, inner function이 그의 parent scope의 변수를 포함한 자원들에 접근이 가능함을 의미한다. 결국 자식 함수들이 부모 함수가 실행되는 문맥에 어휘적으로 묶이는 것 을 의미한다. 이를 static scope 라고 칭하기도 한다.

이를 직관적으로 이해하기 위해 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
function grandfather() {
var name = 'Hammad';
// 'likes' is not accessible here
function parent() {
// 'name' is accessible here
// 'likes' is not accessible here
function child() {
// Innermost level of the scope chain
// 'name' is also accessible here
var likes = 'Coding';
}
}
}

위 예시에서 lexical scope은 forward 방향으로 작동한다는 것을 알 수 있다. 이는 child 함수가 실행될 때 해당 문맥에 name 변수가 포함되어 접근이 가능하다는 것을 의미한다. 하지만 반대로, backward로는 작동하지 않기 때문에 likes는 child의 부모 함수에서 접근이 불가능하다.

이는 같은 이름을 가졌지만 다른 실행 문맥을 가진 변수들이 위에서부터 아래로 코드가 작동하면서 실행 스택 (execution stack) 에 선행 값들 (우선순위) 을 가지고 있음을 의미한다. 이 때 같은 이름을 가진 변수의 경우 안쪽에 존재하는 함수 (실행 스택에서 위쪽에 존재하는 문맥, 함수) 일수록 높은 우선순위를 가진다.

다른 예시를 보자.

1
2
3
4
5
6
7
8
function init() {
var name = 'Mozilla'; // name is a local variable created by init
function displayName() { // displayName() is the inner function, a closure
alert(name); // use variable declared in the parent function
}
displayName();
}
init();

위 코드에서,

  • outer(parent) functioninit() 이 되고,
  • inner(child) functiondisplayName() 이 된다.

init 함수가 지역변수인 namedisplayName 함수를 생성한다. 이 때, displayName은 init 안에 정의된 inner function 이므로 init 내에서만 작동할 수 있다. 특히 해당 함수는 그만의 지역 변수를 생성하지 않는데, inner function의 경우 outer function의 변수에 접근할 수 있으므로 displayName은 그의 parent function인 init 함수의 지역변수인 name에 접근할 수 있다.


Closure

1
2
3
4
5
6
7
8
9
10
function makeFunc() {
var name = 'Mozilla';
function displayName() {
alert(name);
}
return displayName;
}

var myFunc = makeFunc();
myFunc();

위와 동일한 코드이지만, displayName이 실행되기 전 outer function에서 반환된다는 점이 다르다.

보통 함수의 실행이 끝나면 그 내부에 있는 지역변수에 더이상 접근이 불가한 것이 일반적인 경우이지만, 위 코드에서는 makeFunc의 실행이 종료되어도 name 변수에 접근이 가능하다.

그 이유는 JavaScript의 함수는 클로저를 생성하기 때문이다. 클로저는 앞서 말했듯이 함수와 해당 함수가 선언된 lexical environment을 아울러 정의하는데, 이 ‘환경’ 은 클로저가 생성된 그 시기의, in-scope에 해당하는 모든 지역변수를 포함한다.

따라서 위 코드의 경우, myFunc은 makeFunc이 실행될 때 생성된 displayName instance의 참조변수이며, displayName 인스턴스는 그의 lexical environment - name 변수가 존재하는 - 를 지속적으로 참조 (유지) 한다. 따라서, myFunc이 호출되었을 때 name 변수에 계속해서 접근 가능하므로 alert에 “Mozilla”가 전달된다.

또 다른 예시를 보자.

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

위 코드에서는 하나의 인자 x 를 받는 makeAdder(x) 함수가 존재하고, 이는 또 다시 다른 하나의 인자 y 를 받아 x와 y의 합을 반환하는 함수를 반환한다. 결국 makeAdder는 함수 공장으로 작용하는데, 이는 특정 값(y) 을 인자(x) 에 더하는 함수를 생성한다.

위 예시에서는 2개의 함수가 생성되는데,

  • add5 : 하나는 5를 그의 인자에 더하는 함수이고
  • add10 : 다른 하나는 10을 더하는 함수이다.

add5add10 은 모두 클로저이다. 이들은 같은 body를 가진 함수를 공유하지만 다른 lexical environment를 가진다. 이는 add5 의 lexical environment는 x=5이고, add10 에서는 x=10 임을 의미한다.


Closure Scope Chain

모든 클로저는

  1. Local Scope (Own Scope)
  2. Outer Functions Scope
  3. Global Scope

를 가진다.

주의해야 할 부분은 outer function이 중첩된 함수 (중 하나) 일 경우에, 해당 함수의 scope에 접근하는 것은 그를 둘러싼 scope 또한 포함한다는 것 - 결국 함수 scope의 chain을 생성한다 - 이다.

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
// global scope
var e = 10;
function sum(a){
return function(b){
return function(c){
// outer functions scope
return function(d){
// local scope
return a + b + c + d + e;
}
}
}
}

console.log(sum(1)(2)(3)(4)); // log 20

// You can also write without anonymous functions:

// global scope
var e = 10;
function sum(a){
return function sum2(b){
return function sum3(c){
// outer functions scope
return function sum4(d){
// local scope
return a + b + c + d + e;
}
}
}
}

var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) // log 20

위 예시에서는 중첩 함수가 연속적으로 정의되어 있으며 이들은 모두 outer function scope에 접근이 가능하다. 이 때, 클로저는 모든 outer function scope에 접근 가능하다고 정의된다.



Reference

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

https://hyunseob.github.io/2016/08/30/javascript-closure/

이 글은 이화여자대학교 2021-1학기 캡스톤디자인프로젝트B - 스타트7팀(이화BTS) 나정현의 기술 블로그 제출물입니다. 본 팀은 “ERP: Customizing Recommended Data Visualization Plots” 을 주제로 웹 서비스를 제작 중에 있으며, 해당 서비스는 Data2Vis의 Encoder-Decoder 모델, Seq2Seq2 모델에 기반하여 Plot 추천 시스템을 제공합니다.

GitHub 소스코드 : https://github.com/Ewha-BTS/ERP

(※ 현재 비공개인 상태입니다.)




Project Overview

들어가기 앞서, Visualization Recommender System이란 무엇일까?

이는 이름 그대로, 전문가가 일일이 차트를 만들지 않아도 제공되는 차트 리스트 중에 가장 적합한 것을 고를 수 있도록, 자동적으로 차트를 만들어 주는 추천 시스템이다.

본 팀은 큰 데이터셋과 이와 관련된 차트를 이용하여 학습시킨 모델을 통해 시각화 타입과 디자인 초이스를 추천해주는 시스템을 설계하고, 이를 웹에 올려 웹 서비스로서 Visualization 추천 시스템을 제공하는 ERP를 제작하고자 한다.


data를 활용하는 일이 급진적으로 증가하고 있고, 이에 따라 data specialist도 많아지고 있다. 이 트렌드에 맞게 굳이 플랏을 그리기까지의 모든 과정을 다 컨트롤하지 않아도 (길었지만, 흔히 노가다 라고 합니다..) 자동적으로 플랏을 추천해주는 서비스에 대한 요구도 증가하고 있다. 실제로 이에 가장 유명한 서비스는 Tableau 인데, 직접 사용해 본 결과 UI 도 복잡하고 UX도 좋지 못하다고 느꼈다. 하나의 윈도우에 너무 많은 정보가 한꺼번에 떠있는 느낌이었고, UX 플로우가 원활하지 못해 그리는 동안 엑셀에 엔터가 안 쳐지는 것과 비슷한 불편함을 느꼈다.


이를 보완하기 위해, 최근 핫하고 사용성으로 유명한 노션 의 UI와 UX를 차용하여 위 서비스를 사용자 중심 서비스로 탈바꿈하는 것이 어떨까, 생각하였다. 여기에 연구 결과(성능)가 좋아 사용하고 싶었던 Data2Vis 모델을 이용하고, 부족했다고 느꼈거나 원했던 점도 추가해 볼 예정이다.

더하여, - 노션이 가진 최대 장점이라고 생각하는 - 자유자재로 element를 배치하여 활용할 수 있는 대시보드의 편리함을 서비스의 2번째 메인 기능으로 제공하고자 한다. 사용자가 임의로 플랏을 그릴 수 있는 기능을 마지막 메인 기능으로 추가할 예정이다.

이렇게 총 3가지의 메인 기능을 가지고 있는 본 팀의 서비스는 가장 중요한 분야 중 하나인 data 의 활용을 돕고 사용성을 증대시키고자 하며, 이를 통해 전문가가 데이터를 활용함에 있어서 중요한 부분에 집중할 수 있도록 돕고자 한다.



🌝 그럼, 이제부터 전체적인 data 플로우와 웹 UI, 사용한 테크 스택, 실행 방법 및 데모 등에 대해 소개해보겠다.




Model

전체적인 서비스의 흐름도를 ~~아주, 아주!~~ 간단하게 표현해보자면 다음과 같다.

일단 사용자는 플랏으로 그리고자 하는 데이터가 배열에 담긴 .json .csv 과 같은 유형의 data 파일을 input으로 넣는다.

해당 데이터는 앞으로 설명하게 될 Encoder-Decoder과 Seq2Seq2 모델을 기반으로 한 모델을 통해 플랏으로 그려진다. 이 때 데이터는 한개의 플랏이 아닌 모델에서 제시하는 추천 플랏들 k (임의 설정) 개로 표현된다.

이를 토대로 사용자가 원하는 플랏을 선택한 후 세부적인 사항을 조정한 뒤 저장하면, 사용자의 ‘마이 리스트’에 해당 플랏이 저장되어 메인 대시보드에서 이를 이용할 수 있다. 이 때 이용이란, 그래프를 격자 형태를 가진 대시보드에 임의로 배치하고 크기 등을 조정하여 서로 연관된 플랏들을 한눈에 파악 가능하도록 하는 것을 의미한다.


본 팀이 베이스로 사용하게 될 모델은 Vega-Lite를 기반으로 한 Data2Vis 모델이다. 이에 대해 자세히 알아보자.


Data2Vis

Data2Vis를 이해하기 위해, 이와 연관된 연구이자 기본에 충실해 Data Visualization에 대한 이해도를 높일 수 있는 [VizML](https://leahincom.github.io/2021/05/19/VizML-paper-review/) 논문을 리뷰하였으며, 이후 [Data2Vis](https://leahincom.github.io/2021/05/21/Data2Vis-paper-review/) 논문을 리뷰하였다. 이와 관련한 요약본은 다음과 같다.

이 때, 모델에 학습시킬 데이터셋 마련을 위해 양질의 데이터를 크롤링하는 데 한계가 있다고 판단하여, 오픈 소스로 공개되어 있는 Vega-Lite 기반의 Data2Vis의 데이터셋을 이용하여 1차적으로 training 시켰으며, 이후 Plotly 기반의 VizML 데이터셋을 이용하여 Plotly JavaScript 라이브러리를 이용하여 프론트엔드를 구현할 예정이다.


Specification

Data2Vis의 examplesdata에서 가져온 airports.json 의 structure는 다음과 같다.

Screenshot 2021-05-28 at 11 27 09

해당 데이터를 plot으로 그리기 위해 사용된 Vega-Lite grammer은 다음과 같다.

위와 같은 그래머를 training data로 학습시킨 모델이 Vega Editor를 통해 그린 plot들은 다음과 같다.

Screenshot 2021-05-28 at 11 32 10

결론적으로, 본 팀이 사용하고자 하는 모델의 inputoutput , datasetmodel 개요 는 다음과 같다.

input model output dataset
1개의 dataset(.csv) Seq2Seq + Encoder-Decoder Model visualization recommended plots (Top-k개) Data2Vis의 dataset

input dataset의 경우 Training : Eval : Test = 0.8 : 0.1 : 0.1 의 비율로 구성한다.




WEB

Guideline

1. 먼저, 소스를 받기 위해 `git clone` 으로 모든 파일을 local server로 가져온다. 2. 이후, 필요한 모든 npm package를 다운받아 프로젝트를 run할 준비를 마친다. 3. 아직 배포를 하지 않은 상태이므로, localhost로 실행하기 위해 client 폴더로 이동한다. 4. client를 실행시킨다.

※ 자동으로 브라우저가 실행되지 않는 경우, http://localhost:3000 를 주소창에 입력하여 프론트를 확인해 주세요.

1
2
3
4
git clone https://github.com/Ewha-BTS/ERP.git
npm i
cd erp-client
npm start

Front-end

Tech Stack

  • React.js
  • Recoil
  • styled-components (SASS 기반)
  • axios

Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public/
├ index.html
src/
├ assets/
├ components/
├ common/
├ TabBar.js
├ main/
├ Dashboard.js
├ Graph.js
├ DetailBar.js
├ RecBar.js
├ MyList.js
├ lib/
├ api.js
├ state.js
├ pages/
├ MainDashboard.js
├ UserDefine.js
├ Recommendation.js
├ MyPage.js
├ App.js
├ index.css
├ index.js

UI


Details

Recoil

component 내 data 이동이 많아 state를 유연하게 관리하기 위해 필자는 Recoil을 사용하였다. App.js에서 state 관리가 이루어지기 때문에 index.js 에서 컴포넌트를 RecoilRoot 로 감싼 후, data 이동을 관리하였다.

1
import { RecoilRoot } from "recoil";
1
2
3
4
5
6
7
8
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);

Axios

server와의 통신을 위해 필자는 axios 를 사용하였다. 기본적으로 생성한 객체는 다음과 같다.

1
import axios from "axios";
1
2
3
4
const instance = axios.create({
baseURL: "http://203.255.176.80:5016",
timeout: 1000
});

세부적인 API는 서버가 구축된 후 구현할 예정이다.


Back-end

서버의 경우 Data2Vis의 웹 데모 오픈 소스에서 webserver.py 를 본 팀의 프론트엔드에 맞게 변형하여 라우팅 및 기능을 구현하였으며, 모델의 경우 직접 학습시킨 model.ckpt-15000 을 연동시켰다.




Conclusion

본 팀은 현재 Recommendation System Model을 서버에 올려 메인 기능인 "추천 플랏 보여주기" 을 구현하였다.

이후,

  1. 남은 메인 기능 구현
  2. 그에 따른 서버 연동
  3. 모델 수정
  4. 배포

등을 수행하고, 이 과정에서 Plotly 와 같은 다양한 모델과 그에 따른 JavaScript 라이브러리를 이용하여 더 나은 서비스를 구축할 예정이다. 결론적으로 목표하는 바는 Customizing Recommended Data Visualization Plots 을 제공하는 서비스의 완성도를 높이고 직접 배포한 후 유지, 보수 관리를 꾸준히 이어나가는 것이다.

Introduction

많은 상태를 관리할 필요가 생겨났다.

상태

서버 응답, 캐시 데이터, 지역적으로 생성해서 사용하고 있지만 아직 서버에 저장되지 않은 데이터,

활성화된 라우트, 선택된 탭, 로딩을 보여줄지 여부, 페이지네이션 컨트롤 등 다양한 UI 상태

모델이 다른 모델을 업데이트하고, 그리고 뷰가 모델을 업데이트 할 수 있고, 이 뷰가 다시 다른 모델을 업데이트하고, 이에 따라 또 다른 뷰가 업데이트 된다.

더하여, 프론트엔트 제품 개발에 있어서 새로 갖춰야할 복잡한 요건들이 늘어나고 있다. 낙관적 업데이트(Optimistic update), 서버 렌더링, 라우트가 일어나기 전에 데이터 가져오기 등이 이에 해당한다. 이러한 복잡함은 변화(mutation)비동기(asyncronicity) 와 같이 사람이 연동하여 추론하기 어려운 개념을 섞어서 사용한다 는 데서 비롯된다.

비관적(pessimistic) 업데이트

사용자 입력 -> 수정 요청 -> 성공 시 화면 갱신

  • 사용자에게는 불편하지만, 개발자에게는 쉬움

낙관적(optimistic) 업데이트

사용자 입력 -> 바로 화면 먼저 갱신 -> 수정 요청

  • e.g. slack, trello

Redux

  • 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너

  • 일관적, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동, 테스트하기 쉬운 앱 작성을 도와준다.

1
2
npx create-react-app my-app --template redux
npm install redux

  1. 애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장

    • 범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽다.
    • 서버로부터 가져온 상태 는 연결되거나(serialized) 수화되어(hydrated) 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있다.
  2. 상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는 지를 묘사하는 액션 객체를 전달하는 방법뿐

    • 뷰나 네트워크 콜백 등에서 상태를 직접 바꾸지 못 하도록 보장한다.
  3. 변화는 순수 함수로 작성되어야

    • reducer이전 상태액션 을 받아 다음 상태를 반환하는 순수 함수이다. 이 때, 이전 상태를 변경하는 대신 새로운 상태 객체를 생성해서 반환해야한다.
    • 처음에는 하나의 reducer 만으로 충분하지만, 애플리케이션이 성장해나가면 상태 트리의 특정한 부분들을 조작하는 더 작은 개별적인 리듀서들로 나누는 것도 가능하다. (React 의 경우 하나의 루트 컴포넌트에서 시작해서 여러 작은 컴포넌트의 조합으로 나누는 것과 동일)

Concepts

상태

1
type State = any
  • 상태(상태 트리)는 넓은 의미의 단어이지만, Redux API에서는 보통 저장소에 의해 관리되고 getState()에 의해 반환되는 하나의 상태값 을 지칭한다.

  • (넓은 의미의) 상태는 Redux 애플리케이션의 전체 상태 를 나타내며, 보통 깊게 중첩되어 있는 객체입니다.


액션

1
type Action = Object
  • 액션은 상태를 변화시키려는 객체이다.
  • 어떤 형태의 액션이 행해질지 표시하는 type 필드를 가져야 한다. type은 상수로 정의되고 다른 모듈에서 임포트할 수 있다.

리듀서

1
type Reducer<S, A> = (state: S, action: A) => S
  • 리듀서(리듀싱 함수)는 누적(state)값(누적될) 값 을 받아서 새로운 누적값을 반환 하는 함수이다. 이들은 값들의 컬렉션을 받아서 하나의 값으로 줄이는데 사용된다.

  • Redux에서 누적”값”은 상태 객체이고, 누적”될” 값은 액션이다. 리듀서는 주어진 이전 상태와 액션에서 새로운 상태를 계산한다.

  • 반드시 같은 입력이 있으면 같은 출력을 반환 하는 순수 함수여야 한다.


![image-20210527130716974](/Users/JungHyunLah/Library/Application Support/typora-user-images/image-20210527130716974.png)


디스패치 함수

1
2
3
4
// 기본 디스패치 함수
type BaseDispatch = (a: Action) => Action
// 비동기 디스패치 함수 (보통 디스패치 함수)
type Dispatch = (a: Action | AsyncAction) => any
  • 디스패치 함수는 액션이나 비동기 액션을 받는 함수이다.

    1. 저장소 인스턴스가 미들웨어를 거치지 않고 제공하는 기본 dispatch 함수

      • 반드시 동기적으로 저장소의 리듀서에 액션을 보내야 한다. 그러면 리듀서는 저장소가 반환한 이전 상태와 함께 새 상태를 계산한다.
      • 👾 리듀서를 사용하기 위해서 액션은 평범한 객체여야 한다.
    2. 비동기 dispatch 함수 (보통 dispatch 함수)

      • 미들웨어를 통해 디스패치 함수는 비동기 액션을 처리할 수도 있다. 미들웨어가 기본 디스패치 함수를 감쌈으로써 액션이나 비동기 액션을 다음 미들웨어에 넘기기 전에, 변환하거나, 지연시키거나, 무시하거나, 해석할 수 있다.

액션 생산자

1
type ActionCreator<A, P extends any[] = any[]> = (...args: P) => Action | AsyncAction
  • 액션 생산자는 단지 액션을 만드는 함수이다. 이 때, 액션은 정보의 묶음이고, 액션 생산자는 액션을 만드는 곳이다.

  • 액션 생산자를 호출하면 액션을 만들어낼 뿐 dispatch하지는 않는다. 따라서 저장소를 변경하기 위해서는 dispatch 함수를 호출해야 한다.

    • 액션 생산자를 호출해 그 결과를 저장소 인스턴스로 바로 dispatch하는 함수를 바인드된 액션 생산자라고 부르기도 한다.
    • 비동기 액션은 디스패치 함수로 보내지는 값이지만 아직 reducer에게 받아들여질 준비가 되어 있지 않아, 기본 dispatch() 함수로 전달되기 전에 미들웨어를 통해 액션(이나 일련의 액션들)으로 바뀌어야 한다.
      • 이들은 종종 PromiseThunk 와 같은 비동기 기본형으로, 리듀서에게 직접 전달되지는 않지만, 작업이 완료되면 액션을 보낸다.

미들웨어

1
2
type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch

중간 (단계) 역할자

  • 미들웨어는 dispatch 함수를 결합해서 새 dispatch 함수를 반환하는 고차함수이다.
  • 종종 비동기 액션을 액션으로 전환한다.

저장소

1
2
3
4
5
6
type Store = {
dispatch: Dispatch,
getState: () => State,
subscribe: (listener: () => void) => () => void,
replaceReducer: (reducer: Reducer) => void
}
  • 저장소는 애플리케이션의 상태 트리를 가지고 있는 객체이다. reducer 수준 (상태 관리) 에서 결합이 일어나기 때문에, Redux 앱에는 단 하나의 저장소만 있어야 한다.

저장소 생산자

1
type StoreCreator = (reducer: Reducer, preloadedState: ?State) => Store
  • Redux 저장소를 만드는 함수

저장소 인핸서

1
type StoreEnhancer = (next: StoreCreator) => StoreCreator
  • 저장소 생산자를 결합하여 강화된 새 저장소 생산자를 반환하는 고차함수
  • 미들웨어와 비슷하게 조합가능 한 방식으로 저장소 인터페이스를 바꿀 수 있도록 한다.

Do I really need?

Redux를 사용하기 적절한 때는,

  • 계속해서 바뀌는 상당한 양의 데이터가 있다
  • 상태를 위한 단 하나의 근원 (루트 저장소) 이 필요하다
  • 최상위 컴포넌트가 모든 상태를 가지고 있는 것은 더 이상 적절하지 않다.

Basic Example

  1. 앱의 상태 전부는 하나의 저장소(store)안에 있는 객체 트리에 저장된다.

  2. 상태 트리를 변경하는 유일한 방법은 무엇이 일어날지 서술하는 객체인 액션(action)을 보내는 것 뿐이며,

  3. 액션이 상태 트리를 어떻게 변경할지 명시하기 위해 리듀서(reducer)를 작성해야 한다.

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
import { createStore } from 'redux'

/*
* 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
* 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)

// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는,
// 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.
store.subscribe(() => console.log(store.getState())))

// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

상태를 바로 변경하는 대신, 액션이라 불리는 평범한 객체를 통해 일어날 변경을 명시한다.

그리고 각각의 액션이 전체 애플리케이션의 상태를 어떻게 변경할지 결정하는 특별한 함수인 리듀서를 작성합니다.

  • 보통의 Redux 앱에는 하나의 루트 리듀서 함수를 가진 단 하나의 저장소가 있다.

Sample

action은 setter 가 없는 모델(클래스)과 같은 객체이다. 다른 코드가 임의로 이를 수정할 수 없기 때문에, 버그 발생률을 낮춘다.

1
2
3
4
5
6
7
8
9
10
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

state을 바꾸기 위해서는 action을 dispatch 해야 한다. action은 (플레인) 자바스크립트 객체로 전달한다.

1
2
3
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

이렇게 모든 변화를 action으로 표현하는 것은 앱의 세부적인 변화부터 전체적인 변화까지 파악할 수 있도록 한다.

이러한 state과 action을 묶기 위해서, reducer를 사용한다. 다만, reducer은 큰 규모로 다루는 것보다 작은 규모로 나누어서 함수당 하나의 역할을 담당 하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// reducer 1
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}

// reducer 2
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}

최종적으로, 위와 같이 잘게 나눈 reducer를 통합하여 전체적인 reducer 기능을 하는 루트 reducer를 생성한다.

1
2
3
4
5
6
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}

Overview

매개변수(parameter)와 인수(arguments)는 혼용해서 사용되는 헷갈리는 용어이므로 잘 기억해 두자. 매개변수는 함수에 입력으로 전달된 값을 받는 변수를 의미하고 인수는 함수를 호출할 때 전달하는 입력값을 의미한다.

*args처럼 매개변수 이름 앞에 *을 붙이면 입력값을 전부 모아서 튜플로 만들어 주기 때문이다.

**kwargs처럼 매개변수 이름 앞에 **을 붙이면 매개변수 kwargs는 딕셔너리가 되고 모든 key=value 형태의 결괏값이 그 딕셔너리에 저장된다.

함수의 결괏값은 2개가 아니라 언제나 1개라는 데 있다. add_and_mul 함수의 결괏값 a+ba*b는 튜플값 하나인 (a+b, a*b)로 돌려준다.

함수는 return문을 만나는 순간 결괏값을 돌려준 다음 함수를 빠져나가게 된다.

문자열을 출력한다는 것과 반환 값이 있다는 것은 전혀 다른 말이다. 혼동하지 말자. 함수의 반환 값은 오로지 return문에 의해서만 생성된다.

함수의 매개변수에 들어갈 값이 항상 변하는 것이 아닐 경우에는 이렇게 함수의 초깃값을 미리 설정해 두면 유용하다.

하지만,

1
SyntaxError: non-default argument follows default argument

위 오류 메시지는 초깃값을 설정해 놓은 매개변수 뒤에 초깃값을 설정해 놓지 않은 매개변수는 사용할 수 없다는 뜻이다. 즉 매개변수로 (name, old, man=True)는 되지만 (name, man=True, old)는 안 된다는 것이다. 초기화시키고 싶은 매개변수를 항상 뒤쪽에 놓는 것을 잊지 말자.

함수 안에서 새로 만든 매개변수는 함수 안에서만 사용하는 “함수만의 변수”이기 때문이다.

즉 함수 안에서 사용하는 매개변수는 함수 밖의 변수 이름과는 전혀 상관이 없다는 뜻이다.

첫 번째 방법은 return을 사용하는 방법이다.

여기에서도 물론 vartest 함수 안의 a 매개변수는 함수 밖의 a와는 다른 것이다.

두 번째 방법은 global 명령어를 사용하는 방법이다. ( global a )

하지만 프로그래밍을 할 때 global 명령어는 사용하지 않는 것이 좋다. 왜냐하면 함수는 독립적으로 존재하는 것이 좋기 때문이다.

lambda는 함수를 생성할 때 사용하는 예약어로 def와 동일한 역할을 한다. 보통 함수를 한줄로 간결하게 만들 때 사용한다. 우리말로는 “람다”라고 읽고 def를 사용해야 할 정도로 복잡하지 않거나 def를 사용할 수 없는 곳에 주로 쓰인다.

※ lambda 예약어로 만든 함수는 return 명령어가 없어도 결괏값을 돌려준다.

input은 입력되는 모든 것을 문자열로 취급한다.

따옴표로 둘러싸인 문자열을 연속해서 쓰면 + 연산을 한 것과 같다.

콤마(,)를 사용하면 문자열 사이에 띄어쓰기를 할 수 있다.

print() 사용 시 한 줄에 결괏값을 계속 이어서 출력하려면 매개변수 end를 사용해 끝 문자를 지정해야 한다.

파일을 쓰기 모드로 열면 해당 파일이 이미 존재할 경우 원래 있던 내용이 모두 사라지고, 해당 파일이 존재하지 않으면 새로운 파일이 생성된다.

프로그램을 종료할 때 파이썬 프로그램이 열려 있는 파일의 객체를 자동으로 닫아주기 때문이다. 하지만 close()를 사용해서 열려 있는 파일을 직접 닫아 주는 것이 좋다. 쓰기모드로 열었던 파일을 닫지 않고 다시 사용하려고 하면 오류가 발생하기 때문이다.

readline()을 사용해서 파일의 첫 번째 줄을 읽어 출력한다.

readline()은 더 이상 읽을 줄이 없을 경우 빈 문자열(‘’)을 리턴한다.

readlines()는 파일의 모든 줄을 읽어서 각각의 줄을 요소로 갖는 리스트로 돌려준다.

f.read()는 파일의 내용 전체를 문자열로 돌려준다.

추가 모드로 파일을 열었기 때문에 새파일.txt 파일이 원래 가지고 있던 내용 바로 다음부터 결괏값을 적기 시작한다.

with문을 사용하면 with 블록을 벗어나는 순간 열린 파일 객체 f가 자동으로 close되어 편리하다.

인스턴스라는 말은 특정 객체(a)가 어떤 클래스(Cookie)의 객체인지를 관계 위주로 설명할 때 사용한다.

“a는 인스턴스”보다는 “a는 객체”라는 표현이 어울리며 “a는 Cookie의 객체”보다는 “a는 Cookie의 인스턴스”라는 표현이 훨씬 잘 어울린다.

클래스 안에 구현된 함수는 다른 말로 메서드(Method)라고 부른다.

일반 함수와는 달리 메서드의 첫 번째 매개변수 self는 특별한 의미를 가진다.

메서드의 첫 번째 매개변수 self에는 setdata메서드를 호출한 객체 a가 자동으로 전달된다.

잘 사용하지는 않지만 다음과 같이 클래스를 통해 메서드를 호출하는 것도 가능하다.

1
2
>>> a = FourCal()
>>> FourCal.setdata(a, 4, 2)

위와 같이 클래스 이름.메서드 형태로 호출할 때는 객체 a를 첫 번째 매개변수 self에 꼭 전달해 주어야 한다. 반면에 다음처럼 객체.메서드 형태로 호출할 때는 self를 반드시 생략해서 호출해야 한다.

1
2
>>> a = FourCal()
>>> a.setdata(4, 2)

객체에 생성되는 객체만의 변수를 객체변수라고 부른다.

클래스로 만든 객체의 객체변수는 다른 객체의 객체변수에 상관없이 독립적인 값을 유지한다.

객체에 초깃값을 설정해야 할 필요가 있을 때는 setdata와 같은 메서드를 호출하여 초깃값을 설정하기보다는 생성자를 구현하는 것이 안전한 방법이다. 생성자(Constructor)란 객체가 생성될 때 자동으로 호출되는 메서드를 의미한다.

보통 상속은 기존 클래스를 변경하지 않고 기능을 추가하거나 기존 기능을 변경하려고 할 때 사용한다.

“클래스에 기능을 추가하고 싶으면 기존 클래스를 수정하면 되는데 왜 굳이 상속을 받아서 처리해야 하지?” 라는 의문이 들 수도 있다. 하지만 기존 클래스가 라이브러리 형태로 제공되거나 수정이 허용되지 않는 상황이라면 상속을 사용해야 한다.

클래스 변수는 위 예와 같이 클래스이름.클래스 변수로 사용할 수 있다.

또는 클래스로 만든 객체를 통해서도 클래스 변수를 사용할 수 있다.

클래스 변수는 클래스로 만든 모든 객체에 공유된다는 특징이 있다.

import는 현재 디렉터리에 있는 파일이나 파이썬 라이브러리가 저장된 디렉터리에 있는 모듈만 불러올 수 있다.

모듈 이름 없이 함수 이름만 쓰고 싶은 경우도 있을 것이다. 이럴 때는 “from 모듈 이름 import 모듈 함수”를 사용하면 된다.

if __name__ == "__main__"을 사용하면 C:\doit>python mod1.py처럼 직접 이 파일을 실행했을 때는 __name__ == "__main__"이 참이 되어 if문 다음 문장이 수행된다. 반대로 대화형 인터프리터나 다른 파일에서 이 모듈을 불러서 사용할 때는 __name__ == "__main__"이 거짓이 되어 if문 다음 문장이 수행되지 않는다.

파이썬의 __name__ 변수는 파이썬이 내부적으로 사용하는 특별한 변수 이름이다. 만약 C:\doit>python mod1.py처럼 직접 mod1.py 파일을 실행할 경우 mod1.py의 __name__ 변수에는 __main__ 값이 저장된다. 하지만 파이썬 셸이나 다른 파이썬 모듈에서 mod1을 import 할 경우에는 mod1.py의 __name__ 변수에는 mod1.py의 모듈 이름 값 mod1이 저장된다.

sys.path는 파이썬 라이브러리가 설치되어 있는 디렉터리를 보여 준다. 만약 파이썬 모듈이 위 디렉터리에 들어 있다면 모듈이 저장된 디렉터리로 이동할 필요 없이 바로 불러서 사용할 수 있다. 그렇다면 sys.pathC:\doit\mymod 디렉터리를 추가하면 아무 곳에서나 불러 사용할 수 있다.

sys.path.append를 사용해서 C:/doit/mymod라는 디렉터리를 sys.path에 추가한다.

set 명령어를 사용해 PYTHONPATH 환경 변수에 mod2.py 파일이 있는 C:\doit\mymod 디렉터리를 설정한다. 그러면 디렉터리 이동이나 별도의 모듈 추가 작업 없이 mod2 모듈을 불러와서 사용할 수 있다.

파이썬 패키지는 디렉터리와 파이썬 모듈로 이루어진다.

패키지 === 폴더

모듈 === 파일

try문에 else절 사용하기

try문 수행중 오류가 발생하면 except절이 수행되고 오류가 없으면 else절이 수행된다.

abs

abs(x)는 어떤 숫자를 입력받았을 때, 그 숫자의 절댓값을 돌려주는 함수이다.

all

all(x)는 반복 가능한(iterable) 자료형 x를 입력 인수로 받으며 이 x의 요소가 모두 참이면 True, 거짓이 하나라도 있으면 False를 돌려준다.

※ 반복 가능한 자료형이란 for문으로 그 값을 출력할 수 있는 것을 의미한다. 리스트, 튜플, 문자열, 딕셔너리, 집합 등이 있다.

만약 all의 입력 인수가 빈 값인 경우에는 True를 리턴한다.

any

any(x)는 반복 가능한(iterable) 자료형 x를 입력 인수로 받으며 이 x의 요소 중 하나라도 참이 있으면 True를 돌려주고, x가 모두 거짓일 때에만 False를 돌려준다. all(x)의 반대이다.

리스트 자료형 [0, “”]의 요소 0과 “”은 모두 거짓이므로 False를 돌려준다.

만약 any의 입력 인수가 빈 값인 경우에는 False를 리턴한다.

chr

chr(i)는 유니코드(Unicode) 값을 입력받아 그 코드에 해당하는 문자를 출력하는 함수이다.

dir

dir은 객체가 자체적으로 가지고 있는 변수나 함수를 보여 준다.

divmod

divmod(a, b)는 2개의 숫자를 입력으로 받는다. 그리고 a를 b로 나눈 몫과 나머지를 튜플 형태로 돌려주는 함수이다.

enumerate

enumerate는 “열거하다”라는 뜻이다. 이 함수는 순서가 있는 자료형(리스트, 튜플, 문자열)을 입력으로 받아 인덱스 값을 포함하는 enumerate 객체를 돌려준다.

※ 보통 enumerate 함수는 다음 예제처럼 for문과 함께 자주 사용한다.

eval

eval(expression )은 실행 가능한 문자열(1+2, ‘hi’ + ‘a’ 같은 것)을 입력으로 받아 문자열을 실행한 결괏값을 돌려주는 함수이다.

filter

filter란 무엇인가를 걸러낸다는 뜻으로 filter 함수도 동일한 의미를 가진다.

filter 함수는 첫 번째 인수로 함수 이름을, 두 번째 인수로 그 함수에 차례로 들어갈 반복 가능한 자료형을 받는다. 그리고 두 번째 인수인 반복 가능한 자료형 요소가 첫 번째 인수인 함수에 입력되었을 때 반환 값이 참인 것만 묶어서(걸러 내서) 돌려준다.

hex

hex(x)는 정수 값을 입력받아 16진수(hexadecimal)로 변환하여 돌려주는 함수이다.

id

id(object)는 객체를 입력받아 객체의 고유 주소 값(레퍼런스)을 돌려주는 함수이다.

input

input([prompt])은 사용자 입력을 받는 함수이다. 매개변수로 문자열을 주면 다음 세 번째 예에서 볼 수 있듯이 그 문자열은 프롬프트가 된다

int

int(x)는 문자열 형태의 숫자나 소수점이 있는 숫자 등을 정수 형태로 돌려주는 함수로, 정수를 입력으로 받으면 그대로 돌려준다.

int(x, radix)는 radix 진수로 표현된 문자열 x를 10진수로 변환하여 돌려준다.

isinstance

isinstance(object, class )는 첫 번째 인수로 인스턴스, 두 번째 인수로 클래스 이름을 받는다. 입력으로 받은 인스턴스가 그 클래스의 인스턴스인지를 판단하여 참이면 True, 거짓이면 False를 돌려준다.

len

len(s)은 입력값 s의 길이(요소의 전체 개수)를 돌려주는 함수이다.

list

list(s)는 반복 가능한 자료형 s를 입력받아 리스트로 만들어 돌려주는 함수이다.

list 함수에 리스트를 입력으로 주면 똑같은 리스트를 복사하여 돌려준다.

map

map(f, iterable)은 함수(f)와 반복 가능한(iterable) 자료형을 입력으로 받는다. map은 입력받은 자료형의 각 요소를 함수 f가 수행한 결과를 묶어서 돌려주는 함수이다.

max

max(iterable)는 인수로 반복 가능한 자료형을 입력받아 그 최댓값을 돌려주는 함수이다.

min

min(iterable)은 max 함수와 반대로, 인수로 반복 가능한 자료형을 입력받아 그 최솟값을 돌려주는 함수이다.

oct

oct(x)는 정수 형태의 숫자를 8진수 문자열로 바꾸어 돌려주는 함수이다.

open

open(filename, [mode])은 “파일 이름”과 “읽기 방법”을 입력받아 파일 객체를 돌려주는 함수이다. 읽기 방법(mode)을 생략하면 기본값인 읽기 전용 모드(r)로 파일 객체를 만들어 돌려준다.

b는 w, r, a와 함께 사용한다.

ord

ord(c)는 문자의 유니코드 값을 돌려주는 함수이다.

※ ord 함수는 chr 함수와 반대이다.

pow

pow(x, y)는 x의 y 제곱한 결괏값을 돌려주는 함수이다.

range

range([start,] stop [,step] )는 for문과 함께 자주 사용하는 함수이다. 이 함수는 입력받은 숫자에 해당하는 범위 값을 반복 가능한 객체로 만들어 돌려준다.

인수가 하나일 경우

시작 숫자를 지정해 주지 않으면 range 함수는 0부터 시작한다.

인수가 2개일 경우

입력으로 주어지는 2개의 인수는 시작 숫자와 끝 숫자를 나타낸다. 단 끝 숫자는 해당 범위에 포함되지 않는다는 것에 주의하자.

세 번째 인수는 숫자 사이의 거리를 말한다.

round

round(number[, ndigits]) 함수는 숫자를 입력받아 반올림해 주는 함수이다.

round 함수의 두 번째 매개변수는 반올림하여 표시하고 싶은 소수점의 자릿수(ndigits)이다.

※ [, ndigits]는 ndigits가 있을 수도 있고 없을 수도 있다는 의미이다.

sorted

sorted(iterable) 함수는 입력값을 정렬한 후 그 결과를 새로운 리스트로 돌려주는 함수이다.

리스트 자료형에도 sort 함수가 있다. sort 함수는 리스트 원본을 정렬하므로, 새로운 객체를 반환하는 것이 아닌 원본을 수정하는 것이다.

str

str(object)은 문자열 형태로 객체를 변환하여 돌려주는 함수이다.

sum

sum(iterable) 은 입력받은 리스트나 튜플의 모든 요소의 합을 돌려주는 함수이다.

tuple

tuple(iterable)은 반복 가능한 자료형을 입력받아 튜플 형태로 바꾸어 돌려주는 함수이다. 만약 튜플이 입력으로 들어오면 그대로 돌려준다.

type

type(object)은 입력값의 자료형이 무엇인지 알려 주는 함수이다.

zip

zip(*iterable)은 동일한 개수로 이루어진 자료형을 묶어 주는 역할을 하는 함수이다.

※ 여기서 사용한 *iterable은 반복 가능(iterable)한 자료형 여러 개를 입력할 수 있다는 의미이다.

1
2
3
4
5
6
>>> list(zip([1, 2, 3], [4, 5, 6]))
[(1, 4), (2, 5), (3, 6)]
>>> list(zip([1, 2, 3], [4, 5, 6], [7, 8, 9]))
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
>>> list(zip("abc", "def"))
[('a', 'd'), ('b', 'e'), ('c', 'f')]

Library

sys

sys 모듈은 파이썬 인터프리터가 제공하는 변수와 함수를 직접 제어할 수 있게 해주는 모듈이다.

pickle

pickle은 객체의 형태를 그대로 유지하면서 파일에 저장하고 불러올 수 있게 하는 모듈이다.

pickle 모듈의 dump 함수를 사용하여 딕셔너리 객체인 data를 그대로 파일에 저장하는 방법, pickle.dump로 저장한 파일을 pickle.load를 사용해서 원래 있던 딕셔너리 객체(data) 상태 그대로 불러오기

os

OS 모듈은 환경 변수나 디렉터리, 파일 등의 OS 자원을 제어할 수 있게 해주는 모듈이다.

정규 표현식

문자 클래스 [ ]

  • [a-zA-Z] : 알파벳 모두
  • [0-9] : 숫자

문자 클래스 안에 ^ 메타 문자를 사용할 경우에는 반대(not)라는 의미를 갖는다. 예를 들어 [^0-9]라는 정규 표현식은 숫자가 아닌 문자만 매치된다.

  • \d - 숫자와 매치, [0-9]와 동일한 표현식이다.
  • \D - 숫자가 아닌 것과 매치, [^0-9]와 동일한 표현식이다.
  • \s - whitespace 문자와 매치, [ \t\n\r\f\v]와 동일한 표현식이다. 맨 앞의 빈 칸은 공백문자(space)를 의미한다.
  • \S - whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일한 표현식이다.
  • \w - 문자+숫자(alphanumeric)와 매치, [a-zA-Z0-9_]와 동일한 표현식이다.
  • \W - 문자+숫자(alphanumeric)가 아닌 문자와 매치, [^a-zA-Z0-9_]와 동일한 표현식이다.

Dot(.)

정규 표현식의 Dot(.) 메타 문자는 줄바꿈 문자인 \n을 제외한 모든 문자와 매치됨을 의미한다.

1
a.b

“a + 모든문자 + b”

1
a[.]b

“a + Dot(.)문자 + b”

문자 클래스([]) 내에 Dot(.) 메타 문자가 사용된다면 이것은 “모든 문자”라는 의미가 아닌 문자 . 그대로를 의미한다.

반복

** 바로 앞에 있는 문자 a가 0부터 무한대로 반복될 수 있다는 의미이다.

+는 최소 1번 이상 반복될 때 사용한다.

{m, n} 정규식을 사용하면 반복 횟수가 m부터 n까지 매치할 수 있다. 또한 m 또는 n을 생략할 수도 있다.

1
ca{2}t

“c + a(반드시 2번 반복) + t”

? 메타문자가 의미하는 것은 {0, 1} 이다.

1
ab?c

“a + b(있어도 되고 없어도 된다) + c”

re 모듈

1
2
>>> import re
>>> p = re.compile('ab*')

re.compile을 사용하여 정규 표현식(위 예에서는 ab*)을 컴파일한다. re.compile의 결과로 돌려주는 객체 p(컴파일된 패턴 객체)를 사용하여 그 이후의 작업을 수행할 것이다.

1
2
3
4
5
6
p = re.compile(정규표현식)
m = p.match( 'string goes here' )
if m:
print('Match found: ', m.group()) # group() 매치된 문자열을 돌려준다.
else:
print('No match')

finditer는 findall과 동일하지만 그 결과로 반복 가능한 객체(iterator object)를 돌려준다. 반복 가능한 객체가 포함하는 각각의 요소는 match 객체이다.

match 메서드는 문자열의 처음부터 정규식과 매치되는지 조사한다.

match 객체의 start()의 결괏값은 항상 0일 수밖에 없다. 왜냐하면 match 메서드는 항상 문자열의 시작부터 조사하기 때문이다.

1
>>> m = re.match('[a-z]+', "python")

한 번 만든 패턴 객체를 여러번 사용해야 할 때는 이 방법보다 re.compile을 사용하는 것이 편하다.

^는 문자열의 처음을 의미하고, $는 문자열의 마지막을 의미한다.

re.MULTILINE 옵션은 ^, $ 메타 문자를 문자열의 각 줄마다 적용해 주는 것이다.

re.VERBOSE 옵션을 사용하면 문자열에 사용된 whitespace는 컴파일할 때 제거된다(단 [ ] 안에 사용한 whitespace는 제외). 그리고 줄 단위로 #기호를 사용하여 주석문을 작성할 수 있다.

\ 문자가 문자열 자체임을 알려 주기 위해 백슬래시 2개를 사용하여 이스케이프 처리를 해야 한다.

정규식 문자열 앞에 r 문자를 삽입하면 이 정규식은 Raw String 규칙이 적용된다.

백슬래시를 사용하지 않는 정규식이라면 r의 유무에 상관없이 동일한 정규식이 될 것이다.

re.MULTILINE 옵션을 사용할 경우 ^은 각 줄의 문자열의 처음과 매치되지만 \A는 줄과 상관없이 전체 문자열의 처음하고만 매치된다.

re.MULTILINE 옵션을 사용할 경우 $ 메타 문자와는 달리 전체 문자열의 끝과 매치된다.

\b는 파이썬 리터럴 규칙에 의하면 백스페이스(BackSpace)를 의미하므로 백스페이스가 아닌 단어 구분자임을 알려 주기 위해 r'\bclass\b'처럼 Raw string임을 알려주는 기호 r을 반드시 붙여 주어야 한다.

\B 메타 문자는 \b 메타 문자와 반대의 경우이다. 즉 whitespace로 구분된 단어가 아닌 경우에만 매치된다. 단어의 앞뒤에 whitespace가 하나라도 있는 경우에는 매치가 안 된다.

\w+ 부분을 그룹 (\w+)으로 만들면 match 객체의 group(인덱스) 메서드를 사용하여 그루핑된 부분의 문자열만 뽑아낼 수 있다.

group(인덱스) 설명
group(0) 매치된 전체 문자열

그룹이 중첩되어 있는 경우는 바깥쪽부터 시작하여 안쪽으로 들어갈수록 인덱스가 증가한다.

\1은 정규식의 그룹 중 첫 번째 그룹을 가리킨다. 한 번 그루핑한 문자열을 재참조(Backreferences)할 수 있다.

정규식은 그룹을 만들 때 그룹 이름을 지정할 수 있게 했다.

1
(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)

그룹 이름을 사용하면 정규식 안에서 재참조하는 것도 가능하다.

1
2
3
>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'

위 예에서 볼 수 있듯이 재참조할 때에는 (?P=그룹이름)이라는 확장 구문을 사용해야 한다.

  • 긍정형 전방 탐색((?=...)) - ... 에 해당되는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않는다.
  • 부정형 전방 탐색((?!...)) - ...에 해당되는 정규식과 매치되지 않아야 하며 조건이 통과되어도 문자열이 소비되지 않는다.

sub 메서드의 첫 번째 매개변수는 “바꿀 문자열(replacement)”이 되고, 두 번째 매개변수는 “대상 문자열”이 된다.

바꾸기 횟수를 제어하려면 다음과 같이 세 번째 매개변수로 count 값을 넘기면 된다.

subn 역시 sub와 동일한 기능을 하지만 반환 결과를 튜플로 돌려준다는 차이가 있다. 돌려준 튜플의 첫 번째 요소는 변경된 문자열이고, 두 번째 요소는 바꾸기가 발생한 횟수이다.

sub 메서드를 사용할 때 참조 구문을 사용할 수 있다.

1
2
3
>>> p = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
>>> print(p.sub("\g<phone> \g<name>", "park 010-1234-1234"))
010-1234-1234 park

위 예는 이름 + 전화번호의 문자열을 전화번호 + 이름으로 바꾸는 예이다. sub의 바꿀 문자열 부분에 \g<그룹이름>을 사용하면 정규식의 그룹 이름을 참조할 수 있게 된다.

* 메타 문자는 매우 탐욕스러워서 매치할 수 있는 최대한의 문자열을 모두 소비해 버린다.

non-greedy 문자인 ?를 사용하면 *의 탐욕을 제한할 수 있다. non-greedy 문자인 ?*?, +?, ??, {m,n}?와 같이 사용할 수 있다. 가능한 한 가장 최소한의 반복을 수행하도록 도와주는 역할을 한다.

3.1 MNIST

sklearn.datasets : Toy datasets를 담은 lib (크기가 작아 학습용이 아닌 샘플용으로 사용)

일반적인 구조 ⇒ 쓸 수 있는 keys :

  • 데이터셋을 설명하는 DESCR 키
  • 샘플이 하나의 행, 특성이 하나의 열로 구성된 배열을 가진 data 키
  • 레이블 배열을 담은 target 키
  • 등등

1
2
3
4
from sklearn.datasets 
import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
mnist.keys() >>> dict_keys(['data', 'target', 'feature_names', 'DESCR', 'details', 'categories', 'url'])

위의 결과는 mnist의 key들로, dictionary 구조를 가진 mnist에 대해 쓸 수 있는 명령들을 의미한다.



  1. data : 학습용(feed) 데이터
  2. target : label 데이터로, 분류될 class를 의미

따라서,

1
2
X, y = mnist["data"], mnist["target"] 
X.shape >>> (70000, 784) # 784 = 28*28 y.shape >>> (70000,)

X.shape는 70000개의 data에 대해 각 data 당 28*28에 해당하는 pixel 강도(0~255)을 담고 있는 배열을 나타내고,

y.shape는 70000개의 분류된 class를 나타낸다.


mnist의 data를 담은 X 배열에서 하나의 이미지를 예시로 추출하여 출력해보도록 한다.

1
some_digit = X[0] # 0번째 data some_digit_image = some_digit.reshape(28, 28) # 28*28 size에 맞게 벡터 reshape

some_digit.reshape(28, 28)이 의미하는 바는, 현재 [1, 784]로 저장되어 있는 X[0]의 이미지 벡터는 [28,28]의 배열로 reshaping한다는 것이다.

1
y[0] >>> '5'

y에는 분류된 class가 담기므로, X[0]은 ‘5’ class에 속하는 data라는 것이다.

위에서 출력된 것은 ‘(작은따옴표)’로부터 알 수 있듯이 string data이므로 이를 정수로 바꾸는 코드가 다음과 같다.

1
y = y.astype(np.uint8)


1장에서 공부했듯이, 위의 MNIST은 지도 학습 모델이므로,

더 좋은 training result를 내기 위해서는 하나의 dataset에서 train dataset과 test dataset을 적절히 나누어야 한다. 따라서 해당 MNIST에서는 6:1로(전체 7만개의 dataset) train 및 test dataset을 나누었다.

또한 MNIST dataset은 데이터셋이 적절히 섞여 있어, 교차 검증 폴드(train / test set 간 분포의 균질성)가 비슷하고 훈련 샘플의 순서에 따른 성능 저하를 방지한다.


3.2 이진 분류기 훈련

이진 분류기 : YES or NO로 class 분류

1
2
y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)

위의 2가지 변수는 “감지기” 그 자체이다. 즉, 오른쪽 조건이 True인지 False인지 판단하는 감지기의 역할을 한다.

분류 모델로 사용할 SGD(Stochastic Gradient Descent) 분류기는 손실함수 자체를 최소화하는 것이 아닌, 손실함수의 기댓값을 최소화하는 방법을 이용한다. 즉, gradient가 아닌 gradient의 기댓값의 추정치를 이용한다. 이에 사용되는 수식은 다음과 같다. 𝑤(𝑘+1)=𝑤(𝑘)+E[∇𝐿]. 모든 학습 데이터를 사용하는 것이 아닌 minibatch(일부 데이터, 하나의 훈련 샘플 그룹)를 이용하여 gradient의 추정치를 구하므로, gradient의 기댓값의 추정치는 표본 평균으로 작용한다. 따라서 계산량과 학습 데이터가 많은 deep learning과 온라인 학습(미니배치 이용)에 적합하다.

1
2
3
4
from sklearn.linear_model 
import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5) sgd_clf.predict([some_digit]) >>> array([ True])

SGDClassifier(random_state=42)에서 random_state은 무작위 값을 넣으며 최적값을 찾는 SGDClassifier의 특성에 따라, 이전에 실행했던 결과값을 repeatable하게 받기 위해 특정값을 저장하는 것을 의미한다. 값의 의미는 딱히 없으므로, 아무 정수로 설정할 수 있다. 이 후 classifier에 data와 y_train_5를 넣어 classifier을 fitting한다.

classifier은 predict의 결과로 some_digit(=X[0])을 넣었을 때 y_train_5에 따라 5 여부를 감지한 결과를 array에 담는다.


3.3 성능 측정

3.3.1 교차 검증을 사용한 정확도 측정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sklearn.model_selection 
import StratifiedKFold
from sklearn.base
import clone skfolds = StratifiedKFold(n_splits=3, random_state=42)
for train_index, test_index in skfolds.split(X_train, y_train_5) :
clone_clf = clone(sgd_clf)
X_train_folds = X_train[train_index]
y_train_folds = y_train_5[train_index]
X_test_fold = X_train[test_index]
y_test_fold = y_train[test_index]
clone_clf.fit(X_train_folds, y_train_folds)
y_pred = clone_clf.predict(X_test_fold)
n_correct = sum(y_pred == y_test_fold)
print(n_correct / len(y_pred)) >>> 0.9502, 0.96565, 0.96495

StartifiedKFold에서 n_splits=3으로 설정하였으므로, 폴드가 3개인 k-겹 교차 검증이 수행된다.

train/test_index으로 skfolds 객체의 split()을 호출하여 학습용/검증용 데이터로 분할할 수 있는 인덱스를 반환받고 실제 분할된 데이터를 해당 index를 대입하여 추출한다. 이 때, split()의 대상은 X_train → y_train_5 집합이다. 이를 토대로 y 값의 prediction인 y_pred를 X_test_fold로부터 predict하고, 올바른 예측의 수를 y_pred와 y_test_fold를 비교하여 sum 값을 통해 계산한다.

위의 과정은 sklearn의 cross_val_score과 거의 같은 기능을 나타내므로 다음과 같은 코드로 정리할 수 있다.

1
2
from sklearn.model_selection import cross_val_score 
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy") >>> array([0.96355, 0.93795, 0.95615])

이는 약 95%로 굉장히 높은 정확도를 나타낸다. 하지만, 이러한 정확도를 성능 측정 지표로 사용하지 않는 이유는 다음과 같다.



위의 예시와 반대로, 5가 아닌 클래스를 분류하는 더미 분류기를 만들어 정확도를 비교해볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
from sklearn.base 
import BaseEstimator
class Never5Classifier(BaseEstimator) :
def fit(self, X, y=None) :
return self

def predict(self, X) :
return np.zeros((len(X), 1), dtype=bool)

never_5_clf = Never5Classifier()
cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy") >>> array([0.91125, 0.90855, 0.90915])

이 때 Never5Classifier의 predict 함수는 배열의 결과를 전부 False로 세팅하여 배열을 반환한다.

해당 classifier을 cross_val_score에 적용할 시 정확도가 90% 가량 나오는 것을 확인할 수 있다. 이는 약 10%의 이미지가 숫자 5이므로 이를 제외한 90%를 정확도로 출력한다. 따라서, 불균형한 데이터셋(5임/5가 아님 = 어떤 클래스가 다른 것보다 월등히 많음)을 다룰 때는 정확도를 분류기의 성능 측정 지표로 선호하지 않는다.


3.3.2 오차 행렬

오차 행렬 : 데이터와 다른 클래스로 잘못 분류한 횟수를 담은 행렬

1
2
3
from sklearn.model_selection 
import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)

cross_val_score() : 테스트 세트의 output의 평균을 이용해 평가 점수 반환

cross_val_predict() : 테스트 세트의 input의 각 element에 대한 깨끗한 예측 반환 (훈련하는 동안 사용되지 않은 데이터에 대해 예측)

1
confusion_matrix(y_train_5, y_train_pred) >>> array([[53057, 1522], [1325, 4096]])
  • 행 : 실제 클래스 (데이터)
  • 열 : 예측한 클래스

따라서, 위의 결과에서 첫 번째 행이 “5 아님”(음성 클래스)일 때, 첫 번째 열은 True Negative(예측 N, 실제 N)을 나타내며, 두 번째 열은 False Positive(예측 P, 실제 N)를 나타낸다. 두 번째 행은 “5임”(양성 클래스)이며, 첫 번째 열은 FN(예측 N, 실제 P), 두 번째 열은 TP(예측 P, 실제 P)이다.


이에 완벽한 분류기는 TP와 TN만 가지고 있을 것이므로, 실제 confusion matrix의 출력값은 주대각선 값만 존재한다.

1
y_train_perfect_predictions = y_train_5 confusion_matrix(y_train_5, y_train_perfect_predictions) >>> array([[54579, 0], [0, 5421]])

3.3.3 정밀도와 재현율

  • 정밀도 = 정확도 = TP / (TP+FP)
  • 재현율 = 민감도 = TP / (TP+FN)

정밀도는 예측이 양성인 결과(TP, FP)를, 재현율은 실제 양성인 결과(TP,FN)를 이용한다.

즉, 정밀도는 확실한 양성 샘플 하나만 예측할 시 1이 나오지만 이는 다른 모든 양성 샘플을 무시한 결과이므로, 재현율(민감도 또는 진짜 양성 비율)과 같은 다른 지표와 함께 사용해야 한다.

1
2
precision_score(y_train_5, y_train_pred) // 정밀도 >>> 0.729085... 
recall_score(y_train_5, y_train_pred) // 재현율 >>> 0.755580...

0.729는 정밀도로, 5로 판별된 이미지 중 72.9%가 정확하다는 의미이며, 0.756은 재현율으로, 전체 숫자 5에서 75.6%의 5만 감지한 것을 의미한다.



이를 하나의 숫자로 통합해 살펴볼 수 있는 평가 지표가 F1 점수, 즉 정밀도와 재현율의 조화 평균이다.

  • F1 = TP / (TP + (FN + FP) / 2)
1
f1_score(y_train_5, y_train_pred) >>> 0.742096...

정밀도와 재현율이 비슷할 시 F1의 값이 높게 나오므로 간단한 평가 지표로 이용할 수 있지만,

정밀도/재현율 트레이드오프에 따라 정밀도와 재현율이 반비례하는 경우 상황에 따라 둘의 중요성이 달라지므로 F1을 주요 평가 지표로 사용하는 것이 바람직하지 않다.


3.3.4 정밀도/재현율 트레이드오프

정밀도와 재현율은 결정 함수의 결과로 나온 output dataset에 적용하는 임곗값에 따라 달라진다.

즉, 임곗값을 높게 설정할 경우 정밀도는 높아지나 포함되는 TP가 낮아지므로 재현율은 떨어지며, 임곗값을 낮게 설정할 경우 정밀도는 떨어지나 재현율은 높아진다. some_digit의 예측에 사용한 점수를 확인하고 임곗값을 정해 양성/음성 예측을 만들기 위해 decision_function()을 이용한다.

1
2
3
4
5
6
y_scores = sgd_clf.decision_function([some_digit]) 
y_scores >>> array([2412.53175101])
threshold = 0
y_some_digit_pred = (y_scores > threshold) >>> array([ True])
threshold = 8000
y_some_digit_pred = (y_scores > threshold) y_some_digit_[red >>> array([False])

따라서 적절한 임곗값을 설정하기 위하여, cross_val_predict()의 decision_function을 이용하여 모든 샘플의 결정 점수를 얻는다. 이후 precision_recall_curve()를 이용해 가능한 임곗값에 대한 정밀도와 재현율을 얻는다.

1
2
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function") 
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

좋은 정밀도/재현율 트레이드오프는 재현율에 대한 정밀도 곡선에서 정밀도가 급격하게 줄어드는 지점 직전의 값으로 임곗값을 설정하면 된다.

또한 특정한 목표치의 정밀도를 달성하는 것이 목표(n%라고 가정해보자.)일 때는, 다음과 같이 임곗값을 설정하면 된다.

1
2
3
4
threshold_90_precision = thresholds[np.argmax(precisions >= n / 100)] 
y_train_pred_90 = (y_scores >= threshold_90_precision)
precision_score(y_train_5, y_train_pred_90) >>> 0.900038...
recall_score(y_train_5, y_train_pred_90) >>> 0.436819...

하지만 재현율이 너무 낮을 경우, 높은 정밀도의 분류기는 유용하지 않다.


3.3.5 ROC 곡선

ROC 곡선 : 거짓 양성 비율(FPR)에 대한 진짜 양성 비율(TPR, 재현율)의 곡선으로, FPR = 1(실제 음성) - TNR이다. 이 때 TNR을 특이도라고 하며, ROC를 재현율에 대한 1-특이도라고 한다.

TPR과 FPR을 계산하기 위해 사용되는 함수는 roc_curve()로, 다음과 같이 사용된다.

1
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)

TPR이 높아질 수록 FPR도 높아지므로, 좋은 분류기는 완전한 랜덤 분류기(TPR : FPR = 1 : 1)에서 최대한 왼쪽 위 모서리(TPR은 높고 FPR은 낮은)로 멀리 떨어져야 한다. 이를 평가하는 지표로는 곡선 아래의 면적(AUC)가 있는데, AUC를 계산하기 위해 RandomForestClassifier을 이용한다.

1
2
3
4
5
from sklearn.ensemble import RandomForestClassifier 
forest_clf = RandomForestClassifier(random_state = 42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3, method="predict_proba")
y_scores_forest = y_probas_forest[:, 1]
roc_auc_score(y_train_5, y_scores_forest)

RandomForestClassifier에서는 decision_function 대신 predict_proba 기능을 제공한다.

그 결과로서, RandomForestClassifier는 SGDClassifier보다 왼쪽 위 모서리에 더 가까운 ROC 곡선을 생성하므로 AUC 점수로 더 높다.


3.4 다중 분류

다중 분류기로 둘 이상의 클래스를 구별할 수 있으나, 보통 이진 분류기를 이용해 다음과 같은 2가지의 방법으로 다중 클래스를 구별한다. 보통 다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 자동적으로 OvR이나 OvO를 실행하지만, import를 통해 클래스 객체를 생성하여 강제적으로 원하는 Classifier을 설정할 수도 있다.

  • OvR(OvA) : 특정 클래스만 구분하는 클래스별 이진 분류기 n개를 훈련시켜 클래스가 n개의 클래스 분류 시스템을 만든다. 이후 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택한다. 이는 큰 훈련 세트에서 적은 분류기를 훈련시킨다.
1
from sklearn.multiclass import OneVsRestClassifier
  • OvO : 각 클래스의 조합마다 이진 분류기를 훈련시킨다. 이 중 가장 많이 양성으로 분류된 클래스를 선택한다. 이는 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스의 샘플만 필요하므로, 작은 훈련 세트에서 많은 분류기를 훈련시킬 수 있다.
1
from sklearn.multiclass import OneVsOneClassifier

더 높은 정확도를 위해, 입력의 스케일을 조정하면 분류기의 성능을 높일 수 있다.

1
2
3
from sklearn.preprocessing import StandartScaler 
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(float64))

3.5 에러 분석

cross_val_predict()를 이용해 결과값을 예측하고, 이 후 confusion_matrix()를 이용해 오차 행렬을 생성한다. 그 후 matshow()를 실행 시, 각 클래스 별로 예측한 클래스의 확률이 밝기로 나타나는데, 밝을 수록 높은 확률을 나타낸다. 따라서 올바른 오차 행렬은 주대각선의 밝기가 밝고, 나머지는 어두워야 한다.

이러한 오차 행렬의 특성에 따라 잘못 분류된 클래스를 파악하고 분류기의 성능 향상 방안에 대해 생각해볼 수 있다. 특히 이 과정에서 데이터의 전처리가 중요한데, 3과 5의 클래스를 분류하는 예시의 경우 SGDClassifier을 적용할 경우 미세한 픽셀 강도의 차이가 클래스 분류의 오류로 이어지므로 이미지의 회전값을 0으로 조정하는 것이 이에 해당한다.


3.6 다중 레이블 분류

다중 레이블 분류 시스템 : 하나의 샘플에 여러 개의 클래스가 포함될 경우, 레이블을 달아 각 클래스를 구별하는 시스템

다중 레이블 분류 시 적절한 지표를 사용하는 것이 중요한데, 다음과 같이 F1 점수에 다른 가중치 기준을 적용하여 분류기를 평가할 수 있다.

1
2
3
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3) 
f1_score(y_multilabel, y_train_knn_pred, average = "macro") # 모든 레이블의 가중치가 같음
f1_score(y_multilabel, y_train_knn_pred, average = "weighted") # 타깃 레이블에 속한 샘플 수(지지도)에 가중치를 둠

3.7 다중 출력 분류

다중 출력 분류(다중 출력 다중 클래스 분류) 시스템 : 다중 레이블 분류에서 각 레이블이 다중 클래스가 될 수 있도록 일반화한 시스템

이를 잡음이 포함된 이미지를 예시로 설명해보자면, 잡음이 포함된 이미지의 각 픽셀은 하나의 레이블에 해당되며 모든 픽셀은 0~255의 다양한 값을 가질 수 있다. 해당 이미지에서 잡음을 제거 시 각 픽셀의 값은 변화하므로 다중 클래스가 될 수 있다. 따라서 이는 다중 레이블에 다중 클래스가 합쳐진 다중 출력 다중 클래스 분류이다.

Abstract

creating effective visualizations using expressive grammars


Screenshot 2021-05-20 at 17 05 21

End-to-End Trainable Neural Translation Model

  • visualization generation === language translation problem

    formulation

    Vega-Lite

    • mapping : data specifications → visualization specifications
    • in a declarative language

    training

    • a multilayered, attention-based encoder-decoder network

    • with long short-term memory units ( LSTM )

      LSTM

    • on a corpus of visualization specifications


learns…

  • the vocabulary and syntax for a valid visualization specification
  • appropriate transformations
    • e.g. count, bins, mean
  • how to use common data selection patterns that occur within data visualizations

Problem Formulation

applies deep learning for translation and synthesis


  1. data visualization problem === sequence to sequence models ( seq2seq )

    • input sequence : dataset
      • e.g. fields, values in json format
    • output sequence : valid Vega-Lite visualization specification
  2. sequence translation === encoder-decoder networks

    • encoder : reads and encodes a source sequence into a fixed length vector
    • decoder : outputs a translation based on this vector

    👉 jointly trained to maximize the probability of outputting a correct translation

seq2seq

  1. generates data that is sequential or temporally dependent

    • e.g. language translation
  2. finds applications for problems where the output or input is non-sequential

    • e.g. text summarization, image captioning

    👉 bidirectional RNN units

    👉 attention mechanisms


Details

ordinary RNN ( unidirectional )

  1. reads an input sequence x from the first token x_1 to the last x_m

  2. generates an encoding only based on the preceding tokens it has seen


Bidirectional RNN ( BiRNN )

  • consists of a forward RNN + a backward RNN

  • enables an encoding generation based on both the preceding and following tokens

  1. a forward RNN ( →f )

    1. reads the input sequence as it is ordered from x_1 to x_m
    2. calculates a sequence of forward hidden states (→h_1, …, →h_m)
  2. a backward RNN ( ←f )

    1. reads the sequence in the reverse order from x_m to x_1
    2. calculates a sequence of backward hidden states (←h_1, …, ←h_m)
  3. generates a hidden state →h_j

    →h_j

    • a concatenation of both the forward and backward RNNs ( h_j : [→h_j(T); ←h_j(T)]T )
    • contains summaries of both the preceeding and following tokens

attention mechanism

  • focuses on aspects of an input sequence
  • generates output tokens
  • makes translation models robust to performance degradation
  • generates lengthy sequences
  • enables model to learn mappings between source and target sequences of different lengths
  • improves the ability to interpret and debug sequence to sequence models as providing valuable insights on why a given token is generated at each step

  • e.g.

    image captioning

    • model focuses on specific parts of objects in an image
    • generates each word or token in the image caption

👇 enables us to use a sequence translation model which…

  1. first takes into consideration the entire data input (dataset)
  2. and then focuses on aspects of the input (fields) in generating a visualization specification

Model

Encoder-Decoder Architecture with Attention mechanism

  1. 2-layer Encoder

    • bidirectional recurrent neural network (RNN)
      • takes in an input sequence of source tokens x
      • and outputs a sequence of states h
  2. 2-layer Decoder

    • RNN

      • computes the probability of a target sequence y based on the hidden state h

      probability

      • generated based on the recurrent state of the decoder RNN, previous tokens in the target sequence and a context vector c_i

      context vector === attention vector

      • a weighted average of the source states
      • and designed to capture the context of source sequence that help predict the current target token
  3. each with 512 Long Short-Term Memory (LSTM) units (cells)

    • better than Gated Recurrent Unit (GRU)

Data and Preprocessing

Learning Objectives

model should…

  1. select a subset of fields to focus on, when creating visualizations

    • most datasets have multiple fields which cannot all be simultaneously visualized
  2. learn differences in data types across the data fields

    • e.g. numeric, string, temporal, ordinal, categorical, etc.
  3. learn the appropriate transformations to apply to a field given its data type

    • e.g. aggregate transform does not apply to string fields

    👇

    Vega-Lite Grammer

  • view-level transforms (aggregate, bin, calculate, filter, timeUnit)
    • field-level transforms (aggregate, bin, sort, timeUnit)

Achieving Objectives

character based sequence model

  • Challenge : a character tokenization strategy requires

    • more units to represent a sequence
    • a large amount of hidden layers
    • a large amount of parameters to model long term dependencies
  • Solution

    1. replace string and numeric field names using a short notation

      • e.g.* “str”, “num” in the source sequence (dataset)
    2. a similar backward transformation is done in the target sequence

      • to maintain consistency in field names

    👇

    • scaffolds the learning process by reducing the vocabulary size
    • prevents the LSTM from learning field names (which are not needed to be memorized)

Visualization Recommender System

lower the barrier to exploring basic visualizations

  • by automatically generating results
  • for analysts to search and select, not manually specify

Abstract


Screenshot 2021-05-19 at 14 20 00

machine learning-based approach

  • learns visualization design choices
  • from a large corpus of datasets
  • from associated visualisations

by…

  1. identify five key design choices
  2. using one million dataset-visualisation pairs

evaluation

for : generalizability and uncertainty

by : benchmark with a crowdsourced test set

result : comparable to human performance


Problem Formulation

Data visualization communicates information by representing data with visual elements


representations are specified using…

  • encodings : map from data to the retinal properties of graphical marks

  • retinal properties

    • e.g. position, length, colour
  • graphical marks

    • e.g. point, line, rectangle

e.g.

Screenshot 2021-05-19 at 15 04 20

To create scatterplot showing the relationship between MPG and Hp,

  • encoding each pair of data points of a circle on a 2D plane

  • while specifying other retinal properties such as size and colour


Method

Vega-lite

  • selecting mark type and fields to be encoded along the x- and y-axes

Tableau

  • placing the 2 columns onto the respective column and row shelves



  1. formulate basic visualization of a dataset d as a set of interrelated design choices C = {c}

    set of choices that result in valid visualizations

    === a subset of the space of all possible choices

  2. automatically suggest a subset of design choices C_rec ⊆ C that maximize effectiveness

    effectiveness can be defined by informational measures

    • e.g. efficiency, accuracy, memorability, emotive measures

Rule-based Visualization Recommender Systems


ML-based Visualization Recommender Systems

DeepEye
Data2Vis
Draco-Learn
VizML
  • Pros

    • learning task : learn to…

      predict design choices

      • easier to quantitatively validate
      • easier to provide interpretable measures of feature importance
      • easily integrated into visualization systems

      DeepEye : classify and rank visualizations

      Data2Vis : end-to-end generation model

      Draco-Learn : soft constraints weights

    • data quantity : training corpus is…

      orders of magnitude larger than DeepEye and Data2Vis

      • permits the use of large feature sets that capture many aspects of a dataset
      • permits the use of high-capacity models such as deep neural network
    • data quality : the datasets used are…

      extremely diverse in shape, structure, and distribution

      the result of real visual analysis by analysts on their own datasets

      others

      • few datasets used to train
      • visualisations are generated by rule-based systems
      • evaluated number controlled settings

  • cons
    • only recommends visual encodings, not data queries
    • do not create ★ an application that employs the visualization model ★

Data

Feature Extraction

distinguish the feature categories to…

  1. organize how to create and interpret features
  2. observe the contribution of different types of features
  3. to capture if less generalizable than other categories

Screenshot 2021-05-19 at 15 26 21
  • Dimensions D : the number of rows in a column
  • Types T : whether a column is categorical, temporal, or quantitative
  • Values V : the statistical and structural properties of the values within a column
  • Names N : the column name

👉 order in D - T - V - N by how biased the features to be towards the corpus


Method
  1. describe each pair of columns with 30 pairwise-column features

  2. divide into 2 categories : Values and Names

    many pairwise-column features depend on the individual column types

  3. create 841dataset-level features by aggregating single- and pairwise-column features using 16 aggregation functions

    aggregation function : convert single- and pairwise-column features into scalar values

    1. train separate models per number of columns
    2. include column features with padding

Design Choice Extraction

extract an analyst’s design choices to…

parse the traces because each visualization consists of traces that associate collections of data with visual elements


Encoding-level Desing Choices

  • mark type : scatter, etc.
  • column encoding : which column is represented on which axis, whether or not an X or Y column is the single column represented along that axis

by aggregating these choices…

👇

Visualization-level Design Choices

  1. describe the type shared among all traces

  2. determine whether the visualization has a shared axis


Methods

Feature Processing

  1. apply one-hot encoding to categorical features

  2. set numeric values above 99% or below 1% to respective cut-offs

  3. impute missing categorical values using the mode of non-missing values, and missing numeric values with the mean of non-missing values

  4. remove the mean of numeric fields and scale to unit variance

  5. randomly remove datasets that are exact deduplicates of each other

    remove all but one randomly selected dataset per user

    👉 remove bias towards more prolific users


Prediction Tasks

Two visualization-level prediction tasks

use dataset-level features

Screenshot 2021-05-19 at 15 49 37

to predict visualization-level design choices


Three encoding-level prediction tasks

use features about individual columns

Screenshot 2021-05-19 at 15 49 50

to predict how they are visually encoded

consider each column indepedently,

instead of alongside other columns in the same dataset



Visualization Type & Mark Type task

2 class task : line vs. bar

3 class task : scatter vs. line vs. var


Neural Network and Baseline Models

  • fully-connected feedforward neural network

  • with 3 hidden layers, each consisting of 1000 neurons

  • with ReLU activation functions

  • split the data in 60/ 20/ 20 (train/ validation/ test sets) 5 times using 5-fold cross-validation


Features

  1. D
  2. D+T
  3. D+T+V
  4. D+T+V+N = ALL

Concurrency

동시성


자바스크립트는…

동기식, 싱글스레드 언어 이다. 이는 즉,

  • 호이스팅 이후 순차적으로 코드를 실행함을 의미하며,
  • 페이지에서 자바스크립트 블록이 실행 중이라면 다른 자바스크립트는 페이지에서 실행되지 못함

을 의미한다.

이 때, 싱글스레드 프로그래밍 언어이기 때문에 동기적 방식에 비동기 처리가 필수적이다. 비동기 처리는 그 결과가 언제 반환 될지 알 수 없기 때문에 동기식과 같이 결과 반환을 예상할 수 있게 처리하는 기법들이 사용되어야 한다. 자바스크립트에서 이와 같은 비동기 처리의 대표적 방식은 setTimeOut , callback , promise 등이 있다.

자바스크립트는 Ajax를 호출하는 등의 기능을 수행할 때만 (자동적으로) 비동기식 언어로 작동한다. Ajax 호출에 대한 응답이 돌아와야만 (성공적이든 거절되든) Ajax 호출 실행이 중단되고 다른 (다음) 코드가 실행된다. 이 때, 콜백함수는 동기적으로 작동한다. 응답이 돌아오기 전까지, 콜백함수가 실행 중일 때는 다른 코드는 실행되지 않으며 실행 중인 다른 코드 (작동 중인 다른 프로그램) 를 방해하지는 않는다.


동기(synchronous)적 방식 : 현재 실행 중인 코드가 완료된 후 다음 코드를 실행

  • 요청 처리가 완료된 후 다음 요청을 처리 하는 방식으로 이전 요청을 처리하는 시간이 다음 요청에 영향을 준다.
    • 요청과 응답이 같은 시간대에 있다. (사이에 시간 간격을 두지 않음)
  • 일반적으로 작성한 코드는 보통 동기 방식으로 처리된다.

비동기(asynchronous)적 방식 : 현재 실행 중인 코드의 완료 여부 (매개변수가 아닌 함수 실행 여부) 와 무관하게 즉시 다음 코드로 넘어가서 실행

  • 요청과 응답이 다른 시간대에 일어날 수 있다.

  • setTimeout()

single-threaded

싱글스레드 언어는 한 번에 하나의 작업만 수행할 수 있다.

🤔 그렇다면 자바스크립트를 주로 사용하는 웹 사이트에서는 어떻게 한번에 여러 요청을 받을까? 그리고 여러 요청이 오갈 수 있는 자바스크립트는 왜 싱글 쓰레드일까?

정확하게 말하면 자바스크립트의 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어라고 부른다. 하지만 이벤트 루프만 독립적으로 실행되지 않고 웹 브라우저나 NodeJS같은 멀티 쓰레드 환경에서 실행된다. 즉, 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아니다.


콜백 지옥

콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 복잡도가 커지는 현상으로, 엄청나게 많은 중괄호 중첩을 사용하여 가독성이 떨어지고 코드를 수정하기 어려움.

  • 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 콜백 함수를 이용함

e.g. 아래 코드는 0.5초마다 커피 목록을 수집하고 출력합니다.

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
setTimeout(
(name) => {
let coffeeList = name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);
},
500,
'Latte',
);
},
500,
'Mocha',
);
},
500,
'Americano',
);
},
500,
'Espresso',
);
1
2
3
4
5
> 출력값
Espresso (0.5초)
Espresso, Americano (1.0초)
Espresso, Americano, Mocha (1.5초)
Espresso, Americano, Mocha, Latte (2.0초)

각 콜백은 커피 이름을 전달하고 목록에 이름을 추가합니다. 정상적으로 실행되지만 들여쓰기 수준이 과도하게 깊어지고 값이 아래에서 위로 전달되어 가독성이 떨어집니다.


콜백 지옥 탈출
  • 기명함수 : 가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은 익명의 콜백 함수를 모두 기명함수로 전환하는 것입니다.

    • 코드의 가독성을 높일 수 있고 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽는데 어려움이 없다.
    • 일회성 함수를 전부 변수에 할당하는 것은 코드명을 일일이 따라다녀야 하기 때문에 오히려 헷갈림을 유발할 소지가 있다.

    e.g.

    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
    let coffeeList = '';

    const addEspresso = (name) => {
    coffeeList = name;
    console.log(coffeeList);
    setTimeout(addAmericano, 500, 'Americano');
    };

    const addAmericano = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addMocha, 500, 'Mocha');
    };

    const addMocha = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addLatte, 500, 'Latte');
    };

    const addLatte = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    };

    setTimeout(addEspresso, 500, 'Espresso');
  • Promise

    • new 연산자와 함께 호출한, Promise 의 인자로 넘겨주는 콜백 함수호출 시 바로 실행되지만 (비동기적) 그 내부에 resolve 또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then또는 catch로 넘어가지 않는다 (동기적). 따라서 비동기 작업이 완료될 때 resolve 또는 reject 를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해진다.

    e.g.

    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
    new Promise((resolve) => {
    setTimeout(() => {
    let name = 'Espresso';
    console.log(name);
    resolve(name);
    }, 500);
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Americano';
    console.log(name);
    resolve(name);
    }, 500);
    });
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Mocha';
    console.log(name);
    resolve(name);
    }, 500);
    });
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Latte';
    console.log(name);
    resolve(name);
    }, 500);
    });
    });

    아래는 위 코드와 같은 내용을 간결하게 표현한 코드이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const addCoffee = (name) => {
    return (prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    const newName = prevName ? `${prevName}, ${name}` : name;
    console.log(newName);
    resolve(newName);
    }, 500);
    });
    };
    };

    addCoffee('Espresso')()
    .then(addCoffee('Americano'))
    .then(addCoffee('Mocha'))
    .then(addCoffee('Latte'));
  • Generator

    • Generator 함수 : function* () {}
    • Generator 함수를 실행하면 Iterator가 반환되는데, Iterator next 메서드를 가지고 있다. 이 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield 전까지 코드를 실행한다. 따라서 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행되는 것과 같다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const addCoffee = (prevName, name) => {
    setTimeout(() => {
    coffeeMaker.next(prevName ? `${prevName}, ${name}` : name);
    }, 500);
    };

    const coffeeGenerator = function* () {
    const espresso = yield addCoffee('', 'Espresso');
    console.log(espresso);
    const americano = yield addCoffee(espresso, 'Americano');
    console.log(americano);
    const mocha = yield addCoffee(americano, 'Mocha');
    console.log(mocha);
    const latte = yield addCoffee(mocha, 'Latte');
    console.log(latte);
    };

    const coffeeMaker = coffeeGenerator();
    coffeeMaker.next();
  • Promise + async/ await

    • 비동기 작업을 수행하고자 하는 함수 앞에 async 를 붙이고, 함수 내부에서 비동기 작업이 필요한 위치에 await 를 붙임으로써 해당 라인의 코드 내용을 Promise 로 (자동) 전환하고 해당 내용이 resolve 된 이후에야 그에 대한 코드가 진행된다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const addCoffee = (name) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve(name);
    }, 500);
    });
    };

    const coffeeMaker = async () => {
    let coffeeList = '';
    let _addCoffee = async (name) => {
    coffeeList += (coffeeList ? ', ' : '') + (await addCoffee(name));
    };
    await _addCoffee('Espresso');
    console.log(coffeeList);
    await _addCoffee('Americano');
    console.log(coffeeList);
    await _addCoffee('Mocha');
    console.log(coffeeList);
    await _addCoffee('Latte');
    console.log(coffeeList);
    };

    coffeeMaker();



  1. Callback 대신 Promise 사용하기

    • ES2015/ ES6 기준
    • 콜백 지옥 방지
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
// bad
require('request')
.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { // 1차 콜백
// catch 구문
if (requestErr) {
console.error(requestErr);
}
// then 구문 ... 반복
else {
require('fs')
.writeFile('article.html',
response.body, (writeErr) => { // 2차 콜백
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
}
);
}
}
);

// good
require('request-promise')
.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
// Promise가 resolved를 반환하였을 때
.then((response) => {
return require('fs-promise').writeFile('article.html', response);
})
// 다음 Promise
.then(() => {
console.log('File written');
})
// Promise가 rejected를 반환하였을 때
.catch((err) => {
console.error(err);
});
  1. Async/ Await 은 Promise보다 더욱 깔끔하다.

    • ES2017/ ES8 기준
    • 콜백에 대한 Promise을 단독으로 이용할 때보다 더 깔끔한 해결책으로, Promise을 활용한 방식이다.
    • 함수 앞에 async 붙이기
    • 함수의 연속적인 처리 (논리적으로 연결) 를 위해 then 을 계속 이어붙여 코드를 작성하지 않아도 됨
    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
    // not bad
    require('request-promise')
    .get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
    .then(response => {
    return require('fs-promise').writeFile('article.html', response);
    })
    .then(() => {
    console.log('File written');
    })
    .catch(err => {
    console.error(err);
    })

    // better
    async function getCleanCodeArticle() {
    // then 구문
    try {
    // then 작성 없이 await으로 synchronization
    const response = await require('request-promise')
    .get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
    // then 구문 // await로 Promise 처리
    await require('fs-promise').writeFile('article.html', response);
    console.log('File written');
    }
    // catch 구문
    catch(err) {
    console.error(err);
    }
    }


Ref

https://velog.io/@yujo/JS%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EA%B3%BC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%A0%9C%EC%96%B4