0%

테이블 설계

테이블 설계의 기초

테이블이란 ? 데이터를 관리 및 저장하는 장소, 효율적인 관리 및 적절한 조작이 매우 종요하고 실생활에서 광범위학게 사용되는 2차원 표와 유사하다.

테이블 설계 규칙

집합을 나누는 방법

집합을 나누는 방법에 따라 한개 혹은 여러개의 테이블이 될 수 있다.

예를 들어서 회원 테이블을 관리하고 있었는데, 새로운 기준이 적용 되어서 회원을 일반 회원과 프리미엄 회원 테이블로 나누는 경우가 이에 해당한다.

데이터 베이스 vs 자바

데이터 베이스는 자바와 비교 될때가 많다.

데이터베이스에서 테이블은 자바에 클래스와 비교되고, 열은 클래스의 속성과 비교된다. 또한, 행은 자바에서 인스턴스와 비교되기도 한다.

기본키

기본키는 특정 집합에서 특정 행을 유일하게 식별할 수 있는 속성의 집합이다. 예를 들면, 학번, 카드발급번호, 주민등록번호 등이 있다.

따라서 중복된 식별을 없에기 위해서 기본키는 중복 되면 안된다. 또한, 한번 정해지면 가급적 변경하지 않도록 한다.

기본키는 Null 값이 허용 되지 않는데, 업무 상의 이유로 기본키가 없는 테이블이 운영되는 곳이 간혹 있다.

정규형

테이블을 제대로 된 형태로 만들자고 하는 것인데, 테이블을 쪼갤 수 있는 부분까지 쪼개는 것을 의미한다.

제 1 정규형(1NF) 위반

테이블의 셀에 여러 개의 값을 포함하지 않는다.

예를 들어 회원 테이블에서 연락처를 컬럼을 전화번호, 이메일로 같이 관리하는 경우가 1 정규형 위반에 해당한다.

1 정규형 해소를 위해서는 기존에 회원 테이블에서 연락처를 분리해서 회원 연락처 테이블을 만들고 연락처 구분 컬럼을 만들어 핸드폰인지 이메일인지 구분해서 저장해야 한다.

함수 종속성 ?

테이블은 함수오 같다. -> 기본키의 값을 입력하면 특정 출력 값이 나오는 구조이기 때문이다.

예를 들어 환율 테이블에 경우 어떤 통화인지 기본키를 입력하면 환율이 나오는 방식으로 생각할 수 있는다. 어떤 값에 의해 값이 리턴되는 형식에 함수와 같다고 할 수 있다.

제 2 정규형(2NF) 위반

부분 함수 종속성을 허용하지 않음..

부분 함수 종속이란 기본키를 구성하는 열의 일부에만 함수 종속이 존재 하는 것을 의미하는데, 예를들어서 주문 테이블은 고객 아이디와 주문번호를 복합키로 사용해서 주문을 구별한다. 주문 일자에 경우 고객 아이디와 주문번호를 통해서 주문 일자가 나오는게 맞는데 고객명과 고객등급은 복합키가 아닌 고객 아이디만 사용해도 알 수 있다. 이 경우 제 2 정규형 위반이라고 한다.

해소 하기 위해서는 고객 주문 테이블에서 고객명과 고객등급 컬럼을 삭제하고 고객 테이블을 따로 만들어서 부분 함수 종속을 제거해야 한다.

제 3 정규형(3NF) 위반

기본 키를 제외한 일반 컬럼끼리 함수 종속이 발생한 경우에 위반했다고 한다.

예를 들어서 고객 테이블에 경우 고객 아이디를 통해서 고객 명과 나이가 판단되는 것은 맞지만 일반 컬럼인 직업코드와 직업명은 서로간에 함수 종속이 발생했다. 이경우 제 3 정규형 위반이라고 하고 해소 하기 위해서 고객 테이블에서 직업 명 컬럼을 삭제하고 직업테이블을 새로 만들어서 직업 코드 별 직업명을 확인하도록 한다.

4, 5 정규형

일반적으로 거의 쓰이지 않아서 따로 찾아볼것.. 3 정규형 까지는 확실히 알도록 하자.

ER 다이어 그램

데이터 모델링 문야에서 개체-관계 모델이란 구조화된 데이터에 대한 일련이ㅡ 표현이다. 개체-관계 모델리을 하는 것을 ERM(Entity-Relationship Modelling)이라 하고 이에 대한 산출 물을 ERD(Entity-Relationship Diagram) 이라고 한다.

Barker 표기법

ERD를 표시하는 기법 중 하나이고 요즘은 이걸 많이 사용한다.

릴레이션십

엔티티와 엔티티 간의 관계를 의미한다. 엔티티 간의 관계성을 표시한다.

