0%

Lifting State Up

종종 변경 사항을 여러 컴포넌트에 공유 해야할 상황이 있는데 이때 state를 선조 컴포넌트로 끌어 올리는 것이 유용하게 동작한다.

이번에는 물이 특정 온도에서 끓는지 여부를 추정하는 온도계를 만들어 본다.

props 로 celsius를 받고 끓는 온도인지 판단하는 컴포넌트인 BolingVerdict를 만든다.

1
2
3
4
5
6
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}

다음으로 Calculator 를 만든다. 사용자의 인풋을 받아서 상태로 관리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: "" };
}

handleChange(e) {
this.setState({ temperature: e.target.value });
}

render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input value={temperature} onChange={this.handleChange} />
<BoilingVerdict celsius={parseFloat(temperature)} />
</fieldset>
);
}
}

Adding a Second input

화씨 입력도 입력 받아 동기화 하도록 한다.

Calculator 에서 TemperatureInput을 추출한다. props 로 scale를 받아서 화면에 화씨 인지 섭씨 인지 표시해 준다.

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
const scaleNames = {
c: "Celsius",
f: "Fahrenheit",
};

class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: "" };
}

handleChange(e) {
this.setState({ temperature: e.target.value });
}

render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature} onChange={this.handleChange} />
</fieldset>
);
}
}

이제 두개의 분리된 인풋으로 Calculator를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}

하지만, 두개의 인풋 사이에서 동기화는 되지 않는다. 또한, 현재 입력한 온도가 TemperatureInput에서 관리하기 때문에 Calculator 에서 BoilingVerdict도 렌더할 수 없다.

Writing Conversion Functions

섭씨 화씨 변환 함수 작성

1
2
3
4
5
6
7
function toCelsius(fahrenheit) {
return ((fahrenheit - 32) * 5) / 9;
}

function toFahrenheit(celsius) {
return (celsius * 9) / 5 + 32;
}

입력한 온도가 숫자로 변환할 수 없으면 빈 문자열을 반환하고, 변환 가능하며 소수점 세 번째 자리에로 반올림하여 리턴한다.

1
2
3
4
5
6
7
8
9
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return "";
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}

Lifting State Up

지금은 인풋 컴포넌트가 각자 상태를 관리하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}

handleChange(e) {
this.setState({temperature: e.target.value});
}

render() {
const temperature = this.state.temperature;
// ...

리엑트는 공유 되는 State를 만들기 위해서 가장 가까운 조상 요소로 state를 옮기는 방법을 사용하는데 이를 Lifting State Up 이라고 한다.

예제에서는 인풋에 조상 요소인 Calculator 가 state를 관리할 수 있도록 하고 props 로 인풋에 온도를 전달해주면 동기화 기능을 구현할 수 있다.

인풋에서 state로 사용하던 temperature 를 props에 temperature 로 변경한다.

1
2
3
4
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...

props는 읽기 전용이다. 그전에 TemperatureInput에서 변경 됬을때 다시 렌더링 하기 위해서 this.setState()를 호출했던것은 이제 할 수 없다. 대신에 온도 변화에 해당하는 함수인 onTemperatureChange와 같은 함수를 조상 요소인 Calculator에서 만들어서 props로 전달해 주면 똑같은 기능을 할 수 있게 만들 수 있다.

onTemperatureChnage 와 같은 함수 이름은 임의로 정한 것이다 onValueChange 와 같은 이름도 가능하고 마음대로 설정 가능하다.

TemperatureInput에 변경사항은 다음과 같다 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}

handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}

render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature} onChange={this.handleChange} />
</fieldset>
);
}
}

Calculator 의 변경사항은 다음과 같다.:

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
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = { temperature: "", scale: "c" };
}

handleCelsiusChange(temperature) {
this.setState({ scale: "c", temperature });
}

handleFahrenheitChange(temperature) {
this.setState({ scale: "f", temperature });
}

render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius =
scale === "f" ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit =
scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature;

return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange}
/>
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
}

Thinking in React

React로 상품들을 검색할 수 있는 데이터 테이블을 만드는 과정을 통해 리엑트로 생각한다는것이 무엇인지 알아보자.

목업으로 시작하기

다음과 같은 JSON을 서버로 부터 받았다고 가정하자.

1
2
3
4
5
6
7
8
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

1단계: UI를 컴포넌트 계층 구조로 나누기

디자이너가 디자인한 화면을 보고 박스를 그리며 일므을 붙여본다. 이미 그려져 있다면 디자이너와 상의 해보면서 수정하거나 의미를 확실히 파악하도록 해본다.

하지만 어떤것이 컴포넌트가 될지 알수 있을까? 이것은 우리가 함수나 객체를 만들때처럼 생각하면 된다. 한가지 컴포넌트는 한가지 일만 하도록 작게 나누면서 단일 책임 원칙을 지키는 것이다.

JSON 데이터 모델이 적절히 만들어 졌다면 UI와 잘 연결 될 것이다. 이것은 ui와 데이터 모델이 information architecture 을 가지는 경향이 있기 때문이다.

  1. FilterableProductTable(노란색): 예시 전체를 포괄합니다.
  2. SearchBar(파란색): 모든 유저의 입력(user input) 을 받습니다.
  3. ProductTable(연두색): 유저의 입력(user input)을 기반으로 데이터 콜렉션(data collection)을 필터링 해서 보여줍니다.
  4. ProductCategoryRow(하늘색): 각 카테고리(category)의 헤더를 보여줍니다.
  5. ProductRow(빨강색): 각각의 제품(product)에 해당하는 행을 보여줍니다.

3번은 ProductTableHeader로 바꾸어 Name Price만 따로 표현하는게 더 합리적일 수 있다. 그러나 여기선 데이터 콜렉션이라는 역활 책임을 생각해서 남두었다. 뭐가 좋을지는 선택하면 된다.

이제 계층적으로 나타내면 다음과 같다.

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

2단계 : React로 정적인 버전 만들기