데이터 베이스 성능

성능이란 ?

클라이언트 요청에 대한 응답시간(Response Time) 과 시간당 처리할 수 있는 처리량(Throughtput) 이다.

용어 알아보기

Corrent User = Active User + Inactive User : 현제 사용자를 의미하는데 서버에 부하를 일으키는 Active User와 서버에 부하를 일으키지 않는 Inactive User로 나뉜다.

TPS : 서버가 일정 시간 내의 처리한 트랜잭션의 양

Response Time : 요청 후 응답을 받을 때 까지 소요된 시간

Resource : 한정된 값을 가진 시스템의 구성요소

성능의 특성

경합 부하 구간에서 Response Time이 급격하게 늘어나게 된다.

데이터베이스 병목의 원인

여러 요인이 있지만 DBMS 내부 I/O 병목으로 인한 대기시간 증가가 가장 큰 이유이다. 대부분은 I/O(Sleep) 에서 발생힌다.

병목현상을 해결하기 위해서는

  • 절대적인 블록 I/O 를 줄여야 함.
  • 블록 I/O를 줄이기 위한 모델 설계, DBMS 환경 구축, SQL 튜닝등의 기술이 필요함.

포로세스 생성 주기에 영향을 받는다.

여러 프로세스가 하나의 CPU를 공유 할 수 있지만, 특정 순간에는 하나의 프로세스만 CPU를 사용한다. 디스크에서 데이터를 읽어야할 땐 CPU를 OS에 반환하고 잠시 수면 상태에서 I/O 가 완료되기를 기다린다. 즉, SQL 문에서 발생하는 절대적인 I/O 횟루를 줄이는 것이 성냉 개선의 핵심이다.

성능을 결정하는 요인

성능을 결정하는 가장 큰 요인에는 옵티마이저의 성능이 있다. 사용자가 SQL 문을 입력하면 옵티마이저는 파싱한 SQL를 최적화하여 프로시저로 변환시키는 작업을 한다.

옵티마이저는 통계정보를 이용해 각 실행 계획의 예상 비용을 산정한 후 최저 비용을 나나태는 실행 계획을 선택한다.

옵티마이저가 참조하는 통계정보

SQL 파식 -> 실행계획 작성 ( 통계 정보 활용) -> 실행계획 선택 ( 옵티마이저의 역활) -> SQL 실행

통계정보 :

  • 테이블의 행수
  • 데이터형
  • 크기
  • 제약정보
  • 인덱스에 대한 통계
  • 열 값에 대한 통계 등등..

실행 계획은 어떻게 세워지는

인덱스

지정한 컬럼을 기준으로 메모리 영역에 일종의 목차를 생성하는것, 조회 할때는 좋은 성능을 보이만, insert, update, delete 등의 작업을 해야 할때에는 인덱스 테이블까지 변경해하는 상황이 발생하면서 오히려 성능이 안좋을 수 있다.

``SQL
CREATE INDEX IDX_TB_EXECUTION_TEST_01 ON TB_EXECUTION_TEST(CUSTOMER_ID);

1
2
3
4
5
6
7
8
9

인덱스는 B-Tree를 사용하여 테이블을 조회한다. 반면에, 인데스가 없는 조회 조건은 모든 요소를 비교하면서 조회하게 된다. 이를 Table Full Scan 이라고 한다.

반면에 인덱스를 사용하면 B-Tree를 이용하여 Range Scan 을 하게 된다.

먼저 branch 노드에서 커르고 리픝 노드에서 수평적으로 검사하여 디스크에 접근 하는 것이다.

인덱스를 사용할 컬럼은 카디너리티가 높은 컬럼을 사용해야 한다. 카디널리티는 Distint 명령을 수행했을때 숫자 라고 생각하면 쉽게 생각 할 수 있는데, 성별보다 주민등록 번호가 카디너리티가 높다는 것은 여기에 기인하여 생각할 수 있다.
여러 컬럼으로 인덱스를 사용할때에는 카디너리티가 높은 순에서 낮은 순서로 사용해야 한다.

백업 및 복구

지속성과 성능이 양립하는 구조

로그 선행 기법

WAL(Write-ahead logging)이라고 부르면, 시스템에서 모든 수정은 적용 이전에 로그에 기록된다. 예를 들어서 특정 프고그램이 진행되는 동안 정전이 일어났다고 가정해보자. 다시 시작할 때 프로그램은 어느 작업이 수행을 성공적으로 마쳤는지, 실패했는지 등의 정보를 알고 있어야 한다. 로그 선행 기법을 사용한다면 프로그램은 이러한 로그를 검사하여 예기치 않은 정전 시 해야 할 일과 실제 했던 일을 비교하게 된다.

데이터베이스 버퍼

데이터 파일로의 입력을 데이터 베이스 버퍼를 경유해서 하도록 한다. (성능 양립을 위해서 )

순서 :

  1. 갱신 대상의 데이터 포함한 블록이 버퍼풀에 있는지 확인
  2. 없을 경우 데이터 파일로부터 해당 블록을 읽어 들임.
  3. 버퍼 풀 내의 해당 블록을 갱신 수행
  4. 갱신 내용이 Commit 과 함께 로그에 기록
  5. 갱신 되었지만 데이터 파일에 쓰이지 않은 블록은 Dirty 블록이 됨.
  6. 갱신된 데이터 블록은 나중에 정리되어 데이터 파일에 적용됨(체크 포인트)
  7. 체크포인트 이전 로그 파일은 불필요 하게 됨
  8. 갱신과 더불어 위 순서 반복

Crash 복구 흐름

Crash 가 발생하면

  • WAL : 마지막으로 Commit 된 트랜잭션의 갱신 정보 가짐
  • 데이터베이스 버퍼 : Crash 로 내용이 전부 소실
  • 데이터베이스 파일 : 최후 체크포인트까지의 갱신 정보 가짐.

-> 데이터 베이스 파일을 Crash 전 최신 Commit 상태로 수정함.

백업 및 복구

백업의 3가지 관점

  1. 핫 백업과 콜드 백업
  2. 논리 백업과 물리 백업
  3. 풀 백업과 부분(증분/차등) 백업

핫백업과 콜드 백업

핫 백엄 : 온라인 백엄/ 데이터 베이스 기능 이용, 데이터 베이스를 정지 하지 않고 백업 데이터를 얻음

콜드 백업 : 오프라인 백업 / OS 기능 이용, 서버를 내림고 데이터 베이스도 종료 시키고 OS 명령으로 복사

논리 백업과 물리 백업

논리 백업 : SQL 기반 텍스트 형식으로 백업 데이터 기록

  • 장점 : 이식성 우수, 편집가능, 이기종 간의 DB 이행 유리
  • 단점 : 물리 백업보다 느림, 용량이 큼

물리 백업 : 데이터 영역을 그대로 덤프 하는 이미지로 바이너리 형식 기록

  • 장점 : 최소 크기로 데이터 얻음, 백업 및 복원의 속도가 빠름
  • 단점 : 일부 데이터의 내용 수정은 불가능, 호완성이 좋지 않음.

풀 백업꽈 부분 백업

풀백업(전체 백업) : 데이터 베이스 전체 데이터를 매일 백업

부분 백업 : 풀 백업 이후 갱신된 데이터를 백업

  • 차등 백업
  • 증분 백업

Basic Branching and Merging

실제 개발 과정에 flow는 다음과 같다.

  1. 웹사이트가 있고 뭔가 작업을 진행중이다.
  2. 새로운 이슈를 처리할 branch를 하나를 생성한다.
  3. 새로 만든 branch에서 작업을 진행한다.

이때 즁오한 문제가 생겨서 처리해야 할 일이 생긴다. 그러면 아래와 같이 진행한다.

  1. 새로운 이슈를 처리하기 위해 운영 브랜치로 이동한다.
  2. Hotfix 브랜치를 새로 하나 생성한다.
  3. Hotfix 에서 문제를 수정하고 테스트를 마친 후 운영 브랜치로 머지한다.
  4. 다시 작업하던 brnahc로 이동하고 마찬가지로 작업 완료, 테스트 진행 후 운영 브랜치로 머지한다.

브랜치의 기초

회사 내의 새로운 53번 이슈를 해결해야 한다면 다음과 같이 브랜치를 만들고 동시에 switch 할 수 도 있다. 옵션 플래그는 -b 이다.

1
2
$ git checkout -b iss53
Switched to a new branch "iss53"

이제 iss53 브렌치에서 작업을 진행하고 커밋한다.

1
2
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

그러면 main 브렌치보다 커밋을 하나 더한 상태가 된다. 이때 어떤 문제가 생겨서 빨리 해결해야 한다고 가정하자. 이를 해결 하기 위해서 우선은 main 브랜치로 이동하고 HotFix 브랜치를 만들어서 기존에 작업하던 iss53에 영향을 안주고 고칠 수 있다. 먼저 main 브렌치로 옮긴다. 아직 커밋하지 않은 checkout 브랜치와 충돌을 발생시킬수도 있는데 우선은 모두 커밋한 후에 main 브랜치로 이동하도록 한다.

1
2
$ git checkout master
Switched to branch 'master'

(main 브랜치는 기본 브랜치라는 의미여서 이름은 master 여도 상관 없다. )

이제 hotfix 브랜치를 생성, 전환하고 작업한후 결과를 커밋한다.