데이터 모델을 가지고 UI 렌더링은 되지만 상호 작용은 안되는 정적인 버전을 만들어 본다. 정적은 버전은 생각을 적게하고 타이핑을 많이 하고, 상호작용을 위해서는 생각을 많이 하고 타이핑을 적게 하는데, 나중에 살펴 보기로 하고 정적인 버전을 만들어 본다.

정적 버전을 위해서 우선은 state를 사용하지 말아라. 우선은 props로 데이터를 부모에서 자식으로 전달하면서 만들어 보아라.

컴포넌트는 top-down(하양식) 이나 bottom-top(상향식) 으로 만들 수 있다. 간단한 예시에서는 보통 하향식으로 만드는게 쉽지만 프로젝트가 커지면 상향식으로 만드록 테스트를 작성하면서 개발하기가 더 쉽다.

이 단계가 끝나면 데이터 렌더링을 위해 만들어진 재사용 가능한 컴포넌트들의 라이브러리르 가지게 된다. 지금은 render() 메서드만 가지고 있다. 최상위 컴포넌트는 props를 통해 데이터 모델을 받고 자식 컴포넌트에 전달하는데 한번 만들어 봄으로서 이런 흐름을 파악하기 쉽다.

3 단계: UI state 에 대한 최소한의 (하지만 완전한) 표현 찾아 내기

이제 변경해야할 최소한의 state는 무엇일지 생각해본다. TODO 리스트에서 예를 들면 해야할 일 목록을 state를 관리하고, 할일 목록 갯수를 state를 관리하지 않는것을 의미한다. 이것은 중복 배제 원칙이라고 하는데 만약 할일 목록 갯수를 state로 관리하고 싶다면 할일 목록 배열의 갯수를 세는 방법을 사용한다.

다음 목록에서 state가 될만한 것이 무엇이 있을지 생각해 보자.

  • 제품의 원본 목록
  • 유저가 입력한 검색어
  • 체크박스의 값
  • 필터링 된 제품들의 목록

다음 3가지 질문을 통해서 확인할 수 있다.

  1. 부모로부터 props를 통해 전달 됩니까? -> state가 아니다.
  2. 시간이 지나도 변하지 않나요? -> state가 아니다.
  3. 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요? -> 그렇다면 state 가 아니다.

결과적으로 state는 다음 목록이 된다.

  • 유저가 입력한 검색어
  • 체크박스의 값

4단계 : state가 어디 있어야 할지 찾기

리엑트는 단방향 데이터 흐름을 갔기 때문에 어떤 컴포넌트에 state를 가지게 할지가 중요하다. 다음과 같은 기준을 생각해서 정해보자.

  • state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요.
  • 공통 소유 컴포넌트 (common owner component)를 찾으세요. (계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트).
  • 공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 합니다.
  • state를 소유할 적절한 컴포넌트를 찾지 못하였다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하세요.

결정하는 과정으 다음과 같다.

  • ProductTable은 state에 의존한 상품 리스트의 필터링해야 하고 SearchBar는 검색어와 체크박스의 상태를 표시해주어야 합니다.
  • 공통 소유 컴포넌트는 FilterableProductTable입니다.
  • 의미상으로도 FilterableProductTable이 검색어와 체크박스의 체크 여부를 가지는 것이 타당합니다.

5단계: 역방향 데이터 흐름 추가하기

우리가 만든 FilterableProductTable 에서 아직 SeacrBar에 setState를 콜백으로 주지 않았기 때문에 폼을 입력해도 데이터가 변하지 않게 된다. 하위 컴포넌트인 SearchBar에서 상위 컴포넌트인 FilterableProductTable에 state를 변경하기 위해서는 콜백으로 setState를 주고 하위 컴포넌트에서 호출하면된다.

이렇게 역방향 데이터 흐름을 추가할 수 있다.

이게 전부임..

합성 (Composition) vs 상속 (Inheritance)

React는 강력한 합성 모델을 가지고 있으며, 상속 대신 합성을 사용하여 컴포넌트 간에 코드를 재사용한다.

컴포넌트에서 다른 컴포넌트 담기

props children으로 컴포넌트를 전달해서 컴포넌트를 랜더링 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function FancyBorder(props) {
return (
<div className={"FancyBorder FancyBorder-" + props.color}>
{props.children}
</div>
);
}

function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">Welcome</h1>
<p className="Dialog-message">Thank you for visiting our spacecraft!</p>
</FancyBorder>
);
}

특수화

일반적인 컴포넌트에서 특수한 경우에 컴포넌트를 뽑아 낼 수 있다. 예를 들어서 Dialog 컴포넌트는 일반적인 컴포넌트라고 할 수 있고 WelcomeDialog는 특수한 컴포넌트라고 할 수 있다.

이런 컴포넌트를 그릴 때에도 합성을 사용할 수 있는데, 구체적인 컴포넌트에서 일반적인 컴포넌트를 담아서 그리는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">{props.title}</h1>
<p className="Dialog-message">{props.message}</p>
</FancyBorder>
);
}

function WelcomeDialog() {
return (
<Dialog title="Welcome" message="Thank you for visiting our spacecraft!" />
);
}

ReactDOM.render(<WelcomeDialog />, document.getElementById("root"));

마찬가지로 클래스 컴포넌트에서도 합성을 사용할 수 있다.

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
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">{props.title}</h1>
<p className="Dialog-message">{props.message}</p>
{props.children}
</FancyBorder>
);
}

class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = { login: "" };
}

render() {
return (
<Dialog
title="Mars Exploration Program"
message="How should we refer to you?"
>
<input value={this.state.login} onChange={this.handleChange} />
<button onClick={this.handleSignUp}>Sign Me Up!</button>
</Dialog>
);
}

handleChange(e) {
this.setState({ login: e.target.value });
}

handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}

상속은?

대부분 사용하지 않는다. React에서는 Props로 어떤 값도 전달 가능하기 때문에 모양과 동작을 유연하게 커스터마이징 할 수 있다.
또한, JavaScript이기 때문에 모듈로 분리할 수 도 있고 필요할때 import 하기도 가능하다.