1
2
3
4
5
6
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
1 file changed, 2 insertions(+)

작업이 완료 되었으면 다시 main 브랜치로 이동하고 hotfix 브랜치를 병합한다.

1
2
3
4
5
6
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)

hotfix 브랜치는 master 브랜치에서 단순히 이동만 시킨 상태가 되기 때문에 이를 fast=-foward라고 한다.

이제 hotfix 브랜치는 필요 없어졌기 때문에 삭제 하도록 한다. 플래그는 -d 이다.

1
2
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

그리고 다시 iss53 브랜치로 이동한 후 작업을 환료하도록 한다.

1
2
3
4
5
6
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

작업을 완료하면 main 브랜치로 전환 한 후에 iss53번 브랜치와 병합한다. 그럼 hotfix 내용이 포함된 main 브랜치에 새로운 iss도 처리한 브랜치로 병합할 수 있다.

Merge 의 기초

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)

이번에 머지를 진행하면 hotfix 머지했때랑 다른 메세지를 보여준다. 이는 현제 브랜치가 가리키는 커밋이 머지할 브랜치의 조상이 아니므로 fast-foward로 병합하지 못하기 때문이다. 이 경우에 git은 이 둘의 공통 조상이 가리키는 스냅샷과 각각의 스냅샨을 사용한 3-way Merge 실행한다. 각각의 스냅샷을 비교한 후 새로운 커밋을 만들고 그 커밋의 조상이 각각의 브랜치를 바라보게 하는 것이다.

충돌의 기초

3-way Merge를 진행할때 충돌이 발생하기도 한다. 충돌은 merge 하려는 브랜치에서 동일한 작업한 브랜치와 동일한 파일에 내용이 달라서 발생한다. 예를 들어 앞서서 hotfix에서 index.html에 대한 작업을 진행했고, iss53에서도 마찬가지로 index.html에 대한 작업을 진행했으면 아래와 같이 충돌이 발생한다.

1
2
3
4
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

git status 명령을 입력하여 어디가 충돌이 발생했는지 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified: index.html

no changes added to commit (use "git add" and/or "git commit -a")

Unmerged path: 에 있는 파일을 확인하면 된다.

1
2
3
4
5
6
7
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

해당 파일을 열어보면 다음과 같은 표시를 해준다. HEAD 브랜치는 main 브랜치이고 ===== 다음에 내용은 iss53에 내요이다. 이럴때는 HEAD에 내용을 선택할지 iss53에 내용을 선택할지, 아니면 다 섞을지, 내용을 삭제할지 , 새로운 내용을 만들지 정한후 수정하면 된다.

밑에는 새로운 다 지우고 새로운 내용을 추가한 예제이다.

1
2
3
<div id="footer">
please contact us at email.support@github.com
</div>

충돌을 해결하고 나서 해당 파일을 커밋하면 merge 에 대한 내용을 보여주고 merge된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Merge branch 'iss53'

Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#

Branching management

Branch Management

git branch 를 인수 없이 실행하면 생성된 branch에 목록을 확인 할 수 있다.

1
2
3
4
$ git branch
iss53
* master
testing

prifix 인 * 는 현제 HEAD 가 가리키고 있는 브랜치를 뜻한다. 만약 지금 상태에서 작업을 진행하고 커밋하면 master 브랜치에 커밋 내역이 담기게 된다.

git branch -v 를 입력하면 각 브랜치 마다 마지막 커밋을 볼 수 있다.

1
2
3
4
$ git branch -v
iss53 93b412c Fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 Add scott to the author list in the readme

--merged--no-merged 옵션을 사용하면 현제 브랜치에 병합된 브랜치나 병합되지 않은 브랜치를 확인할 수 있다. 현제 master 브랜치에서 병합된 브랜치에 목록을 보려면 다음과 같이 하면 된다.

1
2
3
$ git branch --merged
iss53
* master

일반적으로 이때 * 가 안붙은 브랜치는 삭제해도 무관한다. 이미 작업 내역에 통합했기 때문이다.

--no-merged 옵션을 사용함녀 통합되지 않은 브랜치의 목록도 확인할 수 있다.

1
2
$ git branch --no-merged
testing

이때 나오는 브랜치는 아직 통합하지 않았기 때문에 -d 옵션으로 삭제할 수 없다. 만약 강제로 삭제하고 싶다면 -D 옵션을 주어야 한다.

--no-merged-merged에 옵션을 달았을때 아무런 인수를 주지 않으면 현제 브랜치에 대해서 조사합니다. 따라서 특정한 브랜치에 대해서 조사하고 싶다면 인수로 브랜치 이름을 주어야 합니다.

Changing a branch name

주의 : 다른 사람이 작업중인 브랜치의 이름을 변경하지 마세요.

브랜치 이름을 잘못 지어서 커밋 내역을 유지한 채로 좋은 이름으로 바꾸려면 어떻게 해야 할까? 또한, 리모트 저장소에도 바꾸려면 어떻게 해야 할까?

로컬에서는 다음과 같은 명령어로 할 수 있다.

1
$ git branch --move bad-branch-name correct-branch-name

원격 저장소에도 변경 내용을 추가하고 싶다면 다음과 같이 입력한다.

1
$ git push --set-stream origin correct-branch-name;

다음으로 원격 저장소에 bad-branch-name 브랜치를 삭제해준다.

1
$ git push origin --delete bad-branch-name

Changing the master branch name

경고 : 메인 브랜치 이름을 변경하는 통합 서비스, 도우미 유틸리티, 빌드/릴리스 스크립트가 중단 될 수도 있다. 변경하기전에 반드시 공동작업자와 상의하길 바란다. 만약 변경했다면 철저히 분석해서 스크립트에서 이전 분기 이름에 대한 참조를 변경해야 한다.

로컬에서 이름을 변경하려면 다음과 같이 해야 한다.

1
$ git branch --move master main

원격 저장소에도 적용하려면 다음과 같이 해야 한다.

1
$ git push --set-upstream origin main

하지만 아직 원격 저장소에는 master 브랜치가 남아있고 다른 작업 자들은 기본 브랜치로 master 브랜치를 사용해서 작업 내용을 통합할 것이다. 그렇기 때문에 몇가지 추가 사항이 필요하다.

  • 이에 의존하는 모든 프로젝트는 코드 및/또는 구성을 업데이트해야 합니다.
  • 테스트 실행기 구성 파일을 업데이트합니다.
  • 빌드 및 릴리스 스크립트를 조정합니다.
  • 리포지토리의 기본 분기, 병합 규칙 및 분기 이름과 일치하는 기타 항목에 대한 리포지토리 호스트의 설정을 리디렉션합니다.
  • 문서에서 이전 분기에 대한 참조를 업데이트합니다.
  • 이전 분기를 대상으로 하는 모든 pull 요청을 닫거나 병합합니다.

모든 작업을 완료 한 후에 원격 저장소에 master 브랜치를 삭제하도록 한다.

Branching Workflows

이번에는 브랜치를 이용한 워크 플로우에 대해서 알아본다.

Long-Running Branches

깃은 장기적으로 브랜치를 열어두고 새로운 브랜치에서 병합하는것이 굉장히 쉬운 편이다. 이를 활용해서 git에서 많이 사용하는 개발 플로우가 있다.

master 브랜치는 배포했거나 배포할 코드로서 안정적인 버전을 유지한다.

develop 브랜치는 개발을 진행하고 있는 브랜치로 안정화가 진행되면 master 브랜치로 병합된다. 또한 기능 개발시 topic 브랜치를 만들어서 개발, 테스트 후 devlop 브랜치로 머지하고 devlop 브랜치에서 안정화 작업을 거친 후 master 브랜치로 머지하는 방법을 사용한다.

각 브랜치를 하나의 실험실로 생각한다.

Topic Branches

토픽 브랜치는 어떤 한 가지 주제나 작업을 위해 만든 짧은 호흡에 브랜치이다. git 에서는 브랜치를 만드는 것이 매우 쉬우므로 이렇게 브랜치를 생성 하고 개발하고 머지한다음 삭제한다.

보통 주제별로 브랜치를 만들고 각각은 독립돼 있기 때문에 매우 쉽게 컨텍스트 사이를 옮겨 다닐 수 있다. 묶음별로 나눠서 일하면 내용별로 검토하기에도, 테스트하기에도 더 편하다. 각 작업을 하루든 한 달이든 유지하다가 master 브랜치에 Merge 할 시점이 되면 순서에 관계없이 그때 Merge 하면 된다.

Components and Props

컴포넌트를 사용하면 UI를 재사용 가능한 개별적인 여러 조각으로 나누고, 개별적으로 살펴 볼 수 있다.

개념적으로 Components 는 JavaScript에 함수 와 같다. props라고 불리는 임의의 입력을 받고 화면에 표시하고 싶은 정보를 담은 React elements를 반환한다.

Function and Class Components

컴포넌트를 표현하는 두가지 방법이 있다.

첫번째로, Javascript에 함수 작성방법과 같은 function components가 이다. 이 함수는 props라는 객체 인자를 받고 React elements를 반환한다.

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

다음으로 class를 사용하는 class components 가 있다.

1
2
3
4
5
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

실습 코드

1
2
3
4
5
6
7
8
9
function WelcomeFunction(props) {
return <h1>Hello, {props.name}!</h1>;
}