hooks 개요

Hook 이 뭔가요?

Hook은 함수 컴포넌트에서 React state와 생명주기 기능을 연동(Hook Into) 할 수 있게 해주는 함수이다.

hooks 를 사용하면 class 컴포넌트가 아닌 함수형 컴포넌트로 생명주기 메서드와 동일한 기능을 사용할 수 있다.

버튼을 클릭하면 count가 1씩 증가형 rendering 하는 컴포넌트이다. 여기서 useState를 hooks 라고 하고 초기값을 입력하여 호출하면 값과 설정해주는 함수가 나온다. setCount와 같은 메서드는 클래스 컴포넌트에 this.setState와 같다고 할 수 있는데 setState 처럼 꼭 객체를 넣을 필요가 없고, 이전 state와 새로운 state를 합치지 않는다는 차이점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
function App() {
const [count, setCount] = React.useState(0);

return (
<>
<p> Conter is {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Plus button</button>
</>
);
}

ReactDOM.render(<App />, document.getElementById("root"));

Effect Hook

컴포넌트 안에서 데이털르 가져오거나 구독하고, DOM을 직접 조작하는 작업을 종종 하게 된다. 이런 작업은 다른 컴포넌트에 영향을 줄 수도 있기 때문에 effect 또는 side effcect 라고 한다.

Effect Hook, 즉 useEffect는 side effects를 수행할 수 있게 해준다. 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount 와 같은 목적으로 제공된다.

다음 예는 동적으로 브라우저에 타이틀을 변경 시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function App() {
const [count, setCount] = React.useState(0);

React.useEffect(() => {
document.title = `You Clicked ${count} times`;
}, [count]);

return (
<>
<p> Conter is {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Plus button</button>
</>
);
}

ReactDOM.render(<App />, document.getElementById("root"));

마치 componentDidMountcomponentDidUpdate를 합쳐 놓은것과 같다.

만약 side effect를 일으키는 작업을 해제 시켜야 한다면 useEffect내에서 함수를 반환하면 된다. componentWillUnmount와 같다.

useEffect는 useState와 마찬가지로 여러 개를 사용할 수 있고, effect 해제와 같은 로직을 한곳에 작성할 수 있어서 관리하기 슆다.

Hook 사용 규칙

  • 최상위 에서만 Hook을 호출해야한다. 중첩된 함수에서 hook 실행 금지

  • React 함수 컴포넌트에서만 Hook 을 호출해야 한다. 일반 컴포넌트에서는 Hook 을 호출하면 안된다. 유일하게 허용하는 경우는 costomHook 에서다.

costom hook

커스텀 훅은 컴포넌트 트리에 새 컴포넌트를 추가하지 않고 상태 관련 로직을 재사용할 수 있게 해준다.

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

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

이것을 다른 컴포넌트에서 재사용할 수 있다.

1
2
3
4
5
6
7
8
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

if (isOnline === null) {
return "Loading...";
}
return isOnline ? "Online" : "Offline";
}

다른 컴포넌트 :

1
2
3
4
5
6
7
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);

return (
<li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
);
}

Hook 은 state 자체가 아니다. Hook의 호출은 완전히 독립된 state를 가진다.

custom Hook 은 use 로 시작하고 안에서 Hook 을 호출한 것을 의미한다.

다른 내장 Hook

유용하다고 생각될 만한 내장 Hook 들이 몇몇 있다. 몇가지만 소개하면

useContext 는 컴포넌트를 중첩하지 않고도 React context를 구동할 수 있게 해준다.

1
2
3
4
5
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
}

useReducer 는 복잡한 컴포넌트들의 state를 reducer로 관리할 수 있게 해준다.

1
2
3
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...

Jest Matcher

jest란?

jest 는 자바스크립트 코드를 테스트 할수 있게 해주는 도구이다. 패키지만 다운받으면 별도의 설정없이 바로 사용할 수 있는것이 특징이다.

최소한의 설정?

패키지 설정 및 스크립트 설정에 관한 부분으로 거의 할게 없다.

npm init -y : npm 초기화

npm i jest -D : 개발 과정에서만 사용할 것이므로 jest를 -D 옵션을 붙여서 설치한다. –save-dev 와 똑같다.

package.json으로 이동하여 script를 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "jest-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest" // test 부분에 jest 추가
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^27.3.0"
}
}

npm test : 테스트 코드 실행 가능

test 코드는 test.js 등의 이름이나. __tests__ 폴더 아래 있는 모든 파일을 찾아서 실행한다.

matcher

matcher는 테스트에서 예상한 부분과 실제 결과가 맞는지 매칭시키는 함수를 의미한다. jest 다양한 matcher 가 존재하는데 모두다 외울 필요는 없고 필요할때 확인하면 된다.
보통 expect().matcher()를 사용하여 검사하는데 맞지 않는경우를 조사하려면 not을 붙혀서 expect().not.matcher()를 사용하면 된다.

  • toBe() : 결과가 같은지 확인, 아닌 경우를 판단하려면 expect().not.matcher()를 사용하면 된다.
1
2
3
test("1은 1이야.", () => {
expect(1).toBe(1);
});
  • toEqaul() : 결과가 같은지 확인, 배열 객체 등의 요소는 재귀적으로 판단해야 하기 때문에 이것을 사용해야 한다.
1
2
3
4
5
6
test("이름과 나이를 전달받아서 객체를 반환해줘", () => {
expect(fn.makeUser("mike", 30)).toEqual({
name: "mike",
age: 30,
});
});
  • toStrictEqual() : 보다 엄격한 검사를 실시한다.
  • toBeNull() : Null 값인지 검사
  • toBeUndefined() : undefined인지 검사
  • toBeDefined
  • toBeTruthy : truthy인 값을 리턴하는지 검사.
1
2
3
test("비어있지 않은 문자열은 truly 입니다.", () => {
expect(fn.add("koo", "ja")).toBeTruthy();
});
  • toBeFalsy : falsy인 값을 리턴하는지 검사.