class WelcomClass extends React.Component {
render() {
<h1>Hello, {this.props.name}</h1>;
}
}

리엑트에 관점에서 보면 두 컴포넌트는 모두 동일하다 몇가지 추가적인 기능이 있는데 이는 다음 장에서 설명한다.

Rendering a Component

지금 까지는 DOM 테그 이름을 통해서 React element를 렌더링 했는데 유저가 만든 컴포넌트 이름으로 나타낼 수 도 있다.

1
const elemnt = <Welcome name="Sara" />;

리엑트는 유저가 만든 컴포넌트를 발견하면 jsx 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달한다. 이를 props라고 한다.

1
2
3
4
5
6
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(element, document.getElementById("root"));

실습 코드

1
2
3
4
5
6
7
function WelcomeFunction(props) {
return <h1>Hello, {props.name}!</h1>;
}

function App() {
return <WelcomeFunction name="Jayoon" />;
}

컴포넌트의 이름은 항상 대문자로 시작하도록 작성한다. 리엑트는 소문자로 시작하는 테그를 DOM 태그로 보기 때문이다.

Composing Components

컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있다. 이는 모든 단계에서 추상 컴포넌트를 사용할수 있음을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}

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

실습 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
function WelcomeFunction(props) {
return <h1>Hello, {props.name}!</h1>;
}

function App() {
return (
<div>
<WelcomeFunction name="Jayoon" />
<WelcomeFunction name="daHee" />
<WelcomeFunction name="ok" />
</div>
);
}

Extracting Components

컴포넌트를 여러개의 작은 컴포넌트로 나누느 것을 두려워 하지 마라.

Comment 예제를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img
className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">{props.author.name}</div>
</div>
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
);
}

이 예제는 props 안에 author, text, date를 갖는데, 중첩된 구조로 되어 있어서 변경하기도 어렵고 재상하기도 어렵게 된다. 컴포넌트를 추출해 보도록 한다.

먼저 Avatar 를 추출한다:

1
2
3
4
5
function Avatar(props) {
return (
<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
);
}

Avatar는 자신이 Comment 내에서 렌더링된다는 것을 알 필요가 없다. 따라서 autor 라는 속성 이름 대신에 user라는 일반적인 이름을 사용하기로 한다.

props에 이름은 context가 아닌 컴포넌트 자체의 관점에서 짓는 것을 권장한다.

Comment 를 당므과 같이 단순화 할 수 있다:

1
2
3
4
5
6
7
8
9
10
11
12
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">{props.author.name}</div>
</div>
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
);
}

다음으로, Avatar 옆에 사용자의 이름을 렌더링하는 UserInfo 컴포넌트를 추출한다:

1
2
3
4
5
6
7
8
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">{props.user.name}</div>
</div>
);
}

Comment 가 더욱 단순해 진다:

1
2
3
4
5
6
7
8
9
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
);
}

실습 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const comment = {
date: new Date(),
text: "I hope you enjoy learning React!",
author: {
name: "Hello Kitty",
avatarUrl: "https://placekitten.com/g/64/64",
},
};

function formatDate(date) {
return date.toLocaleDateString();
}

function Avatar(props) {
return (
<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
);
}

function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">{props.user.name}</div>
</div>
);
}

function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
);
}

function App() {
return (
<Comment author={comment.author} text={comment.text} date={comment.date} />
);
}

이런 작업이 지루해 보일수 있으나 큰 프로젝트를 할때 도움이 된다. UI가 여려번 반복되거나 UI 일부가 자체적으로 복잡한 경우에는 별도의 컴포넌트로 만드는 것이 좋다.

Props are Read-Only

함수 컴포넌트나 클래스 컴포넌트 모두 props를 수정하면 안된다.

함수는 pure function과 impure function이 있다. pure function은 입력값을 수정하려 고 하지 않아서 동일한 입력에 대해서 동일한 출력을 반환하는 반면에, impure function은 입력값을 변경한다.

pure function:

1
2
3
function sum(a, b) {
return a + b;
}

impure function:

1
2
3
function withdraw(account, amount) {
account.total -= amount;
}

react는 매우 유연하지만 한가지 엄격한 규칙이 있는데 모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.

애플리케이션의 UI는 동적으로 변화하기 때문에 props를 변화 시키지 않고 순수함수로 동작시킬수 없지 않을까 싶지만, React 는 state를 사용해 이 문제를 해결한다. state는 다음 장에서 설명하도록 한다.

Rendering Elemnts

리엑트 앱의 가장 작은 단위를 의미한다. React dom 은 dom 과 react elemnts 가 일치하도록 업데이트 한다.

컴포넌트와 혼동해서 사용하는 경우가 있다. 엘리먼트는 컴포넌트에 구성요소라고 생각하면 된다.

Rendering an Element into the DOM

HTML 파일에 밑에 div가 있다고 가정.

1
<div id="root"></div>

이제부터 이것을 root DOM node라고 부를 것이다. 루트 돔 노드안에 들어갈 내용을 React DOM을 통해 관리하게 된다.

만약 어떤 앱을 통합하려는 경우에는 여러개의 루트 노드가 있을 수 있다. React elemnt를 root DOM node 에 렌더링 하려면 ReactDOM.render() 에 인수로 element와 root node를 넣고 호출하면 된다.

공식 문서 코드

1
2
const elment = <h1>Hello, World</h1>;
ReactDOM.render(elemnt, document.getElementById("root"));

실습 코드

1
2
3
4
ReactDOM.render(
<React.StrictMode>{elements}</React.StrictMode>,
document.getElementById("root")
);

Updating the Rendered Element

React elements 는 불변 객체이다. 한번 생성하고나면 자식 요소나 속성을 변경할 수 없다. 마치 영화에 프레임과 같아서 특정 시점에 UI를 그리는 역활을 한다.

지금까지의 지식 수준에서는 UI를 업데이틑 하는 방법은 새로운 elemnts를 생성하고 ReactDOM.render()에 인수로 전달하는 방법밖에 없다.

시계 예제

1
2
3
4
5
6
7
8
9
10
11
12
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);

ReactDOM.render(element, document.getElementById("root"));
}

setInterval(tick, 1000);

실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function tick() {
const element = (
<div>
<h1>Hello, World</h1>
<h2>{new Date().toLocaleTimeString()}</h2>
</div>
);

ReactDOM.render(
<React.StrictMode>{element}</React.StrictMode>,
document.getElementById("root")
);
}

setInterval(tick, 1000);

실제로는, ReactDOM.render()는 한번만 실행된다. 다음에 이어지는 주제에서 stateful components에 대해서 배울것이다. 지금 배우는것도 다음 배울 내용과 연관 되어 있으니 쭉 보길 바란다.

React Only Updates What’s Necessary

개발자 도구를 통해 살펴보면 실제로는 전체 UI를 새로 생성하도록 구성하였지만, React DOM은 DOM을 그 자식 엘리먼트를 이전의 엘리먼트와 비교하고 DOM을 원하는 상태로 만드는 데 필요한 경우에만 DOM을 업테이트 한다.

따라서 전체 UI가 업데이트 되는 것이 아니라 시계부분만 업데이트 되는 것이다. 이런 방식으로 접근하는것이 나중에 버그를 없애는데 도움을 줄 것이다.

State and Lifecycle

이번 섹션에는 저번 섹션에서 만들었던 Clock 컴포넌트를 완전히 재사용 가능하고 캡슐화 하는 방법을 배운다. Clock 컴포넌트는 스스로 타이머를 설정할 것이고 업테이트 할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}

function tick() {
ReactDOM.render(<Clock date={new Date()} />, document.getElementById("root"));
}

setInterval(tick, 1000);

위 코드는 며번 새로 컴포넌트를 생성하고 렌더링하고 있다. 원하는 동작은 Clock 컴포넌트가 스스로 state를 가지고 state에 변경에 따라서 스스로를 업데이트 하게 하는 것이다.

state라는 개념이 처음 사용되었는데, state는 props와 유사하지만, 변경 가능하고 컴포넌에 의해 완전히 제어되는 값이라고 생각하면 된다.

Converting a Function to a Class

function ComponentClass Component 로 변경하기 위해서는 다음에 단계를 거치면 된다.

  1. React.Component를 상속받는 클래스 생성
  2. render()라고 불리는 메서드 생성
  3. 함수형 컴포넌트에 내용을 render()안으로 옮기기
  4. propsthis.props로 변경하기
  5. 함수 선언부분 삭제
1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

Clock은 이제 클래스로 정의된다.

render 메서드는 UI 업데이트가 필요할때마다 호출되는데 이때는 하나의 Clock 인스턴스로 부터 실행된다. 이것을 사용하면 state와 생명주기 메서드와 같은 부가적인 기능을 사용할수 있게 된다.

Adding Local State to a Class

Props를 state로 옮길 것이다.

  1. this.props.datethis.state.date로 변경
  2. 생성자 함수를 생성하고 this.state를 초기화 하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date() };
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

클래스 컴포넌트는 항상 props를 부모 생성자에 매겨변수로 넣고 호출해야 한다. -> this가 바인딩 되어야 함.

  1. <Clock /> 에서 date prop 을 삭제하기

다음으로, Clock이 스스로 업데이트 할 수 있도록 만들겠다.

Adding Lifecycle Methods to a Class