숫자 관련된 matcher도 있다.

  • toBeGraterThan() : 큰지 검사
  • toBeGraterThanOrEqual() : 크거나 같은지 검사
  • toBeLessThan() : 작은지 검사
  • toBeLessThanOrEqual() : 작거나 같은지 검사
1
2
3
4
test("Id 는 10글자 보다 작아야 한다.", () => {
const ID = "ID_KJDB_";
expect(ID.length).toBeLessThanOrEqual(10);
});

근사치를 검사해야하는 경우도 있는데 이럴땐 toBeCloseTo를 사용하면 된다.

  • toBeCloseTo : 근사치 검사
1
2
3
test("0.1 더하기 0.2는 0.3 이다.", () => {
expect(fn.add(0.1, 0.2)).toBeCloseTo(0.3);
});

문자열 관련해서 정규 표현식을 사용할 수 있는데 toMatch를 사용하면 된다.

  • toMatch(regex) : 정규표현식과 매칭 되는지
1
2
3
test("Hello World 에 H라는 글자가 있나?", () => {
expect("Hello World").toMatch(/H/);
});

배열 관련된 matcher 도 있다.

  • toContain(요소) : 인자로 넘겨준 요소가 매열내의 포함되어 있는지
1
2
3
4
5
test("유저 리스트에 Mike가 있는지", () => {
const user = "Mike";
const users = ["Tom", "Mike", "Kai"];
expect(users).toContain(user);
});

에러가 발생하는지 여부도 확인할 수 있다.

  • toThrow() : 인자를 넘겨주면 특정 에러인지 확인하고 인자를 넘겨주지 않으면 에러를 발생시키는 여부만 확인한다.
1
2
3
4
5
6
7
test("이거 에러 나나?", () => {
expect(() => fn.throwErr()).toThrow();
});
// 어떤 내용인지 확인하려면 인수로 전달하면 된다.
test("이거 xx 에러인가? ", () => {
expect(() => fn.throwErr()).toThrow("xx");
});

비동기 코드 테스트

jest 는 테스트가 끝나면 바로 끝나버림. 콜백 함수를 넘겨주어도 기다리지 않고 끝나기 때문에 제대로 된 테스트를 진행할 수 없다. 이때는 test네 콜백에 인자로 done을 넘겨주면 된다. 이 done이 호출되면 테스트가 끝났다고 명시할 수 있어서 콜백이 끝나는 부분에서 done을 호출하면 제대로된 테스트를 할 수 있다.

fn :

1
2
3
4
5
6
7
8
9
10
11
12
13
const fn = {
add: (num1, num2) => num1 + num2,
makeUser: (name, age) => ({ name, age, gender: undefined }),
throwErr: () => {
throw new Error("xx");
},
getName: (cb) => {
const name = "Mike";
setTimeout(() => {
cb(name);
}, 3000);
},
};

test :

1
2
3
4
5
6
7
test("3초 후에 받아온 이름은 Mike이다.", (done) => {
const cb = (name) => {
expect(name).toBe("Mike");
done();
};
fn.getName(cb);
});

만약 done을 받고 호출하지 않으면 timeout으로 종료 되고 테스트는 실패한다.

에러를 검사하고 싶으면 try catch를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
test("서버 에러 비동기 테스트", (done) => {
const cb = (name) => {
try {
expect(name).toBe("Mike");
done();
} catch (error) {
done();
}
};
fn.getNameError(cb);
});

promise를 리턴하면 jest는 promise 가 resolve 될때까지 기다려 준다. 단 이때는 return을 명시해 주어야 한다.

1
2
3
test("promise test", () => {
return fn.getAge().then((age) => expect(age).toBe(30));
});

보다 간단하게 사용하기 위한 resolves, rejects matcher 가 있다.

1
2
3
test("promise test resolve Matcher", () => {
return expect(fn.getAge()).resolves.toBe(30);
});

async await 도 사용할 수 있다.

1
2
3
4
test("3초후에 async await test", async () => {
const age = await fn.getAge();
expect(age).toBe(30);
});

마찬가지로 resolves, rejects를 사용할 수 있다.

1
2
3
test("3초 후에 resolves 쓰는 패턴", async () => {
await expect(fn.getAge()).resolves.toBe(30);
});

test 전후

beforEach, afterEach

각 테스트 전에 먼저 실행되어야 할 코드 가 있을 수 있다. 예를 들어서 어떤 테스트가 다음 번에 테스트에 영향을 끼치는 변수를 변경하고 있다면 변수를 초기화 해주는 코드가 필요하다.

해당 작업은 beforeEach를 통해서 할 수 있다.

예시 코드는 각 test 전에 num 값을 0으로 초기화 해서 다음 test에 주는 영향을 없앤다.

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
const fn = require("./fn");

let num = 0;

beforeEach(() => {
num = 0;
});

test("0 더하기 1은 1이야", () => {
num = fn.add(num, 1);
expect(num).toBe(1);
});
test("0 더하기 2은 2이야", () => {
num = fn.add(num, 2);
expect(num).toBe(2);
});
test("0 더하기 3은 3이야", () => {
num = fn.add(num, 3);
expect(num).toBe(3);
});
test("0 더하기 4은 4이야", () => {
num = fn.add(num, 4);
expect(num).toBe(4);
});
test("0 더하기 5은 5이야", () => {
num = fn.add(num, 5);
expect(num).toBe(5);
});

afterEach 는 각 테스트 이후에 실행될 함수이다.

beforeAll, afterAll

어떤 테스트는 처음 테스트가 시행되기전에 한번 사전 작업을 하고, 모든 테스트가 끝나고 난 뒤에 수행해야 할 작업이 있을 수 있다. 예를들어 db에 커넥션을 얻고 해제하는 경우가 그렇다.

만약 beforeEachafterEach 같은 것을 사용하면 각 테스트마다 커넥션을 얻고 해제하는것을 반복하기 때문에 시간이 테스트 수 곱하기로 들것이다. 이러때는 All을 사용해서 한번씩만 호출되도록 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeAll(async () => {
user = await fn.connectUserDb();
});
afterAll(async () => {
user = await fn.disconnectUserDb();
});

test("이름은 KOO 야", () => {
expect(user.name).toBe("KOO");
});
test("나이는 27 야", () => {
expect(user.age).toBe(27);
});
test("성별은 남자야", () => {
expect(user.gender).toBe("male");
});

describe

테스트를 묶어서 설명할 수 있다. 테스트는 한 스코프 안에서 실행되는데 여기에 선언한 before, after 관련 함수는 해당 스코프를 범위로 실행된다.

car 스코프 안에서 커넥션을 얻고 해제하는 과정이 한번 일어난다.

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
const fn = require("./fn");

let user;
beforeAll(async () => {
user = await fn.connectUserDb();
});
afterAll(async () => {
await fn.disconnectUserDb();
});

test("이름은 KOO 야", () => {
expect(user.name).toBe("KOO");
});
test("나이는 27 야", () => {
expect(user.age).toBe(27);
});
test("성별은 남자야", () => {
expect(user.gender).toBe("male");
});

describe("Car 관련 작업", () => {
let car;
beforeAll(async () => {
car = await fn.connectCarrDb();
});
afterAll(async () => {
await fn.disconnectUserDb();
});
test("이름은 sonata 야", () => {
expect(car.name).toBe("sonata");
});
test("브랜드는 kia야", () => {
expect(car.brand).toBe("kia");
});
});

순서?

describe 바깥과 안에 각각 beforeAll, beforeEach, afterEach, afterAll 이 모두 정의 되어 있다면 어떤 순서로 진행 될까 ?

다음과 같은 경우이다 :

1
2
3
4
5
6
7
8
9
10
11
12
13
beforeAll(() => {});
beforeEach(() => {});
afterEach(() => {});
afterAll(() => {});
test("", () => {});

describe("", () => {
beforeAll(() => {});
beforeEach(() => {});
afterEach(() => {});
afterAll(() => {});
test("", () => {});
});

순서는 다음과 같다 :

  1. 밖 beforeAll
  2. 밖 beforeEach
  3. 밖 test
  4. 밖 afterEach
  5.    안 beforeAll
    
  6. 밖 beforeEach
  7.    안 beforeEach
    
  8. 안 test
  9. 안 afterEach
  10. 밖 afterEach
  11. 안 afterAll
    
  12. 밖 afterAll

test 시 순서에 유의하도록 한다.

skip only

skip 은 지정한 test를 스킵할때, only 는 지정한 test만 실행할때 사용할 수 있다.

test 가 실패햇다면 다음과 같은 순서로 작업을 진행할 수 있다.

  1. 어떤 test1 실패
  2. test1 만 only로 실행
  3. test1 수정 -> 통과 확인
  4. 영향 주는 test0 찾음
  5. test0을 수정하는것이 best지만 일단 test를 해야 한다면 test0을 skip 하고 실행

순서대로 하면 다음과 같다.
1 : 마지막 테스트 실패

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test("0 더하기 1은 1이야", () => {
expect(fn.add(num, 1)).toBe(1);
});
test("0 더하기 2은 2이야", () => {
expect(fn.add(num, 2)).toBe(2);
});
test("0 더하기 3은 3이야", () => {
expect(fn.add(num, 3)).toBe(3);
});
test("0 더하기 4은 4이야", () => {
expect(fn.add(num, 4)).toBe(4);
num = 10;
});
test("0 더하기 5은 5이야", () => {
expect(fn.add(num, 5)).toBe(5);
});

2 : 마지막 test만 only 로 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test("0 더하기 1은 1이야", () => {
expect(fn.add(num, 1)).toBe(1);
});
test("0 더하기 2은 2이야", () => {
expect(fn.add(num, 2)).toBe(2);
});
test("0 더하기 3은 3이야", () => {
expect(fn.add(num, 3)).toBe(3);
});
test("0 더하기 4은 4이야", () => {
expect(fn.add(num, 4)).toBe(4);
num = 10;
});
test.only("0 더하기 5은 5이야", () => {
expect(fn.add(num, 5)).toBe(5);
});

3,4 : 수정후 영향을 주는 test 확인

5 : 문제되는 test skip 하고 진행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test("0 더하기 1은 1이야", () => {
expect(fn.add(num, 1)).toBe(1);
});
test("0 더하기 2은 2이야", () => {
expect(fn.add(num, 2)).toBe(2);
});
test("0 더하기 3은 3이야", () => {
expect(fn.add(num, 3)).toBe(3);
});
test.skip("0 더하기 4은 4이야", () => {
expect(fn.add(num, 4)).toBe(4);
num = 10;
});
test("0 더하기 5은 5이야", () => {
expect(fn.add(num, 5)).toBe(5);
});

mock

test를 진행하기 위해서 많은 코들르 작성해야 할때 임시로 mock 함수를 사용해서 기능이 정상 작동하는지 확인할 수 있다.

calls

mock 안에 mock.calls 가 있어서 몇번 실행됬는지 인수는 무엇이였는지 확인 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
const fn = require("./fn");

const mock = jest.fn();

mock();
mock(1);

test("mock 함수는 2번 호출되었다.", () => {
expect(mock.mock.calls.length).toBe(2);
});
test("2번째 호출의 인수는 1이다.", () => {
expect(mock.mock.calls[1][0]).toBe(1);
});

인수도 확인 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fn = require("./fn");

const mock = jest.fn();

function addEachOne(arr) {
arr.forEach((num) => {
mock(num + 1);
});
}

addEachOne([10, 20, 30]);

test("mock 함수는 3번 불렸다.", () => {
expect(mock.mock.calls.length).toBe(3);
});
test("가 값은 11, 21, 31이다.", () => {
expect(mock.mock.calls[0][0]).toBe(11);
expect(mock.mock.calls[1][0]).toBe(21);
expect(mock.mock.calls[2][0]).toBe(31);
});