컴포넌트가 삭제 될때 컴포넌트가 사용하던 리소스를 확보하는것이 중요하다.

먼저, Clock이 처음 DOM에 렌더링 될 때마다 타이머를 설정하려고 하는데 이것을 React에서는 mounting이라고 한다.

또한, Clock의 의해 생성된 DOM 이 삭제될때마다 타이머를 해제하려고 하는데 React에서는 이것을 unmounting 이라고 한다.

컴포넌트 클래스에서 특별한 메서드를 선언하여 컴포넌트가 마운트 되거나 언마운트 될때 일부 코들를 작성 시킬 수 있다.

  • componentDidMount()
  • componentWillUnmount()

이런 메서드를 ‘lifecycle methods`라고 한다.

componentDidMount 는 컴포넌트가 DOM에 렌더링 된후 실행 된다. 따라서 타이머를 설정하기에 가장 좋다.

componentWillUpmount 에서 타이머에 대한 리소스를 해제하면 된다.

다음으로 tick()이라는 메서드를 구현하면 되는데 update를 위해서는 this.setState()를 사용한다.

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
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date() };
}

componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
date: new Date(),
});
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

ReactDOM.render(<Clock />, document.getElementById("root"));
  1. <Clock />ReactDOM.render()로 전달되었을 때 리액트는 Clock에 constructor를 호출하고 this.state 를 초기화 한다.
  2. React는 Clock 컴포넌트의 render() 메서드를 호출한다. 이제 리엑트는 화면에 표시할 내용을 알게 되고 렌더링 출력값을 일치시키기 위해 DOM을 업데이트 한다.
  3. 출력값이 DOM 에 삽입되면 React는 componentDidMount() 생명주기 메서드를 호출한다. 여기서 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.
  4. tick 안에서 this.setState가 호출되고 React는 변경 사항이 발생했다는 것을 알고 render() 메서드를 다시 호출한다. 그리고 DOM을 업데이트 한다.
  5. Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount()를 호출한다.

Using State Correctly

setState()에 대해서 알아야 할 세 가지가 있다.

1. Do Not Modify State Directly

1
2
// wrong
this.state.comment = "Heelo";

위에 코드는 컴포넌트를 다시 렌더링하지 않는다.

1
this.setState({ comment: "Hello" });

this.state 를 설정할 수 있는 유일한 공간은 contructor 뿐이다.

2. State 업데이트는 비동기적일 수 있다.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

this.propsthis.state 가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안된다.

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
1
2
3
4
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment,
}));

3. State Updates are Merged

setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합한다.

state는 독립적인 변수를 포함할 수 있는데 별도의 setState() 호출로 이러한 변수를 독립적으로 업데이트할 수 있다.

1
2
3
4
5
6
7
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});

fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}

The Data Flows Down

컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다.

1
<FormattedDate date={this.state.date} />

FormattedDate 는 date 를 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지 수동을 입력한 것인지 알지 못한다.

일반적으로 이를 top-down 또는 undirectional이라고 부른다. 모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 아래 있는 컴포넌트에만 영향을 미칩니다.

트리 구조에서 부가적으로 나누어 진다고 볼 수 있고 이렇게 나누어진 컴포넌트는 완전히 독립적이다.

Conditional Rendering

React 에서는 JavaScript에서 조건문을 사용하듯이 컴포넌트를 조겅부로 렌더링할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}

ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById("root")
);

논리 && 연산자로 if를 인라인으로 표현하기

JavaScript에서 true && expression 은 항상 expression으로 평가되고 false&& expression은 항상 false로 평가된다.

따라서 && 뒤에 조건이 true일때 출력되고 false일때 React는 무시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 && (
<h2>You have {unreadMessages.length} unread messages.</h2>
)}
</div>
);
}

const messages = ["React", "Re: React", "Re:Re: React"];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById("root")
);

삼항 연산자로 if else 표현하기

1
2
3
4
5
6
7
8
9
10
11
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}

삼항 연산자를 사용하면 if else로 조건부 렌더링을 할 수 있다.

컴포넌트가 렌더링하는 것을 막기

렌더링 하는 대신에 null을 반환하면 렌더링하는 것을 막을 수 있다.

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
function WarningBanner(props) {
if (!props.warn) {
return null;
}

return <div className="warning">Warning!</div>;
}

class Page extends React.Component {
constructor(props) {
super(props);
this.state = { showWarning: true };
this.handleToggleClick = this.handleToggleClick.bind(this);
}

handleToggleClick() {
this.setState((state) => ({
showWarning: !state.showWarning,
}));
}

render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? "Hide" : "Show"}
</button>
</div>
);
}
}

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

컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않는다. 그 예로 componentDidUpdate는 계속해서 호출되게 된다.