return

fn()에 콜백함수를 전달해서 result로 확인할 수 있다.

results 배열로 들어오고 value로 return 값 확인이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fn = require("./fn");

const mock = jest.fn((num) => num + 1);

mock(10);
mock(20);
mock(30);

test("10에서 1증가한 11이 반환된다.", () => {
expect(mock.mock.results[0].value).toBe(11);
});
test("20에서 1증가한 21이 반환된다.", () => {
expect(mock.mock.results[1].value).toBe(21);
});
test("30에서 1증가한 31이 반환된다.", () => {
expect(mock.mock.results[2].value).toBe(31);
});

mockReturnValueOnce를 통해서 한번만 리턴하는 값을 정할 수 있다.

체이닝으로 연결하고 마지막은 mockReturnValue로 정한다.

홀수 판별을 위한 콜백을 당장 작성할수 없다 판단될때 다음과 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fn = require("./fn");

const mock = jest.fn((num) => num + 1);

mock
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValue(true);

test("홀수는 135 이다.", () => {
const result = [1, 2, 3, 4, 5].filter((num) => mock(num));
expect(result).toStrictEqual([1, 3, 5]);
});

mockResolvedValue 를 통해서 비동기 함수를 흉내 낼 수 도 있다.

1
2
3
4
5
6
7
8
9
10
11
const fn = require("./fn");

const mock = jest.fn((num) => num + 1);

mock.mockResolvedValue({ name: "mike" });

test("이름은 mike이다", () => {
mock().then((res) => {
expect(res.name).toBe("mike");
});
});

기존 함수를 mock 함수로 만들기

유저 생성 함수를 test 한다고 생각할 때 실제로 user가 생성해 버리면 롤백하기도 번거로울 것이다.

1
2
3
4
5
6
createUser: (name) => {
console.log("실제로 유저가 생성되었다.");
return {
user: name
}
},

fn을 목킹 모드로 만든다. 이러면 실제 fn에 함수가 호출되는 것이 아니라 목함수가 동작한다.

1
2
3
4
5
6
7
8
9
10
const fn = require("./fn");

jest.mock("./fn");

fn.createUser.mockReturnValue({ user: "Mike" });

test("유저가 생성되었습니다.", () => {
const result = fn.createUser("Mike");
expect(result.user).toBe("Mike");
});

그 외에 유용한 메서드

mock 함수 호출에 대한 유용한 메서드를 제공한다.

  • toBeCalled() 한번이라도 호출됬으면 통과
  • toBeCalledTimes(3) 3번 통과 됬으면 통과
  • toBeCalledWith(10, 20) 인수로 주어진 것을 받은게 있다면 통과
  • lastCalledWith(10, 20) 마지막으로 받은 인수가 10, 20이면 통과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fn = require("./fn");

const mock = jest.fn();

mock(10, 20);
mock();
mock(30, 40);

test("호출됨", () => {
expect(mock).toBeCalled();
});
test("3 번 호출됨", () => {
expect(mock).toBeCalledTimes(3);
});
test("10, 20 인수로 받음", () => {
expect(mock).toBeCalledWith(10, 20);
});
test("호출됨", () => {
expect(mock).lastCalledWith(30, 40);
});

5.SQL 기본

SELECT 문

데이터 조회 조회할 테이블과 항목을 명시해 준다.

1
2
3
4
5
SELECT
CUSTOMER_ID ,
NAME ,
CREDIT_LIMIT
FROM CUSTOMERS ;

모든 항목 조회는

1
2
3
SELECT
*
FROM CUSTOMERS

DUAL 테이블

DB에서 기본적으로 제공해주는 테이블로서 연산 작업에 사용할 수 있다.

1
2
3
SELECT
*
FROM DUAL;
1
2
3
SELECT
(10 + 5) / 2 AS VAL
FROM DUAL;

ORDER BY

크기를 기준으로 정렬해준다.

  • ASC: 오름차순 (디폴트)
  • DESC: 내림차순
1
2
3
4
5
6
SELECT
NAME,
ADDRESS,
CREDIT_LIMIT,
FROM CUSTOMERS
ORDER BY NAME ASC;

ORDER BY 다음으로는 여러개의 항목을 줄 수 있다.

1
2
3
4
5
6
SELECT
FIRST_NAME,
LAST_NAME,
FROM CONTACTS
ORDER BY FIRST_NAME, LAST_NAME DESC
;

FIRST_NAME은 디폴트 값인 오름차순으로 정렬하고 그다음에 LAST_NAME을 기준으로 내림차순 정렬한다.

DISTINCT

중복 값을 제거한다.

1
2
3
4
SELECT
DISTINCT FIRST_NAME
FROM CONTACTS
ORDER BY FIRST_NAME

조회 결과 중에서 FIRST_NAME 이 겹친다면 중복된 요소를 제거한다.

WHERE

조건을 주어서 검생할 수 있다.

1
2
3
4
5
6
7
SELECT
PRODUCT_NAME,
DESCRIPTION,
LIST_PRICE,
CATEGORY_ID
FROM PRODUCTS
WHERE PRODUCT_NAME='Kinston';

일반적인 프로그래밍 언어와 마찬가지로 AND 와 OR을 사용할 수 있고 조건 사이에 넣어 주면 된다.

BETWEEN을 입력하여 이상 이하에 조건을 간략하게 표현할 수 있다.

1
2
3
4
5
6
SELECT
PRODUCT_NAME,
LIST_PRICE
FROM PRODUCTS
WHERE LIST_PRICE BETWEEN 650 AND 680
ORDER BY LIST_PRICE;

LIST_PRICE 가 650 이상 680 미만인 것을 조회한다.

IN 조건을 주면 집합에 속한 모든 요소를 출력할 수 있다. OR 조건과 비슷하다고 생각하면 된다.

1
2
3
4
5
6
SELECT
PRODUCT_NAME
, CATEGORY_ID
FROM PRODUCTS
WHERE CATEGORY_ID IN (1, 4)
ORDER BY PRODUCT_NAME ;

LIKE 를 사용해서 조건을 넣을 수 있다. 밑에는 ASUS로 시작하는 요소를 검색한다.

1
2
3
4
5
6
SELECT
PRODUCT_NAME
, LIST_PRICE
FROM PRODUCTS
WHERE PRODUCT_NAME LIKE 'Asus%'
ORDER BY LIST_PRICE ;

INSERT, UPDATE, DELETE 문

테이블 생성

1
2
3
4
5
6
7
DROP TABLE DISCOUNTS;
CREATE TABLE DISCOUNTS (
DISCOUNT_ID NUMBER GENERATED BY DEFAULT AS IDENTITY , DISCOUNT_NAME VARCHAR2(255) NOT NULL
, AMOUNT NUMBER(3, 1) NOT NULL
, START_DATE DATE NOT NULL
, EXPIRED_DATE DATE NOT NULL
);

INSERT

데이터를 삽입한다. 데이터를 삽입을 확정하기 위해서 COMMIT을 해야 한다. 하지만 보통 DB tools 에서는 자동으로 COMMIT 까지 들어가도록 지원한다. 테이블 생성 명령어는 COMMIT 이 필요하지 않다.

1
2
3
4
5
6
7
8
INSERT INTO
DISCOUNTS (
DISCOUNT_NAME , AMOUNT
, START_DATE
, EXPIRED_DATE )
VALUES ( 'Summer Promotion' , 9.5
, DATE '2017-05-01' , DATE '2017-08-31' );
COMMIT;

UPDATE

테이블 안에 데이터를 업데이트 한다.

1
2
3
4
UPDATE PARTS
SET COST = 130
WHERE PART_ID = 1 ;
COMMIT;

조건을 입력하지 않으면 테이블에 모든 행을 업데이트 한다. 조건문이 없는 UPDATE, DELETE 문은 DB tools 에서 경고문을 던진다.

1
2
3
4
UPDATE PARTS
SET COST = COST * 1.05
;
COMMIT;

DELETE

데이터를 삭제한다.

1
2
3
4
5
6
DELETE
FROM SALES
WHERE ORDER_ID = 1
AND ITEM_ID = 1
;
COMMIT;

VIEW

VIEW 는 물리적으로 데이터를 저장하지 않는 테이블이라고 생각할 수 있는데 쉽게 생각하면 어떤 SQL에 결과값을 저장해 둔 것이라고 생각하면 된다.

INLINE VIEW

SQL 문 안에서 INLINE VIEW 를 사용할 수 있다.

1
2
3
4
5
6
7
8
SELECT A.* FROM
(
SELECT
NAME
, CREDIT_LIMIT
FROM CUSTOMERS
)A
;

VIEW

복잡한 쿼리에 대한 결과를 VIEW로 저장해두고 사용할 수 있다. 다음 쿼리는 발송된 주문에 대한 연도별 각 고객의 매출 총 금액을 구하는 SQL 문이다.

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
C.NAME AS CUSTOMER
, TO_CHAR(A.ORDER_DATE, 'YYYY') AS YEAR
, SUM( B.QUANTITY * B.UNIT_PRICE ) SALES_AMOUNT FROM ORDERS A
, ORDER_ITEMS B
, CUSTOMERS C
WHERE 1=1
AND A.STATUS = 'Shipped'
AND A.ORDER_ID = B.ORDER_ID
AND A.CUSTOMER_ID = C.CUSTOMER_ID
GROUP BY C.NAME, TO_CHAR(A.ORDER_DATE, 'YYYY') ORDER BY C.NAME
;

CREATE OR REPLACE VIEW 로 VIEW를 생성할 수있다.

1
2
3
4
5
6
7
8
9
10
11
CREATE OR REPLACE VIEW CUSTOMER_SALES AS SELECT
C.NAME AS CUSTOMER
, TO_CHAR(A.ORDER_DATE, 'YYYY') AS YEAR
, SUM( B.QUANTITY * B.UNIT_PRICE ) SALES_AMOUNT
FROM ORDERS A
, ORDER_ITEMS B , CUSTOMERS C
WHERE 1=1
AND A.STATUS = 'Shipped'
AND A.ORDER_ID = B.ORDER_ID
AND A.CUSTOMER_ID = C.CUSTOMER_ID
GROUP BY C.NAME, TO_CHAR(A.ORDER_DATE, 'YYYY') ORDER BY C.NAME;

다음 부터는 CUSTOMER_SALES 로 VIEW 테이블을 조회할 수 있다.

1
2
3
4
5
6
SELECT
CUSTOMER
, SALES_AMOUNT
FROM CUSTOMER_SALES
WHERE YEAR = 2017
ORDER BY SALES_AMOUNT DESC;

서브 쿼리

서브쿼리 기본

select 절에 또다른 select절이 있어서 쿼리 안에 또다른 쿼리가 있으면 서브 쿼리라고 한다.

1
2
3
4
5
6
7
8
SELECT
PRODUCT_ID , PRODUCT_NAME , LIST_PRICE
FROM PRODUCTS
WHERE LIST_PRICE = (
SELECT
MAX(LIST_PRICE)
FROM PRODUCTS
);

스칼라 서브 쿼리

select 중간에 쿼리가 있으면 스칼라 서브 쿼리라고 한다.

1
2
3
4
5
6
7
8
9
10
SELECT
A.PRODUCT_NAME
, A.LIST_PRICE
, ROUND( (SELECT AVG(K.LIST_PRICE)
FROM PRODUCTS K
WHERE K.CATEGORY_ID = A.CATEGORY_ID
), 2
) AVG_LIST_PRICE
FROM PRODUCTS A
ORDER BY A.PRODUCT_NAME;

인라인 뷰 서브 쿼리

앞서서 뷰를 보았는데 인라인 뷰도 일종의 서브 쿼리이다. 그래서 이를 인라인 뷰 서브쿼리라고 한다.

1
2
3
4
5
6
7
8
9
10
11
SELECT ORDER_ID
, ORDER_VALUE
FROM
(
SELECT ORDER_ID
, SUM( QUANTITY * UNIT_PRICE ) ORDER_VALUE
FROM ORDER_ITEMS
GROUP BY ORDER_ID
ORDER BY ORDER_VALUE DESC
)
WHERE ROWNUM <= 10;

트랜잭션

트랜잭션이란?

트랜잭션은 데이터베이스 시스템에서 상호작용 단위로서 어떤 기능에 한 단위라고 생가하면 된다. 예를 들어서 송금 서비스의 입근 요청부터 확인까지의 단위이나, 예매 시스템에서 좌석을 확인하고 예매까지 하는 한 뒨위라고 할 수 있다.

트랜잭션의 4대 특징(ACID)

  1. 원자성(Atomicty) : 데이터 조작이 전부 성공 혹은 전부 실패할지 보증하는 구조
  2. 일관성(Consistency) : 데이터 조작 전후에 일관성 유지, 기존의 데이터베이스가 Correct State 라면 트렌잭션을 수행하고 난 후에도 Correct State 여야 한다. -> 도메인 유효봄위, 무결성 제약조건 등의 제약조건을 위배하지 않는 정상적인 상태, 예를들어 시스템에 사용자 등록 시 등록본호에 유일성 제약을 설정하는것과, 잔액이 있어야 송금 할 수 잇다는 일관성을 지키는 것등이 있다.
  3. 고립성(Isolation) : 복수 사용자가 동시에 데이터 조작을 실행한 경우 각각의 처리가 모순 없이 실행되는 것을 보증, 트랜잭션 격리 수준
  4. 지속성(Durability) : 데이터 조작 완료 후 완료 통지 받는 시점에서 결과를 잃지 않는것, 즉 트랜잭션이 Commit 되고 나면 데이터 변경 사항이 영구적으로 확장되도록 보장하는 것

트랜잭션 처리의 필요성

원자성의 중요성

트랜잭션은 전부 성공하거나 혹은 전부 실패 해야 한다. 부분 성공이 있으면 안된다. 예를 들어서 어떤 얘매 시스템에서 좌석을 선택하고 결제단계에서 취소했다면 부분적 성공인 좌석 선택까지를 그대로 성공으로 두는 것이 아니라 모두 실패로 좌석 선택까지 실패시키는 것이다. 이렇게 해야 다른 사람이 좌석을 선택할 수 잇다.

고립성의 중요성

복수의 사용자가 트랜잭션의 모순이 없어야 한다.

격리 수준

  • Read Uncommitted : Commit 되지 않아도 읽기 기능, 동시성은 좋지만, 일관성은 매우 떨어짐.
  • Read Committed : Commit 된 내용만 읽기, 오라클 기본
  • Repeatable Read : 반복 읽기 중에, 내용이 업데이트 되는 것을 방지
  • Serializable : 직렬화 기능으로서 트랜 잭션 처리 중에 insert 도 금지

밑으로 갈수록 격리 수준은 높아지지만 Serializable 수준까지 가면 DBMS 운영 시 동시성이 크게 덜어지면서 성능 이슈가 발생한다.

격리 수준이에 따른 발생 현상

  • Dirty Read : 커밋되지 않은 결과도 읽어서 좋지 않은 상태
  • Non-Repeatable Read: 트랜잭션 중간에 어데이트를 허용해서 애매하게 읽은 상태
  • Phantom Read : 트랜잭션 중간에 insert를 허용해서 혼란스러운 상태, 유령 읽기

격리 수준과 3가지 현상의 관계

ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE; 다음 명령어로 격리 수준 설정 가능.

락과 데드락

어떤 세션에서 트랜잭션 중에 다른 세션에서 데이터를 수정하지 못하도록 잠그는 기능을 락이라고 한다.

락의 유형

  • 공유락 (Shared Lock) : 데이터 읽기를 할때 공유락이 걸려 있으면 데이터를 볼수는 있지만 업데이트 할 수는 없다. (오라클은 공유락을 설정하지 않고 mvcc 라는 기술을 사용한다.)
  • 배타락 (Exclusive Lock) : 배타락이 걸려 있으면 데이터를 볼수도 없다.

데드락

데드락은 두 개 이상의 트랜잭션이 각각 자신의 데이터에 대하여 락을 흭득하고 상대방 데이터에 대하여 락을 요청 요청하면 무한대기 상태에 빠질 수 있는 현상이다.

예를들어,

  1. 사용자1 이 테이블 A에 대해서 쓰기 작업을 진행하면서 락을 걸고
  2. 사용자2 가 테이블 B에 대하여 쓰기 작업을 하면서 락을 걸었을때
  3. 사용자 1 이 테이블 B 에 대하여 쓰기 작업을 진행하려고 하면 락이 걸려서 대기 하게 된다. 아직 트랜잭션이 끝나지 않은 상태이기 때문에 테이블 A 에 걸려있는 락은 유지된다.
  4. 사용자 2가 이때 테이블 A 에 대해서 쓰기 작업을 하려고 하면 여전히 유지중인 테이블 A에 대한 락 때문에 대기하게 되고 무한대로 대기중인 상태에 빠지게 된다.

트랜잭션 처리 시 주의 사항

데드락을 최소화 시키는 DBMS 전반적 대책

  1. 트랜잭션을 자주 커밋
  2. 정해진 순서로 테이블에 엑세스 하게 함.
  3. 필요 없는 경우에는 읽기 잠금 흭득 사용을 피함
  4. 쿼리에 의한 잠금 범위를 좁히거나 더 작은 것으로 하마
  5. 한 테이블 복수 행을 순서 없이 변경 없이 갱신하면 교착 상태 발생이 쉬움
  6. 테이블 단위 잠금 흭득해 갱신 직력화

자제해야 하는 트랜잭션 처리

  • Auto Commit
  • 긴 트랜잭션
  • 트랜잭션 관련 설정 확인 : 트랜잭션 격리 수준 조정