36 | );
37 | };
38 |
39 | export default Image;
40 |
--------------------------------------------------------------------------------
/blog/src/types/AllPostEdge.ts:
--------------------------------------------------------------------------------
1 | import Author from "./Author";
2 |
3 | type AllPostEdge = {
4 | node: {
5 | id: string;
6 | parent: {
7 | id: string;
8 | frontmatter: {
9 | author: Author;
10 | };
11 | };
12 | };
13 | };
14 |
15 | export default AllPostEdge;
16 |
--------------------------------------------------------------------------------
/blog/src/types/AllPostNode.ts:
--------------------------------------------------------------------------------
1 | import Tag from "./Tag";
2 |
3 | type AllPostNode = {
4 | id: string;
5 | slug: string;
6 | title: string;
7 | date: string;
8 | excerpt: string;
9 | description: string;
10 | timeToRead: number;
11 | tags: Tag[];
12 | };
13 |
14 | export default AllPostNode;
15 |
--------------------------------------------------------------------------------
/blog/src/types/AllPostResult.ts:
--------------------------------------------------------------------------------
1 | import AllPostEdge from "./AllPostEdge";
2 | import AllPostNode from "./AllPostNode";
3 |
4 | type AllPostResult = {
5 | nodes: AllPostNode[];
6 | edges: AllPostEdge[];
7 | };
8 |
9 | export default AllPostResult;
10 |
--------------------------------------------------------------------------------
/blog/src/types/Author.ts:
--------------------------------------------------------------------------------
1 | type Author = {
2 | id: string;
3 | name: string;
4 | bio: string;
5 | github?: string;
6 | avatar: {
7 | publicURL: string;
8 | };
9 | };
10 |
11 | export default Author;
12 |
--------------------------------------------------------------------------------
/blog/src/types/Post.ts:
--------------------------------------------------------------------------------
1 | import Tag from "./Tag";
2 | import Author from "./Author";
3 |
4 | interface Post {
5 | author: Author;
6 | id: string;
7 | slug: string;
8 | title: string;
9 | date: string;
10 | excerpt: string;
11 | description: string;
12 | timeToRead: number;
13 | tags: Tag[];
14 | }
15 |
16 | export interface PostDetail extends Post {
17 | body: string;
18 | canonicalUrl?: string;
19 | parent?: {
20 | id: string;
21 | frontmatter: {
22 | author: Author;
23 | };
24 | };
25 | }
26 |
27 | export interface PostListItem extends Post { }
28 |
29 | export default Post;
30 |
--------------------------------------------------------------------------------
/blog/src/types/PostResult.ts:
--------------------------------------------------------------------------------
1 | import Tag from "./Tag";
2 | import Author from "./Author";
3 |
4 | type PostResult = {
5 | id: string;
6 | slug: string;
7 | title: string;
8 | date: string;
9 | excerpt: string;
10 | description: string;
11 | timeToRead: number;
12 | tags: Tag[];
13 | body: string;
14 | canonicalUrl: string;
15 | parent: {
16 | id: string;
17 | frontmatter: {
18 | author: Author;
19 | };
20 | };
21 | };
22 |
23 | export default PostResult;
24 |
--------------------------------------------------------------------------------
/blog/src/types/Tag.ts:
--------------------------------------------------------------------------------
1 | type Tag = {
2 | slug: string;
3 | name: string;
4 | };
5 |
6 | export default Tag;
7 |
--------------------------------------------------------------------------------
/blog/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AllPostResult } from "./AllPostResult";
2 | export { default as AllPostEdge } from "./AllPostEdge";
3 | export { default as AllPostNode } from "./AllPostNode";
4 | export { default as PostResult } from "./PostResult";
5 | export { default as Author } from "./Author";
6 | export { default as Tag } from "./Tag";
7 | export { PostListItem, PostDetail } from "./Post";
8 |
--------------------------------------------------------------------------------
/blog/src/utils/getPostFromQuery.ts:
--------------------------------------------------------------------------------
1 | import { PostDetail, PostResult } from "types";
2 |
3 | const getPostFromQuery = (result: PostResult): PostDetail => ({
4 | ...result,
5 | author: result.parent.frontmatter.author,
6 | });
7 |
8 | export default getPostFromQuery;
9 |
--------------------------------------------------------------------------------
/blog/src/utils/getPostsFromQuery.ts:
--------------------------------------------------------------------------------
1 | import { AllPostEdge, AllPostNode } from "types";
2 |
3 | type QueryResult = {
4 | nodes: AllPostNode[];
5 | edges: AllPostEdge[];
6 | };
7 |
8 | const getPostsFromQuery = ({ nodes, edges }: QueryResult) =>
9 | nodes.map(node => {
10 | const postId = node.id;
11 | const mdxPost = edges.find(edge => {
12 | return edge.node.id === postId;
13 | })?.node;
14 | return {
15 | ...node,
16 | author: mdxPost?.parent.frontmatter.author || "",
17 | };
18 | });
19 |
20 | export default getPostsFromQuery;
21 |
--------------------------------------------------------------------------------
/blog/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as getPostFromQuery } from "./getPostFromQuery";
2 | export { default as getPostsFromQuery } from "./getPostsFromQuery";
--------------------------------------------------------------------------------
/blog/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/blog/static/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/android-chrome-384x384.png
--------------------------------------------------------------------------------
/blog/static/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/blog/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/favicon-16x16.png
--------------------------------------------------------------------------------
/blog/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/favicon-32x32.png
--------------------------------------------------------------------------------
/blog/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/favicon.ico
--------------------------------------------------------------------------------
/blog/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/blog/static/favicon.png
--------------------------------------------------------------------------------
/blog/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 |
3 | sitemap: https://mesh.dev/front-end-engineering/sitemap.xml
--------------------------------------------------------------------------------
/blog/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "moduleResolution": "node",
5 | "module": "esnext",
6 | "target": "esnext",
7 | "sourceMap": true,
8 | "strictPropertyInitialization": false,
9 | "strictFunctionTypes": false,
10 | "allowJs": true,
11 | "noEmit": true,
12 | "strict": true,
13 | "esModuleInterop": true,
14 | "jsx": "react",
15 | "typeRoots": ["@types", "node_modules/@types"]
16 | },
17 | "include": ["src/**/*", "@types/**/*"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/coc/index.md:
--------------------------------------------------------------------------------
1 | # Code Of Conduct
2 |
3 | 우리는 구성원 사이의 강력한 신뢰를 바탕으로 촘촘하게 협업하고 높은 기술 전문성을 추구함으로써 회사의 비즈니스 성공에 기여합니다. 이를 위해서 아래의 내용을 적극 실천합니다.
4 |
5 |
6 |
7 | ### 1. 서로 돕고 함께 성장합니다.
8 |
9 | 혼자서는 우리의 비즈니스 목표를 달성할 수가 없습니다. 일이 흘러가는 큰 흐름 안에서 제품을 만드는 모두의 노력이 한 데에 어우러져야만 합니다. 그래서 분업이 아닌, 협업을 합니다.
10 |
11 | 혼자가 아닌 함께 해야 하는 일이기에 내가 아는 것을 동료와 나눌 때, 그래서 함께 성장할 때, 전체의 힘이 더 빠르게 강해질 수 있다고 믿습니다.
12 |
13 | 우리는 배운 것을 동료와 기꺼이 나눠 함께 성장하고, 일의 완성을 향해서 조건과 경계 없이 서로 돕습니다.
14 |
15 |
16 |
17 | ### 2. 일을 기다리지 않습니다.
18 |
19 | 높은 품질의 소프트웨어를 생산적으로 개발하기 위해서 우리는 위계가 아닌 전문가로서의 양심 위에서 일을 합니다. 일이 흘러가는 큰 흐름 안에서 스스로 문제를 정의하고 한 발 앞서 서로를 돕습니다.
20 |
21 | 디자인이 없으면 디자이너를 찾고, 디자이너가 없으면 더미로 구현을 할 수 있습니다. 서버 API가 없으면 인터페이스만 협의하고 페이크 서버를 이용해서 개발을 할 수도 있습니다. 프로세스에 문제가 있다고 생각한다면 망설이지 말고 조직장이나 동료에게 개선을 요청하세요.
22 |
23 | 우리는 일의 완성을 기다리지 않습니다. 우리는 일이 되도록 만듭니다.
--------------------------------------------------------------------------------
/conventions/commit/index.md:
--------------------------------------------------------------------------------
1 | # 1. 형식
2 |
3 | ### 1) 기본 형식
4 |
5 | ```
6 | [commit type]: [commit message] ([jira ticket number?])
7 | ```
8 |
9 | ### 2) commit type
10 |
11 | | 구분자 | 작업 유형 | 예 | 비고 |
12 | | -------- | ------------------------- | --------------------------------------------------------- | ---- |
13 | | feat | 새 기능 구현 | feat: 예치금 대량 충전 검색 기능 추가 (PP-2345) | |
14 | | fix | 버그 수정 | fix: 상점 목록의 에러처리 예외케이스 대응 (PP-2356) | |
15 | | release | 버전 변경 | release: v10.0.0 → v10.1.1 | |
16 | | docs | 문서(또는 주석) 관련 작업 | docs: 데코레이터 옵션에 대한 문서 추가 (PP-2345) | |
17 | | refactor | 리팩터링 | refactor: createStore의 함수를 작은 함수로 분리 (PP-2345) | |
18 | | test | 테스트 관련 작업 | test: 상점 생성 테스트 추가 (PP-2345) | |
19 | | chore | 기타 작업 | chore: 프로덕션 빌드시 소스맵 생성하도록 변경 (PP-2334) | |
20 |
21 | ### 3) commit message
22 |
23 | 이번 커밋에서 작업한 내용을 간결하게 설명합니다.
24 |
25 | ### 4) jira ticket number
26 |
27 | Jira에 등록한 이슈의 번호를 적습니다.
28 | 연관 티켓이 없다면 작성하지 않습니다.
29 |
30 |
31 |
32 | # 2. 작성 규칙
33 |
34 | - 제목은 50자를 넘지 않아야 합니다.
35 | - 본문은 한 줄에 80자를 넘기지 않습니다.
36 | - 문장의 끝에 구두점(.)을 끝에 찍지 않습니다.
37 | - 문장은 명사로 끝나야 합니다.
38 | - 제목과 본문 사이는 한 줄을 개행하여 분리합니다.
39 |
40 |
41 |
42 | # 3. 커밋 작성 예
43 |
44 | ```markdown
45 | feat: 프렌즈 지원하기 버튼에 GA 이벤트 태그 추가 (PP-2345)
46 |
47 | 구글 광고를 지원하기 위해서 GA이벤트 태그가 아닌 구글 애드센스 추적 코드를 삽입합니다.
48 | 또한, 프렌즈 지원하기 버튼에 정의된 이벤트 태그를 보내는 기능을 추가합니다.
49 | ```
50 |
51 |
52 |
53 | # 4. 지원 도구
54 |
55 | 아래의 도구를 이용해 컨벤션 준수 여부를 확인하는 과정을 자동화합니다.
56 |
57 | [commitlint](https://github.com/conventional-changelog/commitlint)
58 |
--------------------------------------------------------------------------------
/daily-meeting/images/daily-log-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/daily-meeting/images/daily-log-screenshot.png
--------------------------------------------------------------------------------
/daily-meeting/index.md:
--------------------------------------------------------------------------------
1 | # 데일리 미팅 가이드
2 |
3 |
4 |
5 | ## 1. 데일리 미팅의 목표
6 |
7 | 데일리 미팅은 매일, 업무 진행 상태를 확인하고 조율하는 자리입니다. 우리가 하는 일은 매우 불확실해서 해보기 전에는 알 수 없는 것들로 가득합니다. 그래서 일을 진행하면서 알게 된 사실을 계속 공유하고 업무를 조정해야 합니다.
8 |
9 | 데일리 미팅은 구성원이 자신의 업무를 공유하고 빨리 문제를 드러내서 동료와 협력하도록 돕는 자리입니다. 프로세스에 매몰되지 않기 위해서 미팅은 가능한 짧게(10분 - 20분 정도) 끝을 냅니다.
10 |
11 |
12 |
13 | ## 2. 데일리 미팅의 함정
14 |
15 | 매일 하는 데일리 미팅은 잘못하면 지루한 요식행위로 느껴지기 쉽습니다. 그래서 구성원의 자발적 참여도와 논의의 집중력을 높일 수 있는 방안을 끊임없이 고민해야 합니다.
16 |
17 | 우리 팀의 데일리 미팅 포맷은 이런 고민 위에 만들어졌고 계속 다듬어 가고 있습니다.
18 |
19 |
20 |
21 | ## 3. 프로세스
22 |
23 | 데일리 미팅은 아래의 단계로 진행을 합니다.
24 |
25 | 1. 출근 직후 업무 계획
26 | 2. 데일리 미팅
27 | - 체크인
28 | - 데일리 로그 리뷰
29 | - 미팅 개설
30 |
31 | ### 1) 출근 직후 업무 계획
32 |
33 | 출근을 하면 바로 위키의 데일리 로그에 오늘 할 업무를 정리합니다. 우리 팀이 사용하는 포맷은 아래와 같습니다.
34 |
35 | 
36 |
37 | #### 할 일
38 |
39 | 오늘 자신이 할 일을 적습니다. 자연스레 하루의 업무를 정리하는 습관을 만들어 줍니다. 업무 관련자를 위키에 태그로 입력을 하면 자동으로 업무 알림을 전달할 수도 있습니다.
40 |
41 | #### 이슈/공유
42 |
43 | 잘 안 풀리는 문제, 공유하고 싶은 내용, 동료와 함께 논의하고 싶은 내용을 짧게 적습니다.
44 |
45 | #### YIL(Yesterday I Learned)
46 |
47 | 어제 일을 하면서 새로 알게 되어 팀에 공유하고 싶은 내용을 적습니다. 가능하면 상세한 내용은 다른 장소에 적고, 데일리 로그에는 링크만 남깁니다.
48 |
49 | ### 2) 데일리 미팅
50 |
51 | #### 진행자 선정
52 |
53 | 간단한 게임을 해서 데일리 미팅을 진행할 진행자를 선정합니다. 진행자는 정해진 시간 안에 데일리 미팅을 효과적으로 할 수 있게 유도해야 합니다.
54 |
55 | 이렇게 하면 데일리 미팅의 지루함을 덜 수 있고, 다양한 구성원이 프로세스 진행을 경험해 볼 수 있습니다. 우리 팀은 진행자를 선정할 때 [복불복 제비뽑기](https://apps.apple.com/kr/app/%EB%B3%B5%EB%B6%88%EB%B3%B5-%EC%A0%9C%EB%B9%84%EB%BD%91%EA%B8%B0/id627332548)를 이용합니다.
56 |
57 | #### 체크인
58 |
59 | 현재 상태를 5점 만점의 별점으로 공유합니다. 이 단계에서 두 가지 효과를 얻을 수 있습니다. 동료의 현재 감정을 읽을 수 있습니다. 어색함을 덜고, 참여하게 만듭니다.
60 |
61 | #### 데일리 로그 리뷰
62 |
63 | 오늘의 데일리 로그를 함께 2분간 정독합니다. "동료가 오늘 할 일 중에서 나와 관계 있는 일"이 있는지 살펴봅니다. 로그를 정독했으면 한 명씩 돌아가면서 아래의 순서로 진행을 합니다.
64 |
65 | 1. 질문 받기 → "저한테 질문할 분이 계신가요?"라고 말하기
66 | 2. 질문 외에 도움이나 논의, 제안도 가능
67 | 3. 모든 논의는 3분 이내로 진행 → 길어지면 별도 미팅 개설
68 | 4. 질문이 끝나면 이슈/공유
69 | 5. 이슈/공유가 끝나면 YIL 공유
70 |
71 | 이 때 질문을 받는 사람이 프로세스 주도권을 갖도록 만들어야 합니다. 그래야 참여도와 집중도를 높일 수 있습니다.
72 |
73 | #### 미팅 개설
74 |
75 | 논의를 하는 데에 5분 이상 소요될 것 같은 내용은 데일리 미팅에서 다루지 않습니다. 논의할 주제별로 미팅 개설 담당자와 참여자를 할당하여, 데일리 미팅 후에 논의를 진행할 수 있게 돕습니다.
76 |
--------------------------------------------------------------------------------
/design/index.md:
--------------------------------------------------------------------------------
1 | # 설계 원칙
2 |
3 | 설계를 논의할 때 가장 최우선으로 고려해야 할 기본 원칙을 정의합니다.
4 |
5 |
6 |
7 | ### 1. 가장 단순하게 문제를 해결합니다.
8 |
9 | 개발자의 업이란 한정된 자원을 최대한의 결과로 만드는 일입니다. 모든 조건이 완벽하게 갖춰진 순간, 여유로운 날은 오지 않습니다. 화려하고 멋진, 하지만 쓸모없는 것을 만드는 데에 시간을 낭비하지 않습니다. 가장 쉽고 빠른 방법을 찾아서 적용합니다. 그리고 필요할 때 고칩니다.
10 |
11 | 단, 대충 하지 않습니다. 경험하고 학습하며 균형을 찾습니다.
12 |
13 |
14 |
15 | ### 2. 확신할 수 없는 일은 고려하지 않습니다.
16 |
17 | "만약..."을 가정하면 끝이 없습니다. 시간은 가장 소중한 자원입니다. 낭비를 하지 않으려면 기회비용도 고려를 해야 합니다. 해야 할 일에 집중을 하기 위해서, 하지 않아도 될 일은 과감하게 미루세요.
18 |
19 | 눈앞에 보이는 문제에 집중을 합니다. 퇴로는 확보에 둡시다. 테스트 자동화 같은 거 말이죠.
20 |
21 |
22 |
23 | ### 3. 말하지 말고, 보여주세요.
24 |
25 | 설계 이론은 눈에 보이지 않는 이야기를 늘어놓습니다. 모든 문제를 말끔하게 해결하는 설계 이론은 없습니다. 문제가 놓인 맥락을 살펴야 합니다.
26 |
27 | 설계를 논의할 때는, 1) 눈앞에 보이는 우리의 문제와 2) 실제로 일어날 상황을 이야기해야 합니다.
28 |
29 | 눈에 보이는 코드를 놓고 이야기하세요. 프로토타입을 만들어서 보여주는 것도 좋습니다. 설계를 변경해야 할 때 어떤 작용이 발생하는지 눈으로 보게 해주세요. 그래야 빠르게 의사결정을 할 수 있습니다.
--------------------------------------------------------------------------------
/ofc/README.md:
--------------------------------------------------------------------------------
1 | # OFC(Open Feedback Circle)
2 |
3 | # 1. OFC...?
4 |
5 | 한 달에 한 번 테이블에 모여 앉아서 동료들과 피드백을 주고 받는 시간입니다.
6 | 이 활동을 통해 아래의 효과를 얻기를 기대합니다.
7 |
8 | - 자신의 지난 한 달을 스스로 회고한다.
9 | - 부족한 점을 동료에게 드러냄으로써 상호 신뢰를 쌓는다.
10 | - 함께 성장할 수 있는 기회를 만든다.
11 | - 피드백을 주고 받는 연습을 한다.
12 |
13 |
14 |
15 | # 2. 진행 방법
16 |
17 | **1단계: 자기 회고**
18 |
19 | - 각자 5분 동안 `OFC Logs`를 작성
20 | - 우리 팀은 CoC를 피드백 항목으로 선정
21 | - 스스로 점수(참 잘했어요 / 좋아요 / 분발해 주세요)를 부여
22 |
23 | **2단계: 자기 회고 공유하기**
24 |
25 | - 각자 회고 결과를 공유
26 | - 한 달 동안 잘한 점과 개선해야 할 점 공유
27 | - 이 과정을 진행하는 동안에 다른 동료는 개입할 수 없고 듣기만 가능
28 |
29 | **3단계: 동료 피드백 받기**
30 |
31 | - 자기 회고가 끝나면 동료들은 돌아가면서 피드백을 전달
32 | - 긍정적이고 건설적인 메시지를 전달해야 함
33 | - 동료의 "성장에 도움을 줄 수 있는 조언"을 할 것
34 |
35 | **4단계: 피드백에 대한 피드백**
36 |
37 | - 돌아가면서 차례로 모임을 회고
38 | - 예)이런 이야기가 도움이 되었다, 이런 이야기를 들었을 때 이런 느낌을 받았다 등등.
39 |
40 |
41 |
42 | # 3. OFC Logs
43 |
44 | OFC Logs는 자신을 회고한 내용을 기록하는 공간으로 구글 시트에 개인별로 작성하고 모두에게 공유함.
45 |
46 | | 항목 | 2020.07.01 |
47 | | ----------------------------- | ----------- |
48 | | 1. 서로 돕고 함께 성장합니다. | 분발할게요. |
49 | | 2. 일을 기다리지 않습니다. | 잘 했어요. |
50 |
51 |
52 |
53 | # 4. 참고
54 |
55 | [Open Feedback Circle(OFC)](https://medium.com/@padminipyapali/open-feedback-circle-a69601ea5dfd)
--------------------------------------------------------------------------------
/retrospective/index.md:
--------------------------------------------------------------------------------
1 | # 회고 가이드
2 |
3 |
4 |
5 | ## 1. 이 문서는?
6 |
7 | 회고를 하고 싶지만, 어떻게 진행을 해야할지 잘 모르는 분들을 위해 회고를 소개합니다. 어디까지나 참고용으로 작성했을뿐 반드시 이 절차를 따라야 하는 것은 아닙니다. 목적에 따라 다양한 방식으로 회고를 진행할 수 있습니다.
8 |
9 |
10 |
11 | ## 2. 회고에서 중요한 것
12 |
13 | 회고는 단순히 모여서 이야기를 하는 자리가 아닙니다. 회고는 특정 기간 동안 있었던 일을 돌아보고 더 잘할 수 있는 방법을 찾아서 개선하는 활동입니다.
14 |
15 | 의미있는 회고가 되려면, 회고에서 "액션 플랜"을 도출할 수 있어야 합니다. 실제 개선으로 이어지지 않는 단순히 감정을 토로하는 회고는, 잠깐 동안 감정을 해소하는 자리가 될 수는 있으나 장기적으로는 모두를 지치게 만듭니다.
16 |
17 | 회고 진행자는 회고 과정에서 참가자들이 액션 플랜을 만들 수 있도록 회고를 이끌어야 합니다. 또한 구성원이 회고를 통해 좋은 변화를 만들어가고 있다고 느끼도록 도와줘야 합니다.
18 |
19 | 회고는 상당한 노력과 연습이 필요한 과정임을 잊지 말아주세요!
20 |
21 | ```
22 | 액션 플랜이란!
23 |
24 | 구체적인 실천 계획을 말합니다.
25 | 액션 플랜은 현실에서 실천할 수 있어야 하며 아래의 네 가지를 명시해야 합니다.
26 |
27 | 1. 측정할 수 있는 일
28 | 2. 담당자
29 | 3. 완료 기한
30 | 4. 결과를 평가할 방법
31 | ```
32 |
33 | 참고: [좋은 회고를 가려내는 법](http://egloos.zum.com/agile/v/5829827)
34 |
35 |
36 |
37 | ## 3. 사전 준비하기
38 |
39 | 사전 준비 단계는 사람들이 손 쉽게 회고에 집중하도록 만듭니다. 이런 활동을 할 수 있습니다.
40 |
41 | ### (1) 목표 확인 + 감사 인사
42 |
43 | 지난 스프린트의 목표를 확인하고 격려를 합니다. 특별한 목적이 있는 회고일 때는 회고의 목표를 공유하는 것도 좋습니다.
44 |
45 | ### (2) 체크인
46 |
47 | 돌아가면서 스프린트를 마친 소감을 이야기합니다. 초반에 한 번도 입을 열지 않은 사람은 끝까지 입을 열지 않을 가능성이 높습니다.
48 |
49 | 이 단계는 참여자가 최소한 한 번은 입을 열게 함으로써 회의에 조금 더 적극적으로 참여하게 만듭니다. 자신의 속 마음을 가감 없이 이야기할 수 있게 유도하세요.
50 |
51 | ### (3) 작업 규칙 정하기
52 |
53 | 원활하게 회고를 진행할 수 있게 회고를 하는 동안에 모두가 지켜야 할 규칙을 정합니다. 무리해서 규칙을 만들기 보다는 아무런 규칙도 없는 상태에서 회고를 하면서 문제가 생길 때마다 구성원이 스스로 규칙을 정하게 유도하는 게 좋습니다.
54 |
55 | 규칙은 회고를 도와야 하며, 규칙이 회고를 방해하면 곤란합니다.
56 |
57 | 예) 핸드폰 보지 않기, 비난하지 않기
58 |
59 | ```
60 | 스스로 책임지게 만들기!
61 |
62 | 회고 참여자가 스스로 작업 규칙 준수 여부를 감시하게 만들면 회고에 적극 참여하게 만들 수 있습니다.
63 | 또한 진행자는 회고를 매끄럽게 진행하는 일에만 집중할 수 있습니다.
64 |
65 | 예) 규칙을 어기는 사람이 커피 쏘기
66 | ```
67 |
68 | ### (4) 지난 회고 리뷰
69 |
70 | 지난 회고에서 만든 액션 플랜의 실행 결과를 구성원에게 공유하고 격려합니다. 이 단계에서 모두가 우리가 조금씩 나아지고 있다는 느낌을 받을 수 있게 노력하세요.
71 |
72 |
73 |
74 | ## 4. 자료 모으기
75 |
76 | 회고의 목적에 맞게 적절한 활동을 선택해서 자료를 모읍니다. 아래의 내용은 자료를 모으는 두 가지 활동을 예시로 소개합니다.
77 |
78 | ```
79 | # 시간축 그리기
80 |
81 | 1. 목적
82 |
83 | 비교적 긴 이터레이션, 릴리스, 프로젝트 전반에 대한 회고를 준비할 때 사용합니다.
84 |
85 | 2. 회상하기
86 |
87 | 스프린트 동안에 있었던 일을 회상하며 가능한 많은 사건을 수집합니다.
88 | 회상을 한다는 것은 스프린트 동안에 일어났던 일을 하나의 그림으로 그려내는 일입니다.
89 | 사건은 작업, 의사소통, 다툼, 일정 차질, 장애 등 모든 일을 포함할 수 있습니다.
90 | 아래의 목록에 있는 것처럼 눈에 보이는 자료를 이용하면 좋습니다.
91 |
92 | - 이벤트 타임라인
93 | - 소멸 차트(Burndown Chart)
94 | - 완료한 이슈 목록
95 |
96 | 3. 자료 모으기
97 |
98 | 1) 시간 축을 보드에 그립니다.
99 | 2) 참여자는 기억나는 사건이나 감정을 해당하는 구역에 포스트잇으로 붙입니다.
100 | 3) 포스트잇 한 장에 하나의 생각을 적습니다.
101 | 4) 사회자는 포스트잇을 하나씩 읽으면서 비슷한 유형 끼리 짝을 지어 벽에 붙입니다.
102 | 5) 각 그룹을 하나의 문장으로 정리해서 보드에 적습니다.
103 | ```
104 |
105 | ```
106 | # 5.5.5(Triple Nickels)
107 |
108 | 1. 목적
109 |
110 | 행동이나 개선에 대한 아이디어를 모을 때 이 활동을 하면 좋습니다.
111 |
112 | 2. 회고 전에 할 일!
113 |
114 | 회고 전에 주제를 미리 공유해 주세요. 그래야 회고에 들어오기 전에 자신의 생각을 정리해 볼 수 있습니다.
115 |
116 | 3. 자료 모으기
117 |
118 | 1) 주제를 공표합니다.
119 | 2) 이번 활동의 목표는 최대한 많은 아이디어를 만들어내는 것임을 설명합니다.
120 | 3) 사람들에게 적을 종이와 펜을 참석자에게 나눠 줍니다.
121 | 4) 주제에 대한 아이디어를 5분 동안 적게합니다. 최소한 다섯 개는 적게 해주세요!
122 | 5) 5분마다 한 번씩 종이를 오른쪽으로 돌립니다.
123 | 6) 다른 사람이 적은 아이디어에 의견을 달 수 있습니다.
124 | 7) 모든 단계가 끝나면, 각자 자신의 종이에 적힌 아이디어를 읽습니다.
125 | ```
126 |
127 |
128 |
129 | ## 5. 통찰 이끌어내기
130 |
131 | 이전 단계에서 모은 자료를 보고 오늘 통찰을 이끌어내서 액션 플랜을 만들 단서를 찾습니다. 회고를 할 때 가장 어려운 지점입니다.
132 |
133 | 예를 들어, 자료 모으기 단계에서 아래와 같은 의견이 나왔다고 가정합시다.
134 |
135 | ```
136 | - 무리한 일정으로 야근을 많이 해야했다.
137 | - 추정이 매우 부정확했다.
138 | - 하나의 이슈가 너무 커서 일주일 동안 이슈를 닫지 못했다.
139 | ```
140 |
141 | 위의 세 가지 의견을 관통하는 하나의 문제는, "우리가 추정을 제대로 못하고 있다"라고 볼 수 있습니다. 여기에서 "추정을 더 잘할 수 있는 방법을 찾아서 개선하자."라는 문장을 끄집어낼 수 있어야 합니다. MC는 이 과정을 자연스럽게 이끌어야 합니다.
142 |
143 | 통찰을 이끌어내는 걸 돕는 활동으로는 생선 가시, 다섯 번 질문하기, 브레인스토밍, 점 투표로 우선순위 매기기 등이 있습니다. 우리 팀은 [다섯 번 질문하기](https://brunch.co.kr/@bookfit/3239)를 애용합니다.
144 |
145 |
146 |
147 | ## 6. 무엇을 할지 결정하기
148 |
149 | 자료 모으기에서 찾은 주제를 보드에 적습니다. 투표를 하여 하나의 아이디어를 선정합니다. 선정한 아이디어에 대해서 SMART(↓)한 액션 플랜을 만들 수 있도록 유도하세요. 그리고 담당자를 할당합니다.
150 |
151 | - Specific: 구체적이고
152 | - Measurable: 측정할 수 있어야 하며
153 | - Attainable: 달성 가능하며
154 | - Relevant: 문제를 해결하는 데에 적절하고
155 | - Timely: 시기 또한 적절할 것
156 |
157 |
158 |
159 | ## 7. 회고를 회고하기
160 |
161 | 돌아가면서 회고에 대한 소감을 한마디씩 하면서 회고를 회고합니다. 더 나은 회고를 만드는 데 도움을 줄 통찰을 구할 수 있습니다.
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------
/study/README.md:
--------------------------------------------------------------------------------
1 | # 스터디
2 |
3 | ## [리팩터링 스터디 (2020.04 ~ 2020.09)](./refactoring/index.md)
4 |
5 | 리팩터링 2판(마틴 파울러) 을 중심으로 진행한 스터디 입니다.
6 |
7 | - [리팩터링 기법 카탈로그](./refactoring/index.md#리팩터링-카탈로그)
8 | - [리팩터링 적용 사례](./refactoring/index.md#리팩터링-적용-사례)
9 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/change-function-declaration.md:
--------------------------------------------------------------------------------
1 | # 6.5 함수 선언 바꾸기(Change Function Declaration)
2 |
3 | 함수의 이름이나 파라미터를 바꿔서 함수의 재사용성을 높이거나 캡슐화 수준을 높이는 기법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | public loadPlanCommandResult(transportPlanId: string) {
11 | ...
12 | }
13 | ```
14 |
15 | ### 😍 To be
16 |
17 | ```typescript
18 | public loadPlanExecutionResult(transportPlanId: string) {
19 | ...
20 | // Command방식을 사용하지 않게 되면서 함수의 이름도 바뀌었다
21 | }
22 | ```
23 |
24 | ### 📋 상세
25 |
26 | 개발 생산성을 올리기 위해 함수의 이름과 매개변수를 변경할 필요가 있다. 함수의 이름은 함수의 연결관계를 파악하는 데 중요한 역할을 한다. 연결관계가 명확하게 보이면 프로그램 동작을 파악하고 기능을 추가하는 데 용이하다. 함수의 매개변수도 다른 모듈과의 결합도를 결정지을 수 있으며 재사용성을 결정하는 요소가 된다.
27 |
28 | ### ⚙️ 절차
29 |
30 | 간단한 절차
31 |
32 | 1. 함수 선언부와 함수 호출부를 수정한 뒤 테스트 한다.
33 |
34 | 마이그레이션 절차 - 함수가 복잡한 경우
35 |
36 | 1. 기존 함수 본문을 새로운 함수로 추출한 뒤 테스트 한다.
37 | 2. 기존 함수를 인라인해서 모두 새 함수를 호출하도록 변경한다. 이때 하나씩 변경하면서 테스트를 반복한다.
38 | 3. 변경이 끝나면 기존 함수를 삭제하고 새로운 함수의 이름을 변경한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/change-reference-to-value.md:
--------------------------------------------------------------------------------
1 | # 참조를 값으로 바꾸기 (Change Reference to Value)
2 |
3 | 개체 내의 내부 개체를 저장할 때 참조 내의 값을 수정하는 것이 아닌, 불변의 새로운 값 개체를 만들어 저장하는 것
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | class Delivery {
11 | #address: Address;
12 |
13 | updateDetailAddress(detailAddress) {
14 | this.#address.detailAddress = detailAddress;
15 | }
16 | }
17 | ```
18 |
19 | ### 😍 To be
20 |
21 | ```ts
22 | class Delivery {
23 | #address: Address;
24 |
25 | updateDetailAddress(detailAddress) {
26 | this.#address = new Address({
27 | ...this.#address,
28 | detailAddress: detailAddress,
29 | });
30 | }
31 | }
32 | ```
33 |
34 | ### 📋 상세
35 |
36 | 참조 개체는 내부 개체를 그대로 둔 채 내부 속성 값만 갱신하고, [값 개체](https://martinfowler.com/bliki/ValueObject.html)는 불변의 새로운 개체를 새로 생성해 기존 개체를 통째로 대체한다. 값 개체는 불변성을 염두에 두고 설계하므로, 분산 시스템이나 동시성 시스템에서 특히 유용하다.
37 |
38 | ### ⚙️ 절차
39 |
40 | 1. 후보 클래스가 불변인지, 혹은 불변이 될 수 있는지 확인한다.
41 | 2. 각각의 세터를 하나씩 제거한다. (11.7절)
42 | 3. 이 값 객체의 필드들을 사용하는 동치성(equality) 비교 메서드를 만든다.
43 | - `Foo.prototype.equals(anotherFoo: Foo): boolean` 를 구현하라는 의미.
44 | - 객체를 새로 만들어 저장하기 때문에, 들어있는 값이 같은 값인지 비교하려면 이러한 메서드가 필요하다.
45 | - 자바스크립트는 오버라이드 가능한 동치성 비교 메서드를 제공하지 않는다. 적당히 내부 값을 비교하는 함수를 만들자.
46 |
47 | ## 📝 메모
48 |
49 | ### ⚠️ 주의할 점
50 |
51 | - 값 개체는 불변이므로, 특정 개체를 여러 개체에서 공유하고 값의 변경을 다른 개체에 알려줘야 하는 경우 공유 개체를 값이 아닌 참조로 다뤄야 한다.
52 | - 그러나, observable 패턴을 사용하고, reaction이나 observer 패턴을 잘 사용하는 경우 구현 방식에 따라 이 문제를 피해갈 방법은 많다.
53 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/change-value-to-reference.md:
--------------------------------------------------------------------------------
1 | # 9.5 값을 참조로 바꾸기 (Change Value to Reference)
2 |
3 | 하나의 데이터 구조안에 논리적으로 똑같은 제3의 데이터 구조를 참조하는 레코드가 여러 개 있을 때가 있다. 예를 들어 여러 개의 주문이 있는데, 고객 한 명이 여러 개의 주문을 했다고 가정하자. 고객 데이터를 값으로 다룬다면, 각 주문에 고객 데이터가 복사될 것이다. 반면 고객 데이터를 참조로 다룬다면 각 주문은 하나의 고객 데이터를 바라볼 것이다.
4 |
5 | 똑같은 고객 데이터를 값으로 복제해서 사용해도 성능 문제를 야기할 가능성은 적지만, 고객 데이터를 갱신할 때 문제가 발생한다. 모든 복제본을 찾아서 빠짐없이 갱신해야 하는데, 하나라도 놓치면 데이터의 일관성이 깨져버린다. 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는게 좋다.
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```jsx
12 | class Order {
13 | constructor(data) {
14 | this._number = data.number;
15 | this._customer = new Customer(data.customer);
16 | }
17 |
18 | get customer() {
19 | return this._customer;
20 | }
21 | }
22 | ```
23 |
24 | ### 😍 To be
25 |
26 | ```javascript
27 | /* registerData.js */
28 | let _repositoryData;
29 |
30 | export function initialize() {
31 | _repositoryData = {};
32 | _repositoryData.customers = new Map();
33 | }
34 |
35 | export function registerCustomer(id) {
36 | if (!_repositoryData.customers.has(id))
37 | _repositoryData.customers.set(id, new Customer(id));
38 | return findCustomer(id);
39 | }
40 |
41 | export function findCustomer(id) {
42 | return _repositoryData.customers.get(id);
43 | }
44 | ```
45 |
46 | ```javascript
47 | import { registerCustomer } from "registerData";
48 |
49 | class Order {
50 | constructor(data) {
51 | this._number = data.number;
52 | this._customer = registerCustomer(data.customer); // create or find
53 | }
54 |
55 | get customer() {
56 | return this._customer;
57 | }
58 | }
59 | ```
60 |
61 | ### 📋 상세
62 |
63 | 값을 참조로 바꾸기 위해서는 엔티티 하나당 객체도 단 하나만 존재하게 된다. 그러면 보통 이런 객체들을 한데 모아놓고 클라이언트 코드들의 접근을 관리해주는 일종의 저장소가 필요하다. 각 엔티티를 표현하는 객체를 한 번만 만들고, 객체가 필요한 곳에서는 모두 이 저장소로부터 얻어 쓰는 방식이 된다.
64 |
65 | ### ⚙️ 절차
66 |
67 | 1. 같은 부류에 속하는 객체들을 보관할 저장소를 만든다.
68 | 2. 생성자에서 이 부류의 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다.
69 | 3. 호스트 객체의 생성자들을 수정하여 필요한 객체를 이 저장소에서 찾도록 한다. 하나 수정할 때마다 테스트한다.
70 |
71 | ## 📝 메모
72 |
73 | ### ⚠️ 주의 사항
74 |
75 | - 위의 예시 코드는 생성자 본문이 전역 저장소와 결합된다는 문제가 있다. 이 점이 염려된다면 저장소를 생성자 매개변수로 전달하여 의존성을 주입하도록 하자.
76 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/collapse-hierarchy.md:
--------------------------------------------------------------------------------
1 | # 계층 합치기 (Collapse Hierarchy)
2 |
3 | 따로 분리돼있을 이유가 없는 2개의 클래스를 하나로 합치는 것
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | class WebDeveloper {
11 | public requestReviewFor(aPullRequest: PullRequest) {
12 | ...
13 | }
14 | }
15 | class WebFrontEndTeammate extends WebDeveloper {
16 | public growTogetherBy(someMethods: any) {
17 | ...
18 | }
19 | }
20 | ```
21 |
22 | ### 😍 To be
23 |
24 | ```ts
25 | class WebDeveloper {
26 | public requestReviewFor(aPullRequest: PullRequest) {
27 | ...
28 | }
29 |
30 | public growTogetherBy(someMethods: any) {
31 | ...
32 | }
33 | }
34 | ```
35 |
36 | ### 📋 상세
37 |
38 | 계층구조가 진화하면서 어떤 클래스와 그 부모가 너무 비슷해져서 더는 독립적으로 존재해야 할 이유가 사라졌을 때, 그 둘을 하나로 합친다.
39 |
40 | ### ⚙️ 절차
41 |
42 | 1. 두 클래스 중 제거할 것을 고른다.
43 | - 미래를 생각해서 더 적합한 이름의 클래스를 남긴다.
44 | - 둘 다 이상하면 이름을 새로 짓는다.
45 | 2. [필드 올리기](./pull-up-field.md)와 [메서드 올리기](./pull-up-method.md), 혹은 [필드 내리기](./push-down-field.md)와 [메서드 내리기](./push-down-method.md)를 적용해 모든 요소를 하나의 클래스로 옮긴다.
46 | 3. 제거할 클래스를 참조하던 모든 코드가 남겨질 클래스를 참조하도록 고친다.
47 | 4. 빈 클래스를 제거한다.
48 | 5. 테스트한다.
49 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/consolidate-conditional-expression.md:
--------------------------------------------------------------------------------
1 | # 10.2 조건식 통합하기 (Consolidate Conditional Expression)
2 |
3 | 비교하는 조건이 다르지만 그 결과로 수행되는 동작이 같은 코드가 있는 경우, 조건 검사를 하나로 통합해 (6.1 함수 추출하기를 사용하면 좋다) 하려는 일의 의도를 더 명확히 하는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | if (anEmployee.seniority < 2) return 0;
11 | if (anEmployee.monthsDisabled > 12) return 0;
12 | if (anEmployee.isPartTime) return 0;
13 | ```
14 |
15 | ### 😍 To be
16 |
17 | ```js
18 | if (isNotElligibleForDisability()) return 0;
19 |
20 | function isNotElligibleForDisability() {
21 | return (
22 | anEmployee.seniority < 2 ||
23 | anEmployee.monthsDisabled > 12 ||
24 | anEmployee.isPartTime
25 | );
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 'and' 또는 'or'을 사용하면 여러개의 비교 로직을 하나로 합칠 수 있다.
32 | 이 기법이 중요한 이유는 두가지이다.
33 | 첫번째, 조건들을 통합함으로서 내가 하려는 일이 더욱 명확해진다.
34 | 두번째, 이 작업이 함수 추출하기까지 이어질 가능성이 높기 때문이다. (복잡한 조건식읠 함수로 추출하면 코드의 의도가 훨씬 분명하게 드러나는 경우가 많다. '무엇'을 하는지를 넘어서 '왜' 하는지를 표현함)
35 | 반면, 하나의 검사라고 생각할 수 없는, 진짜로 독립된 검사들이라고 판단되면 이 리팩토링을 해서는 안 된다.
36 |
37 | ### ⚙️ 절차
38 |
39 | 1. 해당 조건식들 모두에 부수효과가 없는지 확인한다.
40 | (부수효과가 있는 조건식들에는 11.1 질의 함수와 변경 함수 분리하기를 먼저 사용한다.)
41 | 2. 조건문 두개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합한다.
42 | (순차적으로 이뤄지는 조건문은 or로 결합하고, 중첩된 조건문은 and로 결합한다.)
43 | 3. 테스트한다.
44 | 4. 조건이 하나만 남을때까지 2~3 과정을 반복한다.
45 | 5. 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다.
46 |
47 | ## 📝 메모
48 |
49 | ### 🧐 As is (&& 사용)
50 |
51 | ```js
52 | if (anEmployee.onVacation) {
53 | if (anEmployee.seiority > 10) return 1;
54 | }
55 | return 0.5;
56 | ```
57 |
58 | ### 😍 To be (&& 사용)
59 |
60 | ```js
61 | if (anEmployee.onVacation && anEmployee.seniority > 10) return 1;
62 | return 0.5;
63 | ```
64 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/decompose-conditional.md:
--------------------------------------------------------------------------------
1 | # 10.1 조건문 분해하기(Decompose Conditional)
2 |
3 | 복잡한 조건부 로직을 부위별로 분해하여 의미를 드러내는 함수 호출로 대체하는 기법.
4 |
5 | 
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```jsx
12 | let charge;
13 |
14 | if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
15 | charge = quantity * plan.summerRate;
16 | else
17 | charge = quantity * plan.regularRate + planRegularSeviceCharge;
18 |
19 | return charge;
20 | ```
21 |
22 | ### 😍 To be
23 |
24 | ```jsx
25 | if (isSummerPlan(plan))
26 | return summerCharge();
27 | else
28 | return regularCharge();
29 | ```
30 |
31 | ### 📋 상세
32 |
33 | 복잡한 조건부 로직은 프로그램을 복잡하게 만드는 원흉임. 다양한 조건에 따른 동작을 하나의 함수에서 모두 처리하는 함수는 길어지기 쉬움. 이런 경우 각 조건절의 코드만 봐서는 왜 이런 처리를 해야하는지 이해하기 어려워짐.
34 |
35 | 예를 들어 아래의 코드는 if 조건의 의미를 단번에 이해하기 어려움. if 조건을 만족할 때 수행하는 로직도 어떤 의미를 담고 있는지 코드를 곱씹어봐야만 이해할 수가 있음.
36 |
37 | ```jsx
38 | if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
39 | charge = quantity * plan.summerRate;
40 | ```
41 |
42 | 조건문 분해하기를 적용한 아래의 코드는, 우리가 훨씬 쉽게 의도를 읽을 수 있음.
43 |
44 | ```jsx
45 | if (isSummerPlan(plan))
46 | return summerCharge();
47 | ```
48 |
49 | ### ⚙️ 절차
50 |
51 | 1. 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.
52 | 2. 테스트한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/encapsulate-collection.md:
--------------------------------------------------------------------------------
1 | # 컬렉션 캡슐화 하기 (Encapsulation Collection)
2 |
3 | 컬렉션의 캡슐화를 통해 클래스가 모르는 사이 컬렉션의 원소가 바뀌는 것을 방지한다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Person {
11 | get courses() { return this._courses; }
12 | set courses(aList) { this._courses = aList;}
13 | }
14 | ```
15 |
16 | ### 😍 To be
17 |
18 | ```js
19 | class Person {
20 | get courses() { return this._course.slice();}
21 | addCourse(aCourse){ ... }
22 | removeCourse(aCourse){ ... }
23 | }
24 | ```
25 |
26 | ### 📋 상세
27 |
28 | 클라이언트 코드가 컬렉션이 속한 클래스가 인지하지 못하는 사이에 컬렉션의 내용을 바꾸는 문제를 방지하기 위해 캡슐화를 시킨다.
29 | 컬렉션을 리턴하는 함수가 있다면 원본이 아닌 복제본을 리턴하도록 한다. 그리고 컬렉션 자체를 바꾸는 세터는 제거하고, 컬렉션애 접근하려면 컬렉션이 속한 클래스의 메서드를 통해서만 가능하도록 한다.(add/remove 메서드 제공)
30 |
31 | ### ⚙️ 절차
32 |
33 | 1. 컬렉션을 캡슐화 하지 않았다면 변수 캡슐화를 진행한다.
34 | 2. 컬렉션에 원소를 추가/제거 하는 함수를 추가한다.
35 | 3. 정적 검사를 수행한다.
36 | 4. 컬렉션을 참조하는 부분을 찾아 컬렉션의 변경자를 호출하는 함수를 새로 추가한 추가/제거 함수로 변경한다.
37 | 5. 컬렉션 게터 함수는 읽기전용 프록시나 복제본을 반환하게 한다.(복제본 반환이 가장 많이 사용되는 방식.)
38 | 6. 테스트한다.
39 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/encapsulate-record.md:
--------------------------------------------------------------------------------
1 | # 7.1 캡슐화하기
2 |
3 | `Record`자료형을 `Class`로 만들어 데이터의 원본을 숨기고 추상화 하는 기법
4 |
5 | ### 🧐 As is
6 |
7 | ```js
8 | organization = { name: '애크미 구스베리', country: 'GB' };
9 | ```
10 |
11 | ### 😍 To be
12 |
13 | ```js
14 | class Organization {
15 | constructor(data) {
16 | this._name = data.name;
17 | this._country = data.country;
18 | }
19 |
20 | get name() {
21 | return this._name;
22 | }
23 | set name(arg) {
24 | this._name = arg;
25 | }
26 | get country() {
27 | return this._country;
28 | }
29 | set country(arg) {
30 | this._country = arg;
31 | }
32 | }
33 | ```
34 |
35 | ### 🗣 설명
36 |
37 | 가변 데이터가 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확하게 구분해서 저장해야 하는 경우에 레코드를 객체로 리팩토링한다.
38 |
39 | 1. 객체를 사용하면 어떻게 저장했는지를 숨긴 채 원하는 값을 메서드로 제공할 수 있다.
40 | 2. 사용자는 원하는 값이 계산된 값인지 원본 값인지 알 필요가 없다.
41 |
42 | ### ⚙️ 절차
43 |
44 | 1. 레코드를 담은 변수를 캡슐호 한다.
45 | 2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
46 | 3. 테스트한다.
47 | 4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
48 | 5. 레코드를 반환하는 예전 함수를 상요하는 코드를 4에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할떄는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀때마다 테스트한다.
49 | 1. 중첩된 구조의 복잡한 레코드라면 먼저 데이터를 갱신하는 클라이언트들을 주의깊게 확인하고 읽기만 하는 부분이라면 읽기전용 객체를 내려주자
50 | 6. 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 모두 제거한다.
51 | 7. 테스트한다.
52 | 8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.
53 |
54 | ## 📝 메모
55 |
56 | - 중첩된 코드를 캡슐화 할때는 데이터를 변경하는 부분에 집중하자. 데이터 구조의 안쪽으로 들어가서 변경하는 코드를 `setter`를 작성하여 뽑아낸다. 캡슐화에서는 값을 수정하는 부분을 명확하게 드러내고 한곳에 모아두는것이 중요하기 때문이다.
57 | - 데이터 읽기를 처리하는 방법은 몇 가지가 있다.
58 | - 읽는 코드를 독립 함수로 추출한다.
59 | - 장점: 데이터 사용 방법을 모두 확인가능
60 | - 단점:읽는 패턴이 다양해지면 작성하는 코드 양이 늘어남
61 | - 실제 데이터를 반환한다.
62 | - 장점: 간단함.
63 | - 단점: 클라이언트가 실제 데이터를 바꿀 수가 있다.
64 | - 클라이언트가 반환된 데이터를 변경하지 못하게 하려면 두가지 방법이 있다.
65 | - 깊은 복사를 해서 리턴한다.
66 | - 읽기 전용 `proxy`를 리턴한다.
67 | - 레코드 캡슐화를 재귀적으로 한다.
68 | - 장점: 확실하게 모든 부분을 제어할 수 있다.
69 | - 단점: 작업량이 매우 늘어난다.
70 |
71 | ### 📖 참고
72 |
73 | - 읽기 전용 Proxy를 만드는 법
74 |
75 | - 책에는 js에서는 어렵다고 쓰여 있지만 `es6`에서는 비교적 간단하게 `proxy`를 제공해 줄 수 있다. 단, 이 방법의 단점은 1depth까지만 쓰기 차단을 지원한다는 점이다.
76 |
77 | ```jsx
78 | const readonlyHandler = {
79 | set(target, name, value) {
80 | throw new Error('Cannot Assgin value to readonly property');
81 | },
82 | };
83 |
84 | class Data {
85 | constructor(data) {
86 | this._data = data;
87 | this._proxy = new Proxy(this._data, readonlyHandler);
88 | }
89 |
90 | get data() {
91 | return this._proxy;
92 | }
93 | }
94 | ```
95 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/extract-class.md:
--------------------------------------------------------------------------------
1 | # 7.5 클래스 추출하기 (Extract Class)
2 |
3 | 큰 클래스에서 역할에 따라 일부 기능을 담당하는 클래스를 분리하는 것
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Delivery {
11 | get zipCode() {
12 | return this._zipCode;
13 | }
14 | get baseAddress() {
15 | return this._baseAddress;
16 | }
17 | get detailAddress() {
18 | return this._detailAddress;
19 | }
20 | }
21 | ```
22 |
23 | ### 😍 To be
24 |
25 | ```js
26 | class Delivery {
27 | get zipCode() {
28 | return this._address.zipCode;
29 | }
30 | get baseAddress() {
31 | return this._address.baseAddress;
32 | }
33 | get detailAddress() {
34 | return this._address.detailAddress;
35 | }
36 | }
37 |
38 | class Address {
39 | get zipCode() {
40 | return this._zipCode;
41 | }
42 | get baseAddress() {
43 | return this._baseAddress;
44 | }
45 | get detailAddress() {
46 | return this._detailAddress;
47 | }
48 | }
49 | ```
50 |
51 | ### 📋 상세
52 |
53 | 메서드와 데이터가 적은 클래스는 이해하기 쉽다. 이해하기 힘들고 복잡한 클래스를 단순화하고, 의존성을 명확하게 정리할 수 있다. 다음과 같을 때 이 리팩터링을 고려해볼 수 있다.
54 |
55 | - 메서드와 데이터가 너무 많을 때
56 | - 일부 데이터와 메서드를 따로 묶을 수 있을 때
57 | - 함께 변경되는 일이 많거나, 서로 의존하는 데이터들을 분리한다.
58 | - 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없는 것은 분리한다.
59 | - 서브클래스를 만들 때, 다음과 같은 신호가 있다면 분리하는 것이 좋다.
60 | - 작은 일부 기능만을 위해 서브클래스를 만들어야 할 때
61 | - 확장해야 할 기능이 무엇인지에 따라 서브클래스를 만드는 방식이 달라질 때
62 |
63 | ### ⚙️ 절차
64 |
65 | 1. 클래스의 역할을 분리할 방법을 정하고, 분리된 역할을 담당할 새 클래스를 만든다.
66 | 2. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성해 필드에 저장한다.
67 | 3. 분리될 역할에 필요한 필드를 새 클래스로 옮기고 테스트한다1).
68 | 4. 메서드들을 새 클래스로 옮기고 테스트한다2). 이때 저수준 메서드(다른 메서드에게 호출을 많이 당하는 메서드)부터 옮긴다.
69 | 5. 양쪽 클래스의 인터페이스에서 불필요한 메서드를 제거하고 이름을 변경한다.
70 | 6. 새 클래스를 외부로 노출할지 정한다. 노출할 경우 새 클래스에 참조를 값으로 바꾸기3)를 적용할지 고민해본다.
71 |
72 | ## 📝 메모
73 |
74 | - 메쉬원에는 이 리팩토링을 적용해야 하는 것들이 굉장히 많다
75 | - 리스트와 상세: 둘은 다른 모델을 다루고 있고, 관심사도 정확히 분리된다
76 | - 거대해진 뷰모델 스토어: 뷰모델 스토어에서 다루는 기능들을 하나로 뭉쳐서 쓰지 않는다면(ex. 탭이 다르거나, 모달이 새로 열리거나, ...) 분리하는 것이 낫다.
77 |
78 | ### 📖 참고
79 |
80 | 1) 8.2 필드 옮기기: 특정 값을 다른 필드의 하위값으로 옮기기
81 | 2) 8.1 함수 옮기기: 특정 메서드를 다른 메서드로 옮기기
82 | 3) 9.4 참조를 값으로 바꾸기: 필드가 immutable한 것이 좋을 때, 값을 setter가 참조를 통해 바꾸는 것이 아니라 새로 선언해 저장하게 하는 것
83 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/extract-function.md:
--------------------------------------------------------------------------------
1 | # 함수 추출하기 (Extract Function)
2 |
3 | 하나의 독립된 논리로 분리할 수 있는 코드 덩어리를 찾아서 별도의 함수로 분리하여 목적을 더 선명하게 드러내는 기법.
4 |
5 | 
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```js
12 | function printOwing(invoice) {
13 | printBanner();
14 | let outstanding = calculateOutstanding();
15 | console.log("고객명: ${invoice.customer}");
16 | console.log("채무액: ${outstanding}");
17 | }
18 | ```
19 |
20 | ### 😍 To be
21 |
22 | ```js
23 | function printOwing(invoice) {
24 | printBanner();
25 | const outstanding = calculateOutstanding();
26 | printDetails(outstanding);
27 | }
28 |
29 | function printDetails(outstanding) {
30 | console.log("고객명: ${invoice.customer}");
31 | console.log("채무액: ${outstanding}");
32 | }
33 | ```
34 |
35 | ### 📋 상세
36 |
37 | 하나의 독립된 논리로 분리할 수 있는 코드 덩어리를 찾아서 별도의 함수로 분리하고 목적을 드러내는 이름을 할당한다. 함수가 지나치게 길거나 함수의 논리가 복잡해서 코드가 하는 일을 쉽게 이해하기가 어려운 문제를 해결할 때 사용할 수 있는 기법이다. 함수가 복잡하지 않아도 의도를 더 분명하게 드러내기 위해서 함수 추출하기를 사용할 수도 있다.
38 |
39 | ### ⚙️ 절차
40 |
41 | 1. 함수를 새로 만들어서 함수의 목적을 설명하는 이름을 함수에 붙여 준다.
42 | 2. 추출할 코드를 원본 함수에서 복사해서 새 함수에 붙여 넣는다.
43 | 3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수가 있다면 매개변수로 전달한다.
44 | 4. 원본 함수에서 추출한 코드를 새로 만든 함수를 호출하는 문장으로 바꾼다.
45 | 5. 테스트한다.
46 | 6. 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다.
47 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/extract-superclass.md:
--------------------------------------------------------------------------------
1 | # 12.8 슈퍼클래스 추출하기 (Extract Superclass)
2 |
3 | 비슷한 일을 수행하는 두 클래스가 보이면 상속 메커니즘을 이용해서 비슷한 부분을 공통의 슈퍼클래스로 옮겨 담는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Department{
11 | get totalAnnualCost(){...}
12 | get name(){...}
13 | get headCount(){...}
14 | }
15 |
16 | class Employee{
17 | get annualCost(){...}
18 | get name(){...}
19 | get id(){...}
20 | }
21 | ```
22 |
23 | ### 😍 To be
24 |
25 | ```js
26 | class Party{
27 | get name(){...}
28 | get annualCost(){...}
29 | }
30 |
31 | class Department extends Party{
32 | get headCount(){...}
33 | }
34 |
35 | class Employee extends Party{
36 | get id(){...}
37 | }
38 | ```
39 |
40 | ### 📋 상세
41 |
42 | 객체 지향을 설명할 때 현실세계의 분류 체계에 기초하여 부모 자식 관계를 신중하게 설계하라고 흔히들 하는데.
43 | 실제로는 구현하면서 상속관계를 깨우쳐가는 경우가 많다. (공통 요소를 찾았을 때 슈퍼클래스로 끌어올린다.)
44 |
45 | 슈퍼클래스 추출하기의 대안으로는 클래스 추출하기가 있다. 중복 동작을 상속으로 해결하느냐, 위임으로 해결하느냐의 차이.
46 | 슈퍼클래스 추출하기가 간단할 경우가 많으니 이 기법부터 시도하길 추천.
47 |
48 | ### ⚙️ 절차
49 |
50 | 1. 빈 슈퍼클래스를 만든다. 원래의 클래스들이 새 클래스를 상속하도록 한다.
51 | 2. 테스트한다.
52 | 3. 생성자 본문 올리기, 메서드 올리기, 필드 올리기를 차례를 적용하여 공통 원소를 슈퍼클래스로 옮긴다.
53 | 4. 서브클래스에 남은 메서드들을 검토한다. 공통되는 부분이 있다면 함수로 추출한 다음 메서드 올리기를 적용한다.
54 | 5. 원래 클래스를 사용하는 코드를 검토하여 슈퍼클래스의 인터페이스를 사용하게 할지 고민해본다.
55 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/hide-delegate.md:
--------------------------------------------------------------------------------
1 | # 7.7 위임 숨기기 (Hide Delegate)
2 |
3 | 캡슐화의 기본적인 기능인 필드를 숨기는 것에서 더 나아가 객체가 가지고 있는 위임 객체를 직접적으로 부르지 않고 메서드를 통하여 호출함으로써 위임 객체를 숨기는 리팩터링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | class Person {
11 | #name: string;
12 | #department: Department;
13 |
14 | constructor(name: string, department: Department) {
15 | this.#name = name;
16 | this.#departsment = department;
17 | }
18 |
19 | get name() {
20 | return this.#name;
21 | }
22 |
23 | get department() {
24 | return this.#department;
25 | }
26 | }
27 |
28 | class Department {
29 | #manager: string;
30 | #code: number;
31 |
32 | constructor(manager: string, code: number) {
33 | this.#manager = manager;
34 | this.#code = code;
35 | }
36 |
37 | get manager() {
38 | return this.#manager;
39 | }
40 |
41 | get code() {
42 | return this.#code;
43 | }
44 | }
45 |
46 | // 생성 생략
47 | console.log(person.department.manager);
48 | ```
49 |
50 | ### 😍 To be
51 |
52 | ```typescript
53 | class Person {
54 | // ...중략
55 |
56 | get manager() {
57 | return this.#department.manager;
58 | }
59 | }
60 |
61 | // 생성 생략
62 | console.log(person.manager);
63 | ```
64 |
65 | ### 📋 상세
66 |
67 | - 위임 객체의 인터페이스가 변경되더라도 클라이언트의 코드를 모두 수정할 필요가 없다.
68 | - 즉, 서버의 코드가 바뀌더라도 최소한의 변경으로 하위호환성을 살릴 수 있다.
69 | - 특정 클래스 내부에 선언되어 있는 위임 객체의 인터페이스를 너무 많이 사용하고 있을 때 리팩토링을 고려해볼 수 있다.
70 |
71 | ex)
72 |
73 | ```typescript
74 | // as-is
75 | partner.contact.name;
76 | partner.contact.phone;
77 | partner.contact.address;
78 | // 여기서 contact는 Partner를 관리하는 사람에 대한 위임 객체
79 |
80 | // to-be
81 | partner.managerName;
82 | partner.managerPhone;
83 | partner.managerAddress;
84 | ```
85 |
86 | ### ⚙️ 절차
87 |
88 | 1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성한다.
89 | 2. 클라이언트가 위임 객체 대신 서버(위임 객체의 getter 또는 메서드)를 호출하도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
90 | 3. 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
91 | 4. 테스트한다.
92 |
93 | ## 📝 메모
94 |
95 | - 위임 객체를 제거하고 위임 메서드를 생성할 때, 의미의 변질을 막기 위해서 문맥에 맞는 네이밍을 정해야 한다.
96 | - 위임 메서드가 늘어날수록 클래스의 관리가 어려워지기 때문에 이 경우에는 7.8 중개자 제거하기를 사용해야 한다.
97 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/imgs/decompose-conditional.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/study/refactoring/catalogs/imgs/decompose-conditional.png
--------------------------------------------------------------------------------
/study/refactoring/catalogs/imgs/extract-function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/study/refactoring/catalogs/imgs/extract-function.png
--------------------------------------------------------------------------------
/study/refactoring/catalogs/imgs/inline-function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/study/refactoring/catalogs/imgs/inline-function.png
--------------------------------------------------------------------------------
/study/refactoring/catalogs/imgs/replace-temp-with-query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/study/refactoring/catalogs/imgs/replace-temp-with-query.png
--------------------------------------------------------------------------------
/study/refactoring/catalogs/imgs/split-loop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/study/refactoring/catalogs/imgs/split-loop.png
--------------------------------------------------------------------------------
/study/refactoring/catalogs/inline-class.md:
--------------------------------------------------------------------------------
1 | # 클래스 인라인하기 (Inline Class)
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```js
8 | // 소스 클래스
9 | class TrackingInformation {
10 | public shippingCompany;
11 | public trackingNumber;
12 |
13 | get display() {
14 | return `${this.shippingCompany}: ${this.trackingNumber}`
15 | }
16 | }
17 |
18 | // 타깃 클래스 (소스 클래스를 포함하고 있는 클래스)
19 | class Shipment {
20 | public trackingInformation;
21 | }
22 |
23 | shipment.trackingInformation.display;
24 | ```
25 |
26 | ### 😍 To be
27 |
28 | ```js
29 | class Shipment {
30 | public shippingCompany;
31 | public trackingNumber;
32 |
33 | get trackingInformationDisplay() {
34 | return `${this.shippingCompany}: ${this.trackingNumber}`
35 | }
36 | }
37 |
38 | shipment.trackingInformationDisplay;
39 | ```
40 |
41 | ### 적용 시점
42 |
43 | - 더 이상 역할을 못 해서 그대로 두면 클래스를 발견했을 때
44 | - 리팩터링 후 특정 클래스에 남은 역할이 거의 없을 때 자주 발생한다.
45 | - 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때
46 | - 클래스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출하는 게 쉬울 수도 있다.
47 |
48 | ### ⚙️ 절차
49 |
50 | 1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다.
51 | - 이 메서드 들은 단순히 작업을 소스 클래스로 위임해야 한다.
52 | 2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다.
53 | - 하나씩 바꿀 때마다 테스트한다.
54 | 3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.
55 | - 하나씩 바꿀 때마다 테스트한다.
56 | 4. 소스 클래스를 삭제하고 조의를 표한다.
57 |
58 | #### 중간 과정 (2. 이후)
59 |
60 | ```js
61 | class Shipment {
62 | public trackingInformation;
63 |
64 | get trackingInformationDisplay() {
65 | return trackingInformation.display;
66 | }
67 | }
68 |
69 | shipment.trackingInformationDisplay;
70 | ```
71 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/inline-function.md:
--------------------------------------------------------------------------------
1 | # 함수 인라인하기 (Inline Function)
2 |
3 | 함수를 호출하는 코드를 함수 본문으로 대체하는 기법.
4 |
5 | 
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```js
12 | function getRating(driver) {
13 | return moreThanFiveLateDeliveries(drive) ? 2 : 1;
14 | }
15 |
16 | function moreThanFiveLateDeliveries(driver) {
17 | return drive.numberOfLateDeliveries > 5;
18 | }
19 | ```
20 |
21 | ### 😍 To be
22 |
23 | ```js
24 | function getRating(driver) {
25 | return drive.numberOfLateDeliveries > 5 ? 2 : 1;
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 함수 본문을 함수 이름 뒤로 감추지 않고 직접 드러내는 게 더 유리할 때 함수를 호출하는 코드를 함수 본문으로 대체하는 방법. 주로 리팩터링을 하는 중간에 논리를 한 곳으로 모으기 위해서 사용하며, 단순히 위임만 하는 함수가 너무 많은 경우에도 사용할 수 있다.
32 |
33 | ### ⚙️ 절차
34 |
35 | 1. 다형 메서드인지 확인한다. 다형 메서드라면 인라인을 하면 안 된다.
36 | 2. 인라인할 함수를 호출하는 곳을 모두 찾는다.
37 | 3. 각 호출문을 함수 본문으로 교체한다.
38 | 4. 하나씩 교체할 때마다 테스트한다.
39 | 5. 함수를 삭제한다.
40 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/introduce-assertion.md:
--------------------------------------------------------------------------------
1 | # 10.6 어서션 추가하기 (Introduce Assertion)
2 |
3 | 특정 조건이 참일 때만 제대로 동작하는 코드 영역 위에 어서션을 추가한다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | if (this.discountRate) {
11 | base = base - this.discountRate * base;
12 | }
13 | ```
14 |
15 | ### 😍 To be
16 |
17 | ```js
18 | assert(this.discountRate >= 0);
19 | if (this.discountRate) {
20 | base = base - this.discountRate * base;
21 | }
22 | ```
23 |
24 | ### 📋 상세
25 |
26 | 어서션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘못했다는 뜻이다. 어서션 실패는 시스템의 다른 부분에서는 절대 검사하지 않아야 하며, 어서션이 있고 없고가 프로그램 기능의 정상 동작에 아무런 영향을 주지 않도록 작성돼야 한다.
27 |
28 | 단위 테스트를 꾸준히 추가하여 사각을 좁히면 어서션보다 나을 때가 많다. 하지만 소통 측면에서는 어서션이 여전히 매력적이다.
29 |
30 | 반드시 참이어야 하는 것만, 프로그래머가 일으킬만한 오류에만 어서션을 사용한다. 데이터를 외부에서 읽어 온다면 예외 처리로 대응해야 한다.
31 |
32 | ### ⚙️ 절차
33 |
34 | 1. 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
35 |
36 | ## 📝 메모
37 |
38 | - JavaScript에서는 `assert()`가 없다.
39 | - Node.js에서는 [assert API](https://nodejs.org/api/assert.html#assert_assert_value_message))를 사용해서 조건이 거짓인 경우 `AssertionError`를 던지도록 할 수 있다.
40 | - 브라우저에서는 `console.assert()`를 사용하여 조건이 거짓인 경우 메시지를 콘솔에 출력할 수 있다. 또는 npm에서 [assert](https://www.npmjs.com/package/assert) 모듈을 사용해서 Node.js와 동일하게 사용할 수 있다.
41 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/introduce-parameter-object.md:
--------------------------------------------------------------------------------
1 | # 매개변수 객체 만들기 (Introduce Parameter Object)
2 |
3 | > 데이터 항목 여러 개가 이 함수에서 저 함수로 함께 몰려다닐 때 데이터 구조 하나로 모아줘야 한다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | function checkNumberInRange(n, min, max) {
11 | // ...
12 | }
13 |
14 | checkNumberInRange(7, 1, 10);
15 | ```
16 |
17 | ### 😍 To be
18 |
19 | ```js
20 | function checkNumberInRange(n, range) {
21 | // ...
22 | }
23 |
24 | const range = new NumberRange(1, 10);
25 | checkNumberInRange(7, range);
26 | ```
27 |
28 | ### ⚙️ 절차
29 |
30 | 1. 적당한 데이터 구조를 (새 클래스로) 만든다.
31 | 2. 테스트한다.
32 | 3. 함수 선언 바꾸기(6.5절)로 새 데이터 구조를 매개변수로 추가한다.
33 | 4. 테스트한다.
34 | 5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
35 | 6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
36 | 7. 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.
37 |
38 | ### 예시
39 |
40 | ```js
41 | function checkNumberInRange(n, min, max) {
42 | return n > min && n < max;
43 | }
44 |
45 | checkNumberInRange(7, 1, 10);
46 | ```
47 |
48 | ```js
49 | class NumberRange {
50 | constructor(min, max) {
51 | this._data = { min, max };
52 | }
53 |
54 | get min() {
55 | return this._data.min;
56 | }
57 |
58 | get max() {
59 | return this._data.max;
60 | }
61 | }
62 | ```
63 |
64 | ```js
65 | function checkNumberInRange(n, min, max, range) {
66 | return n > min && n < max;
67 | }
68 |
69 | const range = new NumberRange(1, 10);
70 | checkNumberInRange(7, 1, 10, range);
71 | ```
72 |
73 | ```js
74 | function checkNumberInRange(n, range) {
75 | return n > range.min && n < range.max;
76 | }
77 |
78 | const range = new NumberRange(1, 10);
79 | checkNumberInRange(7, range);
80 | ```
81 |
82 | ## 📝 메모
83 |
84 | - 이렇게 새로 만든 클래스가 프로그램을 간결하고 추상적으로 만드는데 도움이 된다.
85 | - 관련된 동작을 클래스 내부로 옮길 수 있다.
86 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/introduce-special-case.md:
--------------------------------------------------------------------------------
1 | # 특이 케이스 추가하기 (Introduce Special Case)
2 |
3 | 데이터 구조의 특정 값을 확인한 후 같은 동작을 수행하는 코드가 여러 곳에 있다면 그 반응을 한데로 모아서 처리하는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | // 고객명 처리
11 | const aCustomer = site.customer;
12 | let customerName;
13 | if (aCustomer === "미확인 고객") customerName = "거주자";
14 | else customerName = aCustomer.name;
15 | ```
16 |
17 | ### 😍 To be
18 |
19 | ```js
20 | class UnknownCustomer {
21 | get name() {
22 | return "거주자";
23 | }
24 | }
25 |
26 | class Site {
27 | get customer() {
28 | return this._customer === "미확인 고객"
29 | ? new UnknownCustomer()
30 | : this._customer;
31 | }
32 | }
33 |
34 | // 고객명 처리
35 | const customerName = aCustomer.name;
36 | ```
37 |
38 | ### 📋 상세
39 |
40 | 특이 케이스는 여러 형태로 표현할 수 있다.
41 | 단순히 데이터를 읽기만 한다면 리터럴 객체.
42 | 동작이 필요하다면 메서드를 담은 객체.
43 |
44 | 클래스가 반환하도록 하게 할수도 있고
45 | 변환 함수를 거처 기존 데이터 구조에 데이터를 추가하는 형태도 될 수 있다.
46 |
47 | ### ⚙️ 절차
48 |
49 | 1. 미확인 고객인지를 나타내는 메서드를 고객 클래스에 추가한다.
50 |
51 | ```js
52 | class Customer {
53 | get isUnknown() {
54 | return false;
55 | }
56 | }
57 | ```
58 |
59 | 2. 미확인 고객 전용 클래스를 만든다.
60 |
61 | ```js
62 | class UnknownCustomer {
63 | get isUnknown() {
64 | return true;
65 | }
66 | }
67 | ```
68 |
69 | 3. 특이 케이스인지 확인하는 코드를 함수로 추출한다.
70 |
71 | ```js
72 | function isUnknown(arg) {
73 | if (!(arg instanceof Customer || arg === "미확인 고객"))
74 | throw new Error(`잘못된 값과 비교: <${arg}>`);
75 | return arg === "미확인 고객";
76 | }
77 |
78 | let customerName;
79 | if (isUnknown(aCustomer)) customerName = "거주자";
80 | else customerName = aCustomer.name;
81 | ```
82 |
83 | 4. 특이 케이스일때 Site가 UnknownCustomer 객체를 반환하도록 수정.
84 |
85 | ```js
86 | class Site {
87 | get customer() {
88 | return this._customer === "미확인 고객"
89 | ? new UnknownCustomer()
90 | : this._customer;
91 | }
92 | }
93 | ```
94 |
95 | 5. isUnknown 함수도 UnknownCustomer 사용하도록 수정
96 |
97 | ```js
98 | function isUnknown(arg) {
99 | if (!(arg instanceof Customer || arg instanceof UnknownCustomer))
100 | throw new Error(`잘못된 값과 비교: <${arg}>`);
101 | return arg.isUnknown;
102 | }
103 | ```
104 |
105 | 6. 모든 기능이 잘 작동하는지 테스트
106 | 7. 기본값으로 대체할 수 있는 코드를 클래스에 묶기 (고객 이름 메서드를 UnknownCustomer에 추가)
107 |
108 | ```js
109 | class UnknownCustomer {
110 | get name() {
111 | return "거주자";
112 | }
113 | }
114 | ```
115 |
116 | 8. 예외 케이스 검사처리
117 |
118 | ```js
119 | const name = aCustomer.isUnknown ? "미확인 거주자" : aCustomer.name;
120 | ```
121 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/move-field.md:
--------------------------------------------------------------------------------
1 | # 필드 옮기기 (Move Field)
2 |
3 | > 주어진 문제에 적합한 데이터 구조를 활용하면 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Customer {
11 | constructor(discountRate) {
12 | this._discountRate = discountRate;
13 | this._contract = new CustomerContract();
14 | }
15 |
16 | get discountRate() { return this._discountRate; }
17 | }
18 |
19 | class CustomerContract {
20 | }
21 | ```
22 |
23 | ### 😍 To be
24 |
25 | ```js
26 |
27 | class Customer {
28 | constructor(discountRate) {
29 | this._contract = new CustomerContract();
30 | this._setDiscountRate(discountRate);
31 | }
32 |
33 | get discountRate() { return this._contract.discountRate; }
34 | // private 함수이므로 set 키워드를 사용하지 않음
35 | _setDiscountRate(arg) { this._contract.discountRate = arg; }
36 | }
37 |
38 | class CustomerContract {
39 | get discountRate() { return this._discountRate; }
40 | set discountRate(arg) { this._discountRate = arg; }
41 | }
42 | ```
43 |
44 | ### 📋 상세
45 |
46 | 초기 설계에서는 실수가 빈번하므로 데이터 구조가 적절치 않음을 깨닫게 되면 곧바로 수정해야 한다. 필드 옮기기 리팩터링은 대체로 더 큰 변경의 일환으로 수행된다. 공유 객체로 이동하는 경우에는 문제가 발생할 수 있으므로 실제 데이터를 확인해야 한 뒤에 리팩터링해야 한다. (예: '계좌' 클래스에 있던 '이자율' 필드를 '계좌 타입' 클래스로 옮기는 경우, 리팩터링 후에는 '계좌 타입'이 같으면 항상 '이자율'이 같게 된다.)
47 |
48 | ### ⚙️ 절차
49 |
50 | 1. 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
51 | 2. 테스트한다.
52 | 3. 타깃 객체에 필드(와 접근자 메서드)를 생성한다.
53 | 4. 정적 검사를 수행한다.
54 | 5. 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
55 | 6. 접근자들이 타깃 필드를 사용하도록 수정한다.
56 | 7. 테스트한다.
57 | 8. 소스 필드를 제거한다.
58 | 9. 테스트한다.
59 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/move-function.md:
--------------------------------------------------------------------------------
1 | # 8.1 함수 옮기기
2 |
3 | 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있는지를 나타내는 모듈성(modularity)이다.
4 |
5 | 모듈성이란 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 수정을 가능하게 해주는 능력이다. 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
6 |
7 | 모듈성에 대한 이해가 높아질수록 소프트웨어 요소들을 더 잘 묶는 새로운 방법을 깨우치게 된다. 높아진 이해를 반영하기 위해서는 요소들을 이리저리 옮겨야 할 수 있다.
8 |
9 | ## 🗣 설명
10 |
11 | ### 🧐 As is
12 |
13 | ```javascript
14 | // GPS 추적 기록의 총 거리를 계산하는 함수
15 | function trackSummary(points) {
16 | const totalTime = calculateTime();
17 | const totalDistance = calculateDistance():
18 | const pace = totalTime / 60 / totalDistance;
19 | return {
20 | time: totalTime,
21 | distance: totalDistance,
22 | pace: pace,
23 | }
24 |
25 | function calculateDistance() {
26 | let result = 0;
27 | for (let i = 1; i < points.length; i++) {
28 | return += distance(points[i-1], points[i]);
29 | }
30 | }
31 |
32 | function distance(p1, p2) { /* ... */ }
33 | function radians(degrees) { /* ... */ }
34 | function calculateTime() { /* ... */ }
35 | }
36 | ```
37 |
38 | ### 😍 To be
39 |
40 | 중첩 함수인 `calculateDistance`를 최상위로 옮겨 "추적 거리"를 다른 정보와는 독립적으로 계산한다.
41 |
42 | ```javascript
43 | function trackSummary(points) {
44 | const totalTime = calculateTime();
45 | const pace = totalTime / 60 / totalDistance(points);
46 | return {
47 | time: totalTime,
48 | distance: totalDistance(points),
49 | pace: pace,
50 | }
51 |
52 | function calculateTime() { /* ... */ }
53 | }
54 |
55 | // '추적 거리'를 계산하는 함수
56 | function totalDistance(points) {
57 | let result = 0;
58 | for (let i = 1; i < points.length; i++) {
59 | return += distance(points[i-1], points[i]);
60 | }
61 | }
62 |
63 | function distance(p1, p2) { /* ... */ }
64 | function radians(degrees) { /* ... */ }
65 | ```
66 |
67 | ### 📋 상세
68 |
69 | 모든 함수는 어떤 컨텍스트 안에 존재한다. 객체 지향 프로그래밍의 핵심 모듈화 컨텍스트는 클래스다. 또는 함수를 다른 함수에 중첩시켜 또 다른 공통 컨텍스트를 만들게 된다.
70 |
71 | 어떤 함수가 자신이 속한 모듈 A의 요소들보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 마땅하다. 이렇게 하면 이 소프트웨어의 나머지 부분은 모듈 B의 세부사항에 덜 의존하게 된다.
72 |
73 | 그리고 다른 함수 안에서 도우미 역할로 정의된 함수 중 독립적으로도 고유한 가치가 있는 것은 접근하기 쉬운 장소로 옮기는 게 낫다.
74 |
75 | ### ⚙️ 절차
76 |
77 | 1. 소스 컨텍스트(옮기기 이전의 컨텍스트)에서 옮길 함수를 사용하고 있는 모든 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는지 고민해본다.
78 | 2. 옮길 함수가 다형 메서드인지 확인한다.
79 | 3. 옮길 함수를 타깃 컨텍스트로 옮긴다(원래의 함수를 소스 함수라 하고, 옮긴 후에 새로 만들어진 함수를 타깃 함수라 한다).
80 | 4. 정적 분석을 수행한다.
81 | 5. 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
82 | 6. 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
83 | 7. 테스트한다.
84 | 8. 소스 함수를 인라인할지 고민해본다.
85 |
86 | ## 📝 메모
87 |
88 | ### 💁♂️Tip
89 |
90 | - 함수를 옮길지 말지 정하기는 쉽지 않지만, 옮길 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보면 도움이 된다.
91 | - 옮길 함수를 호출하는 함수들은 무엇인지, 옮길 함수가 호출하는 함수들은 또 무엇이 있는지, 옮길 함수가 사용하는 데이터는 무엇인지 살펴봐야 한다.
92 | - 여러 함수를 새로운 컨텍스트로 묶어야 한다면, "클래스로 묶기"나 "클래스 추출하기"로 해결할 수 있다.
93 | - 함수를 옮길 최적의 장소를 정하기가 어려울 수도 있지만, 선택이 어려울수록 큰 문제가 아닌 경우가 많다. 그곳이 얼마나 적합한지는 차차 깨달아갈 것이고, 잘 맞지 않다고 판단되면 위치는 언제든 옮길 수 있다.
94 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/move-statements-into-function.md:
--------------------------------------------------------------------------------
1 | # 8.3 문장을 함수로 옮기기(Move Statements into Function)
2 |
3 | 특정 함수 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 것이 계속 중복될 때, 해당 문장을 함수 안으로 옮기는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | result.push(`
`,
37 | ];
38 | }
39 | ```
40 |
41 | ### 📋 상세
42 |
43 | 기본적으로 중복 코드를 제거하는 방법 중 하나. 문장 앞뒤로 똑같은 코드들이 수행되는 것이 많으며, 그 문장들이 피호출 함수의 일부라는 확신이 있을 때 이 리팩토링을 한다.
44 | 이 코드의 동작을 여러 변형으로 나눠야 하는 순간이 있다면, 8.4장의 "문장을 호출한 곳으로 옮기기"를 적용해 다시 뽑아내면 된다.
45 | 피호출 함수와 한 몸이라는 확신은 없지만 같이 불려다니는 일이 많은 문장이 있을 경우에는, 해당 문장들과 함수를 또 하나의 함수로 묶어 추출하면 된다. 이 책에서 소개하는 단계가 함수 추출 후 인라인하기 - 이름 바꾸기로 이뤄져 있으므로, 이 경우 아래 절차의 4번까지만 진행하고 멈추면 된다.
46 |
47 | ### ⚙️ 절차
48 |
49 | 1. 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이드하기를 적용해 근처로 옮긴다.
50 | 2. 타깃 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사하고 테스트한다. 이 경우 나머지 단계는 진행하지 않는다.
51 | 3. 호출자가 둘 이상이면 호출자 중 하나에서 타깃 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께 다른 함수로 추출(6.1절)하고, 기억하기 쉬운 임시 이름을 붙여준다.
52 | 4. 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정하고, 수정할 때마다 테스트한다.
53 | 5. 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
54 | 6. 새로운 함수의 이름을 원래 함수 이름으로 바꿔주거나, 더 나은 이름이 있을 경우 그 이름을 쓴다.
55 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/move-statements-to-callers.md:
--------------------------------------------------------------------------------
1 | # 8.4 문장을 호출한 곳으로 옮기기 (Move Statements to Callers)
2 |
3 | 함수의 기능이 변경되어 호출자에 따라 다르게 동작해야하는 경우, 달라진 동작을 함수에서 꺼내 해당 호출자로 옮기는 리팩터링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```javascript
10 | emitPhotoData(outStream, person.photo);
11 |
12 | function emitPhotoData(outStream, photo) {
13 | outStream.write(`
\n`);
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 함수는 추상화의 기본 블록이다. 하지만 추상화의 경계는 모호하기 때문에 코드베이스의 기능 범위가 달라지면 추상화의 경계도 같이 움직인다.
32 |
33 | 함수도 초기에는 응집도 높고 한 가지 일만을 수행하다가, 변경이 거듭될수록 둘 이상의 다른 일을 수행하게 될 수도 있다.
34 |
35 | 이런 경우에 여러 곳에서 사용하던 기능이 일부 호출자에서는 다르게 동작하도록 바뀌어야할 때가 있는데, 이 때에 리팩터링을 적용한다.
36 |
37 | ### ⚙️ 절차
38 |
39 | 1. 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음(혹은 마지막)줄(들)을 잘라내어 호출자(들)로 복사해 넣는다(필요하면 적당히 수정한다). 테스트만 통과하면 이번 리팩터링은 여기서 끝이다.
40 | 2. 더 복잡한 상황에서는, 이동하지 '않길' 원하는 모든 문장을 [함수로 추출](./extract-function.md)한 다음 검색하기 쉬운 임시 이름을 지어준다.
41 | > -> 대상 함수가 서브클래스에서 오버라이드됐다면 오버라이드한 서브클래스들의 메서드 모두에서 동일하게, 남길 부분을 메서드로 추출한다. 이때 남겨질 메서드의 본문은 모든 클래스에서 똑같아야 한다. 그런 다음 (슈퍼클래스의 메서드만 남기고) 서브클래스들의 메서드를 제거한다.
42 | 3. 원래 [함수를 인라인](./inline-function.md)한다.
43 | 4. 추출된 함수의 이름을 원래 함수의 이름으로 변경한다([함수 이름 바꾸기](./rename-variable.md))
44 | > -> 더 나은 이름이 떠오르면 그 이름을 사용하자.
45 |
46 | ## 📝 메모
47 |
48 | - 함수의 기능이 너무 많아져 분리를 고려할 때, 호출자로 구문을 옮길 수 있는지도 같이 검토해보자.
49 | - 반대로 이 리팩터링을 사용하여 분리할 구문이 너무 많다면 함수로 분리하는 것도 생각해보자.
50 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/parameterize-function.md:
--------------------------------------------------------------------------------
1 | # 함수 매개변수화 하기 (Parameterize Function)
2 |
3 | 두 함수의 로직이 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 처리해 하나의 함수로 중복을 없앨 수 있다.
4 | 매개변수만 바꾸면 여러 곳에서 사용할 수 있어 함수의 유용성이 커진다.
5 |
6 | ## 🗣 설명
7 |
8 | ### 🧐 As is
9 |
10 | ```js
11 | function tenPercentRaise(aPerson) {
12 | aPerson.salary = aPerson.salary.multiply(1.1);
13 | }
14 | function fivePercentRaise(aPerson) {
15 | aPerson.salay = aPerson.salary.multiply(1.05;
16 | }
17 | ```
18 |
19 | ### 😍 To be
20 |
21 | ```js
22 | function raise(aPerson, factor) {
23 | aperson.salary = aPerson.salary.multiply(1 + factor);
24 | }
25 | ```
26 |
27 | ### 📋 상세
28 |
29 | ### ⚙️ 절차
30 |
31 | 1. 비슷한 함수 중 하나를 선택한다.
32 | 2. 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
33 | 3. 이 함수를 호출하는 곳 모두에 적잘한 리터럴 값을 추가한다.
34 | 4. 테스트 한다.
35 | 5. 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다.하나 수정할 때마다 테스트한다.
36 | 6. 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. 하나 수정할 때마다 테스트한다.
37 | -> 매개변수화된 함수가 대체할 비슷한 함수와 다르게 동작한다면, 그 비슷한 함수의 동작도 처리할 수 있도록 본문 코드를 적절히 수정 후 진행한다.
38 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/preserve-whole-object.md:
--------------------------------------------------------------------------------
1 | # 11.4 객체 통째로 넘기기(Preserve Whole Object)
2 |
3 | 레코드를 통째로 넘겨 함수를 변화에 대응하기 쉽도록 변경하는 방법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | function withinRange(bottom: number, top: number) {
11 | return bottom >= temperatureRange.low && top <= temperatureRange.high;
12 | }
13 |
14 | const { low, high } = daysTempRange;
15 | if (withinRange(low, high)) {
16 | alerts.push("방 온도가 지정 범위를 벗어났습니다.");
17 | }
18 | ```
19 |
20 | ### 😍 To be
21 |
22 | ```typescript
23 | function withinRange(tempRange: object) {
24 | const { low, high } = tempRange;
25 | return bottom >= low && top <= high;
26 | }
27 |
28 | if (!withinRange(daysTempRange)) {
29 | alerts.push("방 온도가 지정 범위를 벗어났습니다.");
30 | }
31 | ```
32 |
33 | ### 📋 상세
34 |
35 | 레코드를 통째로 넘기면 변화에 대응하기 쉽다.
36 | 예컨대 그 함수가 더 다양한 데이터를 사용하도록 바뀌어도 매개변수 목록은 수정할 필요가 없다.
37 | 그리고 매개변수 목록이 짧아져서 일반적으로는 함수 사용법을 이해하기 쉬워진다.
38 |
39 | ### ⚙️ 절차
40 |
41 | 1. 매개변수들을 원하는 형태로 받는 빈 함수를 만든다.
42 | → 마지막 단계에서 이 함수의 이름을 변경해야 하니 검색하기 쉬운 이름으로 지어준다.
43 | 2. 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
44 | 3. 정적 검사를 수행한다.
45 | 4. 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트하자.
46 | → 수정 후에는 원래의 매개변수를 만들어내는 코드 일부가 필요 없어질 수 있다. 따라서 죽은 코드 제거하기로 없앨 수 있다.
47 | 5. 호출자를 모두 수정했다면 원래 함수를 인라인한다.
48 | 6. 새 함수의 이름을 적절히 수정하고 모든 호출자에 반영한다.
49 |
50 | ## 📝메모
51 |
52 | 하지만 함수가 레코드 자체에 의존하기를 원치 않을 때는 이 리팩터링을 수행하지 않는데, 레코드와 함수가 서로 다른 모듈에 속한 상황이면 특히 더 그렇다.
53 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/pull-up-constructor-body.md:
--------------------------------------------------------------------------------
1 | # 12.3 생성자 본문 올리기 (Pull Up Constructor Body)
2 |
3 | 생성자는 다루기 까다롭다. 일반 메서드와는 많이 달라서, 생성자에서 하는 일에는 제약을 두는 것이 좋다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```jsx
10 | class Party {
11 | // 생략
12 | }
13 |
14 | class Employee extends Party {
15 | constructor(name, id, monthlyCost) {
16 | super();
17 | this._id = id;
18 | this._name = name;
19 | this._monthlyCost = monthlyCost;
20 | }
21 | // 생략
22 | }
23 |
24 | class Department extends Party {
25 | constructor(name, staff) {
26 | super();
27 | this._name = name;
28 | this._staff = staff;
29 | }
30 | // 생략
31 | }
32 | ```
33 |
34 | ### 😍 To be
35 |
36 | ```jsx
37 | class Party {
38 | constructor(name) {
39 | this._name = name;
40 | }
41 | // 생략
42 | }
43 |
44 | class Employee extends Party {
45 | constructor(name, id, monthlyCost) {
46 | super(name);
47 | this._id = id;
48 | this._monthlyCost = monthlyCost;
49 | }
50 | // 생략
51 | }
52 |
53 | class Department extends Party {
54 | constructor(name, staff) {
55 | super();
56 | this._staff = staff;
57 | }
58 | // 생략
59 | }
60 | ```
61 |
62 | ### 📋 상세
63 |
64 | 마틴 파울러는 서브 클래스에서 기능이 같은 메서드들을 발견하면 "함수 추출하기"와 "메서드 올리기"를 차례로 적용해서 말끔히 슈퍼클래스로 옮기곤 한다. 그런데 그 메서드가 생성자라면 스텝이 꼬인다. 생성자는 할 수 있는 일과 호출 순서에 제약이 있기 때문에 조금 다른 식으로 접근해야 한다.
65 |
66 | ### ⚙️ 절차
67 |
68 | 1. 슈퍼클래스에 생성자가 없다면 하나 정의한다. 서브클래스의 생성자들에서 이 생성자가 호출되는지 확인한다.
69 | 2. "문장 슬라이드하기"로 공통 문장 모두를 `super()` 호출 직후로 옮긴다.
70 | 3. 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 고통 코드에서 참조하는 값들을 모두 `super()` 로 건넨다.
71 | 4. 테스트한다.
72 | 5. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 "함수 추출하기"와 "메서드 올리기"를 차례로 적용한다.
73 |
74 | ## 📝 메모
75 |
76 | ### ⚠️ 공통 코드가 나중에 올 때
77 |
78 | "2 단계"에서 공통 코드를 `super` 뒤로 옮길 수 없는 경우도 있다.
79 |
80 | ```jsx
81 | class Employee {
82 | constructor(name) { ... }
83 | get isPrivileged() { ... }
84 | assignCar() { ... }
85 | }
86 |
87 | class Manager extends Employee {
88 | constructor(name, grade) {
89 | super(name);
90 | this._grade = grade;
91 | if (this.isPrivileged) this.assignCar(); // 모든 서브 클래스에서 중복되는 코드
92 | }
93 |
94 | get isPrivileged() {
95 | return this._grade > 4;
96 | }
97 | }
98 | ```
99 |
100 | 이 경우에는 먼저 공통 코드를 함수로 추출한다. 그리고 추출한 메서드를 슈퍼 클래스로 옮긴다.
101 |
102 | ```jsx
103 | class Employee {
104 | // 생략...
105 | finishConstruction() {
106 | if (this.isPrivileged) this.assignCar();
107 | }
108 | }
109 |
110 | class Manager extends Employee {
111 | constructor(name, grade) {
112 | super(name);
113 | this._grade = grade;
114 | this.finishConstruction(); // 슈퍼 클래스의 메서드를 호출하게 한다.
115 | }
116 |
117 | get isPrivileged() {
118 | return this._grade > 4;
119 | }
120 | }
121 | ```
122 |
123 | ### 💁♂️Tip
124 |
125 | - 이 리팩터링이 간단히 끝날 것 같지 않다면 "생성자를 팩터리 함수로 바꾸기"를 고려해본다.
126 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/pull-up-field.md:
--------------------------------------------------------------------------------
1 | # 12.2 필드 올리기(Pull Up Field)
2 |
3 | 서브클래스들이 독립적으로 개발되었거나 뒤늦게 하나의 계층구조로 리팩터링된 경우에서 일부 기능이 중복되어 있을 때 사용하는 리팩터링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | class Employee {}
11 |
12 | class SalesPerson extends Employee {
13 | private name: string;
14 | }
15 |
16 | class Engineer extends Employee {
17 | private name: string;
18 | }
19 | ```
20 |
21 | ### 😍 To be
22 |
23 | ```typescript
24 | class Employee {
25 | protected name: string;
26 | }
27 |
28 | class SalesPerson extends Employee {}
29 | class Engineer extends Employee {}
30 | ```
31 |
32 | ### 📋 상세
33 |
34 | 일부 기능이 중복되어 있는 경우 비슷한 방식으로 쓰인다고 판단되면 슈퍼클래스로 끌어올린다.
35 | 이 경우 두 가지 중복을 줄일 수 있다.
36 |
37 | 1. 데이터 중복 선언을 없앨 수 있다.
38 | 2. 해당 필드를 사용하는 동작을 서브클래스에서 슈퍼클래스로 옮길 수 있다.
39 |
40 | ### ⚙️ 절차
41 |
42 | 1. 후보 필드들을 사용하는 곳 모두가 그 필드들을 똑같은 방식으로 사용하는지 면밀히 살핀다.
43 | 2. 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다(필드 이름 바꾸기).
44 | 3. 슈퍼클래스에 새로운 필드를 생성한다.
45 | → 서브클래스에서 이 필드에 접근할 수 있어야 한다(대부분 언어에서는 protected로 선언하면 된다).
46 | 4. 서브클래스의 필드들을 제거한다.
47 | 5. 테스트한다.
48 |
49 | ## 📝메모
50 |
51 | 이 리팩터링을 진행하면서 서브클래스 하나에서만 사용하는 필드는 "필드 내리기"를 사용해서 슈퍼클래스에서 필드를 제거하자.
52 |
53 | 한 번에 두 개의 리팩터링을 할 수 있다.
54 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/pull-up-method.md:
--------------------------------------------------------------------------------
1 | # 12.1 메서드 올리기(Pull Up Method)
2 |
3 | 같은 동작을 하는 메서드들을 슈퍼클래스로 이동해 서브클래스가 상속받아 사용하도록 만들어서 코드 중복과 관리비용을 줄이는 기법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | class Party {
11 | ...
12 | }
13 |
14 | class Employee extends Party {
15 | get annualCost() {
16 | return this.monthlyCost * 12;
17 | }
18 |
19 | get monthlyCost() {
20 | ...
21 | }
22 | }
23 |
24 | class Department extends Party {
25 | get totalAnnualCost() {
26 | return this.monthlyCost * 12;
27 | }
28 |
29 | get monthlyCost() {
30 | ...
31 | }
32 | }
33 | ```
34 |
35 | ### 😍 To be
36 |
37 | ```typescript
38 | class Party {
39 | get annualCost() {
40 | return this.monthlyCost * 12;
41 | }
42 |
43 | get monthlyCost() {
44 | throw new SubclassResponsibilityError('자식 클래스에서 구현해주세요');
45 | }
46 | }
47 |
48 | class Employee extends Party {
49 | get monthlyCost() {
50 | ...
51 | }
52 | }
53 |
54 | class Department extends Party {
55 | get monthlyCost() {
56 | ...
57 | }
58 | }
59 | ```
60 |
61 | ### 📋 상세
62 | - 메서드에서 참조하는 필드가 슈퍼클래스에 없는 경우 [12.2 필드 올리기](./pull-up-field.md)를 해야한다고 하는데 자바스크립트에는 해당 없음
63 | - 다만, 함정 메서드를 만들면 좋다. (서브클래스 책임 오류)
64 |
65 | ### ⚙️ 절차
66 | 1. 대상이 되는 메서드들이 똑같이 동작하는 메서드인지 살펴본다. (차이점에 유념하며 살펴본다.)
67 | 1. 같은 일을 하지만 코드가 다르다면 코드가 같아질때까지 리팩터링 한다. ([11.2 함수를 매개변수화하기](./parameterize-function.md))
68 |
69 |
70 | 예제
71 |
72 |
73 | `annualCost`와 `totalAnnualCost`가 서로 같은 일을 한다.
74 | ```typescript
75 | class Employee extends Party {
76 | get annualCost() {
77 | return this.monthlyCost * 12;
78 | }
79 | }
80 |
81 | class Department extends Party {
82 | get totalAnnualCost() {
83 | return this.monthlyCost * 12;
84 | }
85 | }
86 | ```
87 |
88 |
89 | 2. 메서드 안에서 참조하는 필드와 호출하는 메서드가 슈퍼클래스에서도 참조하고 호출할 수 있는지 확인한다.
90 | 1. 이름이 다르다면 [6.5 함수 선언 바꾸기](./change-function-declaration.md) 로 통일한다.
91 |
92 |
93 | 예제
94 |
95 |
96 | `annualCost`로 이름을 통일한다. `monthlyCost`는 각 서브클래스에 구현되어 있다.
97 | ```typescript
98 | class Employee extends Party {
99 | get annualCost() {
100 | return this.monthlyCost * 12;
101 | }
102 | }
103 |
104 | class Department extends Party {
105 | get annualCost() {
106 | return this.monthlyCost * 12;
107 | }
108 | }
109 | ```
110 |
111 |
112 | 3. 슈퍼클래스에 새로운 메서드를 생성해서 옮길 대상 메서드 코드를 복사한다.
113 |
114 |
115 | 예제
116 |
117 |
118 | 대상 메서드를 슈퍼클래스로 복사한다.
119 | ```typescript
120 | class Party {
121 | get annualCost() {
122 | return this.monthlyCost * 12;
123 | }
124 | }
125 | ```
126 |
127 |
128 | 4. 정적 검사를 수행한다.
129 | 5. 문제가 없다면 서브클래스 중 하나의 메서드를 제거하고 테스트한다.
130 | 6. 모든 서브클래스의 메서드가 없어질때까지 5.를 반복한다.
131 |
132 |
133 | 예제
134 |
135 |
136 | 필요하다면 서브클래스에서 구현해야 하는 메서드는 함정메서드로 바꿔준다.
137 | ```typescript
138 | class Party {
139 | get annualCost() {
140 | return this.monthlyCost * 12;
141 | }
142 |
143 | get monthlyCost() {
144 | throw new SubclassResponsibilityError('자식 클래스에서 구현해주세요');
145 | }
146 | }
147 | ```
148 |
149 |
150 | ## 📝메모
151 |
152 | ### 템플릿 메서드 패턴
153 | - 대상 메서드들의 알고리즘 흐름은 비슷하지만, 세부적인 내용이 다르다면 슈퍼클래스의 메서드에서 흐름을 정의해주고, 세부 동작은 서브클래스에서 구현하도록한다.
154 |
155 |
156 | 예제
157 |
158 |
159 | ```typescript
160 | class Beverage {
161 | prepareRecipe() {
162 | this.boilWater();
163 | this.brew();
164 | this.pourInCup();
165 | this.addCondiments();
166 | }
167 | }
168 |
169 | class Coffee extends Beverage {
170 | brew() {
171 | console.log('커피를 우려냅니다');
172 | }
173 |
174 | addCondiments() {
175 | console.log('우유를 추가합니다');
176 | }
177 | }
178 |
179 | class Tea extends Beverage {
180 | brew() {
181 | console.log('차를 우려냅니다');
182 | }
183 |
184 | addCondiments() {
185 | console.log('레몬을 추가합니다');
186 | }
187 | }
188 | ```
189 |
190 |
191 | - 참고: [templateMethod](https://refactoring.com/catalog/formTemplateMethod.html)
192 |
193 | ### 서브클래스 책임 오류
194 | 서브클래스가 확장에 대한 인터페이스를 준수하도록 강제하는 오류
195 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/push-down-field.md:
--------------------------------------------------------------------------------
1 | # 12.5 필드 내리기(Push Down Field)
2 |
3 | 특정 서브 클래스만 사용하는 필드를 해당 서브 클래스로 옮기기.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```jsx
10 | class Employee {
11 | private SalesPolicy policy;
12 | }
13 |
14 | class Engineer extends Employee { ... }
15 | class Salesperson extends Employee { ... }
16 | ```
17 |
18 | ### 😍 To be
19 |
20 | ```jsx
21 | class Employee { ... }
22 |
23 | class Engineer extends Employee { ... }
24 | class Salesperson extends Employee {
25 | protected SalesPolicy policy;
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 하나(또는 소수)의 서브 클래스가 사용하는 필드가 부모 클래스에 있을 때, 이 필드를 서브 클래스로 옮기는 리팩터링 기법. 불필요한 정보를 제거하여 코드 복잡도를 낮추고, 응집성은 높일 수 있다.
32 |
33 | ### ⚙️ 절차
34 |
35 | 1. 대상 필드를 모든 서브 클래스에 정의한다.
36 | 2. 슈퍼 클래스에서 그 필드를 제거한다.
37 | 3. 테스트를 한다.
38 | 4. 이 필드를 사용하지 않는 모든 서브 클래스에서 제거한다.
39 | 5. 테스트를 한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/push-down-method.md:
--------------------------------------------------------------------------------
1 | # 메서드 내리기 (Push Down Method)
2 |
3 | ## 🗣 설명
4 |
5 | 특정 서브클래스 하나와만 관련된 메서드는 슈퍼클래스에서 서브클래스에 추가한다.
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Unit {
11 | get getFuel{...}
12 | }
13 |
14 | class Soldier extends Unit {...}
15 | class Tank extends Unit {...}
16 | ```
17 |
18 | ### 😍 To be
19 |
20 | ```js
21 |
22 | class Unit {...}
23 |
24 | class Soldier extends Unit {...}
25 | class Tank extends Unit {
26 | get getFuel{...}
27 | }
28 | ```
29 |
30 | ### 📋 상세
31 |
32 | 호출자가 해당 기능을 제공하는 서브클래스가 정확히 무엇인지 알고 있을 때만 적용이 가능하다.
33 | 그렇지 못한 상황이라면 서브클래스에 따라 다르게 동작하는 슈퍼클래스의 기만적인 조건부 로직을 다형성으로 바꿔야한다.
34 |
35 | ### ⚙️ 절차
36 |
37 | 1. 대상 메서드를 모든 서브클래스에 복사한다.
38 | 2. 슈퍼클래스에서 그 메서드를 제거한다.
39 | 3. 테스트한다.
40 | 4. 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
41 | 5. 테스트한다.
42 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/remove-dead-code.md:
--------------------------------------------------------------------------------
1 | # 8.9 죽은 코드 제거하기(Remove Dead Code)
2 |
3 | 개발 생산성을 높이기 위해서 불필요한 코드를 제거하는 방법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | if (false) {
11 | doSomething();
12 | }
13 | ```
14 |
15 | ### 😍 To be
16 |
17 | ```typescript
18 |
19 | ```
20 |
21 | ### 📋 상세
22 |
23 | 사용되지 않는 (다른 곳에서 참조되거나 실행될 일이 없는) 코드는 개발 생산성을 위해 제거되어야 한다. 코드 자체로는 '더 이상 실행되지 않으니 무시해도 된다'는 신호를 줄 수 없기때문에 개발자가 코드를 이해하는데 방해가 될 수 있기 때문이다. 사용하지 않으면 버전관리 시스템이 있으니 삭제해서 읽어야 할 필요가 있는 코드들만 남겨두는 것이 좋다.
24 |
25 | ### ⚙️ 절차
26 | 1. 죽은 코드가 외부에서 참조되는 곳이 있는지 확인한다.
27 | 2. 없다면 제거하고 테스트 한다.
28 |
29 | ## 📝메모
30 | 만약 삭제하되, 나중에 찾기 불안하다면 검색하기 쉬운 별도의 커밋으로 삭제 커밋을 만들면 좋을 것 같다.
31 |
32 | ### 참고
33 | - [죽은 코드 종류 예시1](https://cimfalab.github.io/deepscan/2016/07/unused-codes-1)
34 | - [죽은 코드 종류 예시1](https://cimfalab.github.io/deepscan/2016/07/unused-codes-2)
35 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/remove-flag-argument.md:
--------------------------------------------------------------------------------
1 | # 11.3 플래그 인수 제거하기(Remove Flag Argument)
2 |
3 | 플래그 인수를 받는 함수를, 각각의 제한적 기능을 수해하는 두 개의 함수로 분리하여 함수의 동작을 예측하기 쉽게 만드는 기법.
4 |
5 |
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```jsx
12 | function setDimension(name, value) {
13 | if(name === "height") {
14 | this._height = value;
15 | return;
16 | }
17 | if(name === "width") {
18 | this._width = value;
19 | return;
20 | }
21 | }
22 | ```
23 |
24 | ### 😍 To be
25 |
26 | ```jsx
27 | function setHeight(value) { this._height = value; }
28 | function setWidth(value) { this._width = value; }
29 | ```
30 |
31 | ### 📋 상세
32 |
33 | **1) 플래그 인수 제거하기**
34 |
35 | 플래그 인수란? 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수.
36 |
37 | 플래그 인수(flag argument)를 갖는 함수는, 함수의 동작과 호출 방식을 이해하기가 어렵다. 플래그 값에 따른 함수의 동작 시나리오가 겉으로 잘 드러나지 않기 때문이다.
38 |
39 | 아래 함수의 두 번째 인자는 어떤 의미일까? 두 번째 인자에 false를 입력한다면 이 함수는 어떻게 동작할까?
40 |
41 | ```jsx
42 | buyTicket(aCustomer, true);
43 | ```
44 |
45 | 물론 임시 변수를 이용해 플래그 인자의 의미를 더 선명하게 드러낼 수도 있다.
46 |
47 | ```jsx
48 | const premium = true;
49 | buyTicket(aCustomer, premium);
50 | ```
51 |
52 | 하지만 플래그 인수를 제거한 후 책임을 분리한 아래의 함수는 앞의 두 예제에 비해 훨씬 함수의 의도를 쉽게 이해할 수 있다.
53 |
54 | ```jsx
55 | buyPremiumTicket(aCustomer);
56 | ```
57 |
58 | > 함수는 똑똑해질수록 복잡해진다. 함수가 똑똑한다는 것은 `많은 일`을 하고 있다는 방증이기 때문이다. 플래그 인자가 보인다면 함수의 책임을 분리해야 한다는 신호는 아닌지 살펴보자.
59 |
60 | **2) 매개변수를 까다로운 방식으로 사용하는 경우**
61 |
62 | 플래그 인수를 함수 내부에서 훨씬 까다롭게 사용하는 아래와 같은 경우는, 단순히 `onRush` 함수를 바깥으로 드러내는 걸로는 문제를 해결할 수 없다.
63 |
64 | ```jsx
65 | function deliveryDate(anOrder, onRush) {
66 | let result;
67 | let deliveryTime;
68 |
69 | if(anOrder.deliveryState === "MA" || anOrder.deliveryState === "CT") {
70 | deliveryTime = onRush ? 1 : 2;
71 | } else if(anOrder.deliveryState === "NY" || anOrder.deliveryState === "NH") {
72 | deliveryTime = 2;
73 | if(anOrder.deliveryState === "NH" && !isRush) {
74 | deliveryTime = 3;
75 | }
76 | } else if(onRush) {
77 | deliveryTime = 3;
78 | } else if(anOrder.deliveryState === "ME") {
79 | deliveryTime = 3;
80 | } else {
81 | deliveryTime = 4;
82 | }
83 |
84 | result = anOrder.placedOn.plusDays(2 + deliveryTime);
85 | if(onRush) {
86 | result = result.minusDays(1);
87 | }
88 |
89 | return result;
90 | }
91 | ```
92 |
93 | 이런 경우는 `deliveryDate` 함수를 감싸는 래핑 함수를 만드는 걸 고려해볼 수 있다.
94 |
95 | ```jsx
96 | function rushDeliveryDate(anOrder) {
97 | return deliveryDate(anOrder, true);
98 | }
99 |
100 | function regularDeliveryDate(anOrder) {
101 | return deliveryDate(anOrder, false);
102 | }
103 | ```
104 |
105 | > 개인적으로는 이런 경우에는 각 조건절의 로직을 캡슐화해서 별도 개체로 분리하는 게 더 낫다고 생각하지만 이 챕터의 범위를 넘어서기 때문에 여기에는 논외로 한다.
106 |
107 | ### ⚙️ 절차
108 |
109 | 1. 플래그 인자에 대응하는 개별 함수를 만든다.
110 | 2. 원래 함수를 호출하는 코드를 모두 찾아서 개별 함수를 호출하도록 수정한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/remove-intermediary.md:
--------------------------------------------------------------------------------
1 | # 7.8 중개자 제거하기
2 |
3 | "위임 숨기기"의 반대 기법이며, 클래스 사이의 연결 관계를 너무 많이 숨기려다 인터페이스가 비대해질 경우 사용하는 기법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | const manager = aPerson.manager;
11 |
12 | class Person {
13 | get manager() {return this.department.manager;}
14 | }
15 | ```
16 |
17 | ### 😍 To be
18 |
19 | ```js
20 | const manager = aPerson.department.manager;
21 |
22 | class Person {
23 | get department() {return this._department;}
24 | }
25 | ```
26 |
27 | ### 📋 상세
28 |
29 | "위임 숨기기"를 많이 적용하면 클래스에 단순한 위임 메서드가 많아져서 나중에는 클래스가 중개자 역할만 하게 된다. "중개자 제거하기"는 불필요하게 위임만 하는 메서드를 줄여서 클래스의 인터페이스를 간결하게 만들 수 있는 기법이다.
30 |
31 | ### ⚙️ 절차
32 |
33 | 1. 위임 객체를 얻는 게터를 만든다.
34 | 2. 이제 각 클라이언트가 부서(`department`) 객체를 직접 사용하도록 고친다.
35 | 3. 클라이언트를 모두 고쳤다면 `Person`의 `manager()`메서드를 삭제한다. `Person`에 단순히 위임 메서드가 더는 남지 않을 때까지 이 작업을 반복한다.
36 |
37 | ## 📝 메모
38 |
39 | ### ⚠️ 주의할 점
40 |
41 | - "위임 숨기기"와 이 기법 중 하나만 사용해야 한다는 법은 없으며, 적당히 섞어도 된다.
42 | - 자주 쓰는 위임은 "위임 숨기기"로 그대로 두는 편이 낫다.
43 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/remove-setting-method.md:
--------------------------------------------------------------------------------
1 | # 11.7 세터 제거하기 (Remove Setting Method)
2 |
3 | 객체 생성 후 수정하기를 원하지 않는 필드의 세터를 제거해서, 수정하지 않겠다는 의도를 명백하게 밝히는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class Person {
11 | constructor() {}
12 | get name() {
13 | return this._name;
14 | }
15 | set name(arg) {
16 | this._name = arg;
17 | }
18 | get id() {
19 | return this._id;
20 | }
21 | set id(arg) {
22 | this._id = arg;
23 | }
24 | }
25 | ```
26 |
27 | ### 😍 To be
28 |
29 | ```js
30 | class Person {
31 | constructor(id) {
32 | this._id = id;
33 | }
34 | get name() {
35 | return this._name;
36 | }
37 | set name(arg) {
38 | this._name = arg;
39 | }
40 | get id() {
41 | return this._id;
42 | }
43 | }
44 | ```
45 |
46 | ### 📋 상세
47 |
48 | 이 기법을 사용해야 하는 경우는 주로 2가지이다.
49 |
50 | 1. 사람들이 무조건 접근자 메서드를 통해서만 필드를 바꾸려 할 때. (ex: 생성자 안에서만 호출하는 세터가 있는 경우)
51 | 2. 생성 스크립트를 사용해 객체를 생성하려 할때.
52 |
53 | ```js
54 | const martin = new Person();
55 | martin.name = "마틴";
56 | martin.id = "1234";
57 | ```
58 |
59 | ### ⚙️ 절차
60 |
61 | 1. 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
62 | 2. 생성자 밖에서 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나 수정할 때마다 테스트한다.
63 | 3. 세터 메서드를 인라인 한다. 가능하다면 해당 필드를 불변으로 만든다.
64 | 4. 테스트한다.
65 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/rename-field.md:
--------------------------------------------------------------------------------
1 | # 9.2 필드 이름 바꾸기(Rename Field)
2 |
3 | 데이터 필드의 이름을 변경하고 싶을때 사용하는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | class Organization {
11 | get name() {}
12 | }
13 | ```
14 |
15 | ### 😍 To be
16 |
17 | ```ts
18 | class Organization {
19 | get title() {}
20 | }
21 | ```
22 |
23 | ### 📋 상세
24 |
25 | 데이터 구조를 깔끔하게 유지하는 것은 중요하다. 개발을 진행할 수록 다루고 있는 데이터에 대하여 더 많이 이해하게 되는데, 이 깊어진 이해를 데이터에 반영하기 위해서는 값을 설명하는 필드 이름을 그에 맞게 수정해야 한다.
26 |
27 | ### ⚙️ 절차
28 |
29 | 1. 레코드의 유효 범위가 제한적이라면 필드에 접근하는 모든 코드를 수정한 후 테스트한다. 이후 단계는 생략한다.
30 | 2. 레코드가 캡슐화되지 않았다면 우선 레코드를 캡슐화 한다.
31 | 3. 캡슐화된 객체 안의 private 필드 명을 변경하고, 그에 맞게 내부 메서드들을 수정한다.
32 | 4. 테스트한다.
33 | 5. 생성자의 매개변수 중 필드와 이름이 겹치는 게 있다면 함수 선언 바꾸기로 변경한다.
34 | 6. 접근자들의 이름도 바꿔준다.
35 |
36 | ## 📝 메모
37 |
38 | 필드명 변경은 전반적으로 캡슐화된 객체 내부 => 외부 순으로 진행된다.
39 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/rename-variable.md:
--------------------------------------------------------------------------------
1 | # 변수 이름 바꾸기 (Rename Variable)
2 |
3 | > 명확한 프로그래밍의 핵심은 이름짓기다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | const badName = "a";
11 | ```
12 |
13 | ### 😍 To be
14 |
15 | ```js
16 | const goodName = "a";
17 | ```
18 |
19 | ### ⚙️ 절차
20 |
21 | 1. 폭넓게 쓰이는 변수라면 변수 캡슐화하기를 고려한다.
22 | 2. 이 변수를 참조하는 곳을 모두 찾아서 하나씩 변경한다.
23 | - 외부에 공개된 변수 X
24 | - 상수는 다른 이름으로 복제본을 만들어서 점진적으로 변경할 수 있다.
25 | 3. 테스트한다.
26 |
27 | ### 예시: 캡슐화하기
28 |
29 | ```js
30 | let badName = "a";
31 | ```
32 |
33 | ```js
34 | let badName = "a";
35 |
36 | function goodName() {
37 | return badName;
38 | }
39 | function setGoodName(arg) {
40 | badName = arg;
41 | }
42 | ```
43 |
44 | ```js
45 | let _goodName = "a";
46 |
47 | function goodName() {
48 | return _goodName;
49 | }
50 | function setGoodName(arg) {
51 | _goodName = arg;
52 | }
53 | ```
54 |
55 | ### 예시: 상수 이름 바꾸기
56 |
57 | ```js
58 | const badName = "a";
59 | ```
60 |
61 | ```js
62 | const goodName = "a";
63 | const badName = goodName;
64 | ```
65 |
66 | ## 📝 메모
67 |
68 | - TypeScript의 경우 에디터의 이름 바꾸기 (VSCode에서는 F2) 기능을 사용하면 점진적으로 변경할 필요가 없다.
69 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-command-with-function.md:
--------------------------------------------------------------------------------
1 | # 명령을 함수로 바꾸기 (Replace Command with Function)
2 |
3 | ## 🗣 설명
4 |
5 | 명령 객체의 로직이 크게 복잡하지 않다면 평범한 함수로 바꿔준다.
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | class ChargeCalculator {
11 | constructor(customer, usage, provider) {
12 | this._customer = customer;
13 | this._usage = usage;
14 | this._provider = provider;
15 | }
16 |
17 | get baseCharge() {
18 | return this._customer.baseRate * this._usage;
19 | }
20 |
21 | get charge() {
22 | return this.baseCharge + this._provider.connectionCharge;
23 | }
24 | }
25 |
26 | monthCharge = new ChargeCalculator(customer, usage, provider).charge;
27 | ```
28 |
29 | ### 😍 To be
30 |
31 | ```js
32 | function charge(customer, usage, provider) {
33 | const baseCharge = customer.baseRate * usage;
34 | return baseCharge + provider.connectionCharge;
35 | }
36 | ```
37 |
38 | ### 📋 상세
39 |
40 | 복잡한 연산을 다루는 명령 객체는 큰 연산 하나를 여러 개의 작은 메서드로 쪼개고 필드를 이용해 쪼개진 메서드들끼리 정보를 공유할 수 있다. 또한 어떤 메서드를 호출하냐에 따라 다른 효과를 줄 수 있고 각 단계를 거치며 데이터를 조금씩 완성해갈 수도 있다.
41 | 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는 게 낫다.
42 |
43 | ### ⚙️ 절차
44 |
45 | 1. 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수(명령을 대체할 함수)로 추출한다.
46 | 2. 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인한다. 값을 반환하는 메서드라면 반환할 값을 변수로 추출한다.
47 | 3. 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
48 | 4. 명령의 실행 메서드에서 참조하는 필드들 대신 전달받은 매개변수 모두를 실행 메서드로 옮긴다.
49 | 5. 생성자 호출과 명령의 실행 메서드 호출을 호출자 안으로 인라인한다.
50 | 6. 테스트한다.
51 | 7. 죽은 코드 제거하기로 명령 클래스를 없앤다.
52 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-conditional-with-polymorphism.md:
--------------------------------------------------------------------------------
1 | # 10.4 조건부 로직을 다형성으로 바꾸기(Replace Conditional with Polymorphism)
2 |
3 | 특정 타입에 따라 서로 다른 조건부 로직으로 동작하는 경우나, 기본 동작에 추가되는 특수한 동작으로 구성된 로직을 다형성을 이용해 분리하는 기법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | ...
11 |
12 | function captainHistoryRisk(voyage, history) {
13 | let result = 1;
14 | if (history.length < 5) result += 4;
15 | result += history.filter(v => v.profit < 0).length;
16 |
17 | // 기본동작과 구분되는 특수한 조건부 로직
18 | if (voyage.zone === '중국' && hasChina(history)) result -= 2;
19 |
20 | return Math.max(result, 0);
21 | }
22 |
23 | function voyageProfitFactor(voyage, history) {
24 | let result = 2;
25 | if (voyage.zone === '중국') result += 1;
26 | if (voyage.zone === '동인도') result += 1;
27 |
28 | // 기본동작과 구분되는 특수한 조건부 로직
29 | if (voyage.zone === '중국' && hasChina(history)) {
30 | result += 3;
31 |
32 | if (history.length > 10) result += 1;
33 | if (voyage.length > 12) result += 1;
34 | if (voyage.length > 18) result += 1;
35 | } else {
36 | if (history.length > 8) result += 1;
37 | if (voyage.length > 14) result += 1;
38 | }
39 |
40 | return result;
41 | }
42 | ```
43 |
44 | ### 😍 To be
45 |
46 | ```typescript
47 | class Rating {
48 | ...
49 |
50 | get captainHistoryRisk() {
51 | let result = 1;
52 | if (this.history.length < 5) result += 4;
53 | result += this.history.filter(v => v.profit < 0).length;
54 | return Math.max(result, 0);
55 | }
56 |
57 | get voyageProfitFactor() {
58 | let result = 2;
59 | if (this.voyage.zone === '중국') result += 1;
60 | if (this.voyage.zone === '동인도') result += 1;
61 | result += this.historyLengthFactor;
62 | result += this.voyageLengthFactor;
63 | return result;
64 | }
65 |
66 | get historyLengthFactor() {
67 | return this.history.length > 8 ? 1 : 0;
68 | }
69 |
70 | get voyageLengthFactor(): number {
71 | return this.voyage.length > 14 ? -1 : 0;
72 | }
73 | }
74 |
75 | // 특수 로직을 오버라이드 하는 서브클래스
76 | class ExperiencedChinaRating extends Rating {
77 | get captainHistoryRisk() {
78 | const result = super.captainHistoryRisk - 2;
79 | return Math.max(result, 0);
80 | }
81 |
82 | get voyageLengthFactor() {
83 | let result = 0;
84 | if (this.voyage.length > 12) result += 1;
85 | if (this.voyage.length > 18) result += 1;
86 | return result;
87 | }
88 |
89 | get historyLengthFactor() {
90 | return this.history.length > 10 ? 1 : 0;
91 | }
92 |
93 | get voyageProfitFactor() {
94 | return super.voyageProfitFactor + 3;
95 | }
96 | }
97 | ```
98 |
99 | ### 📋 상세
100 |
101 | 다형성을 이용해서 분리하는 경우는 크게 두가지가 있다. 첫번째는 각 함수마다 여러개의 타입에 따라 조건부 로직이 필요한 경우이다. 이 경우에는 클래스로 함수들을 묶어주고 다형성을 이용해 각 타입에 어울리는 서브클래스를 만들어주면 조건부 로직의 중복을 없앨 수 있다. 두번째로는 기본 동작을 위한 로직과 특수한 경우에 추가되는 조건부 로직이 존재하는 경우이다. 이런 경우에는 기본 동작을 수퍼 클래스로 분리하고 서브 클래스에서는 기본 동작과 차이가 있는 특수 동작에만 관여하는 로직을 담아두면, 복잡한 조건부 로직이 기본 로직에만 집중하는 수퍼 클래스와 특수 로직에만 관여하는 서브클래스로 분리되어서 코드에 대한 이해가 쉬워진다.
102 |
103 | ### ⚙️ 절차
104 | 1. 다형적 동작을 표현하는 클래스를 만든다. 조건에 따라 알맞은 인스턴스를 반환하는 팩터리 함수도 만든다.
105 | 2. 팩터리 함수를 사용하도록 코드를 변경한다.
106 | 3. 리팩터링 대상인 조건부 로직을 함수로 추출하고, 수퍼클래스의 메서드로 옮긴다.
107 | 4. 서브 클래스에서 수퍼클래스의 조건부 로직 메서드를 오버라이드 한다.
108 | 1. 조건부에서 해당 서브클래스에 해당하는 조건절을 서브클래스 메서드에서 적절히 수정한다.
109 | 5. 수퍼클래스 메서드에서는 기본 동작만 남긴다. 필요에 따라 추상클래스로 선언하거나, 서브클래스에서 처리해야함을 알리도록 에러를 던진다.
110 |
111 | ## 📝메모
112 | ### 프로젝트 적용하기
113 | - `src/common/lib/format/weeklyHours.ts` 의 경우에 적용해 볼 수 있지 않을까?
114 |
115 | ### 개념
116 | - 다형성: 객체지향 프로그래밍에서 객체가 다양한 형태를 가질 수 있는 능력
117 | - 다형성을 만족하는 서로 다른 객체들은 동일한 요청에 반응할 수 있다.
118 | - 하지만 동일한 요청에 대해서 동작하는 방식은 서로 다를 수 있다.
119 | - 위의 예제에서 `Rating`과 `ExperiencedChinaRating`는 모두 `captainHistoryRisk`라는 요청에 대해 반응할 수 있다.
120 | - 하지만 `captainHistoryRisk`메소드의 동작 방식은 서로 다르다.
121 | - `Rating`과 `ExperiencedChinaRating`는 인터페이스는 같기 때문에 호출하는 입장에서는 같은 메소드를 호출(요청)하기만 하면 된다.
122 | - 다만 다형성을 갖게되니 이렇게 여러가지 형태의 객체로 구현해서 필요에 따라 다른 동작을 하도록 구현할 수 있는 것이다.
123 |
124 | ### 참고
125 | - [위키 - 다형성](https://en.wikipedia.org/wiki/Polymorphism_(computer_science))
126 | - 객체지향의 사실과 오해(책)
127 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-constructor-with-factory-function.md:
--------------------------------------------------------------------------------
1 | # 11.8 생성자를 팩터리 함수로 바꾸기 (Replace Constructor with Factory Function)
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```js
8 | leadEngineer = new Employee(document.leadEngineer, "E");
9 | ```
10 |
11 | ### 😍 To be
12 |
13 | ```js
14 | leadEngineer = createEngineer(document.leadEngineer);
15 | ```
16 |
17 | ### 📋 상세
18 |
19 | 생성자에는 일반 함수에는 없는 제약이 있다.
20 |
21 | - 기본 이름보다 더 적절한 이름이 있어도 사용할 수 없다.
22 | - new 연산자를 사용해야 해서 일반 함수가 오길 기대하는 자리에 쓰기 어렵다.
23 |
24 | 팩터리 함수에는 이런 제약이 없다.
25 |
26 | > 함수에 문자열 리터럴을 건네는 건 악취로 봐야 한다.
27 |
28 | ### ⚙️ 절차
29 |
30 | 1. 팩터리 함수를 만든다. 팩터리 함수의 본문에서는 원래의 생성자를 호출한다.
31 | 2. 생성자를 호출하던 코드를 팩터리 함수 호출로 바꾼다.
32 | 3. 하나씩 수정할 때마다 테스트한다.
33 | 4. 생성자의 가시 범위가 최소가 되도록 제한한다.
34 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-control-flag-with-break.md:
--------------------------------------------------------------------------------
1 | # 제어 플래그를 탈출문으로 바꾸기 (Replace Control Flag with Break)
2 |
3 | 플래그 값을 나중에 검사해 실행시키는 구조를 뒤집어, 더이상 반복문이나 함수 실행이 필요하지 않은 시점에서 `break`나 `return`문을 통해 제어 플래그를 제거하는 리팩토링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | for (const p of people) {
11 | if (!found) {
12 | if (p === "Joker") {
13 | sendAlert();
14 | found = true;
15 | }
16 | }
17 | }
18 | ```
19 |
20 | ### 😍 To be
21 |
22 | ```ts
23 | for (const p of people) {
24 | if (p === "Joker") {
25 | sendAlert();
26 | break;
27 | }
28 | }
29 | ```
30 |
31 | ### 📋 상세
32 |
33 | 주로 반복문에서, 플래그 변수는 반복문을 실행할지 말지를 결정짓거나, 결과 중에 특정 케이스가 발견됐는지 확인하는 데 자주 쓰인다. 그러나 대부분의 케이스에서 플래그 변수는 반복문 쪼개기와 탈출문으로 바꾸기를 통해 삭제할 수 있다.
34 |
35 | ### ⚙️ 절차
36 |
37 | 1. 제어 플래그를 사용하는 코드를 함수로 추출할지 고려한다.
38 | 2. 제어 플래그를 갱신하는 코드 각각을 적절한 제어문(`return`, `break`, `continue`)으로 바꾼다. 하나 바꿀 때마다 테스트 한다.
39 | 3. 모두 수정했다면 제어 플래그를 제거한다.
40 |
41 | ## 📝 메모
42 |
43 | - 반복문에서 하는 것이 많아서 제어 플래그가 여기저기 심어져 있어야 한다면, 반복문 쪼개기를 고려해볼 시간이다.
44 | - 저자는 `break`문이나 `continue`문 사용에 익숙치 않은 사람이 플래그를 자주 심는다고 언급했는데, 파이프라인에 익숙치 않은 사람도 플래그를 자주 심기 마련이다. 반복문 대신 파이프라인을 사용하는 경우, 제어 플래그가 제대로 먹히지 않을 것이다. 이 때 플래그를 사용해 파이프라인이 부수 효과를 일으키는 것을 고려하기 보단, `.some`, `.filter`, `.find` 등 적당한 필터 파이프라인을 앞단계에 넣어주는 것이 좋을 것이다. (더 가다듬기 참고)
45 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-derived-variable-with-query.md:
--------------------------------------------------------------------------------
1 | # 9.3 파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)
2 |
3 | 값을 쉽게 계산해낼 수 있는 변수(파생 변수)를 그 변수를 계산하는 함수(질의 함수)로 바꾼다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | get discountedTotal() { return this._discountedTotal };
11 | set discount (value) {
12 | const oldDiscount = this._discount;
13 | this._discount = value;
14 | this._discountedTotal += oldDiscount - value;
15 | }
16 | ```
17 |
18 | ### 😍 To be
19 |
20 | ```js
21 | get discountedTotal() { return this._baseTotal - this._discount; };
22 | set discount (value) {
23 | this._discount = value;
24 | }
25 | ```
26 |
27 | ### 📋 상세
28 |
29 | 파생 변수가 있으면 변경된 값을 깜빡하고 파생 변수에 반영하지 않는 실수가 일어나기 쉽다. 질의 함수의 계산 과정을 보여주는 코드 자체가 데이터의 의미를 더 분명히 드러내는 경우도 자주 있다.
30 |
31 | 예외적으로 새로운 데이터 구조를 생성하는 변형 연산이라면 비록 계산 코드로 대체할 수 있더라도 그대로 두는 것도 좋다.
32 |
33 | 변형 연산
34 |
35 | - 데이터 구조를 감싸며 그 데이터에 기초하여 계산한 결과를 속성으로 제공하는 객체
36 | - 데이터 구조를 받아 다른 데이터 구조로 변환해 반환하는 함수
37 |
38 | ### ⚙️ 절차
39 |
40 | 1. 변수 값이 갱신되는 지점을 모두 찾는다. 필요하면 변수 쪼개기(9.1)을 활용해 각 갱신 지점에서 변수를 분리한다.
41 | 2. 해당 변수의 값을 계산해주는 함수를 만든다.
42 | 3. 해당 변수가 사용되는 모든 곳에 어서션을 추가(10.6)하여 함수의 계산 결과가 변수의 값과 같은지 확인한다.
43 | 4. 테스트한다.
44 | 5. 변수를 읽는 코드를 모두 함수 호출로 대체한다.
45 | 6. 테스트한다.
46 | 7. 변수를 선언하고 갱신하는 코드를 죽은 코드 제거하기로 없앤다.
47 |
48 | ## 📝 메모
49 |
50 | - `useMemo`, `computed` 등을 사용하면 질의 함수에서의 불필요한 계산도 줄일 수 있다.
51 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-error-code-with-exception.md:
--------------------------------------------------------------------------------
1 | # 11.12 오류 코드를 예외로 바꾸기(Replace Error Code with Exception)
2 |
3 | 프로그램의 정상 동작 범주에 들지 않는 오류를 나타내고 싶을 때 사용하는 리팩터링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | function localShippingRules(country: string) {
11 | const data = countryData.shippingRules[country];
12 | if (data) return new ShippingRules(data);
13 | else return -23;
14 | }
15 |
16 | function calculateShippingCosts(anOrder: object) {
17 | // 관련 없는 로직
18 | const shippingRules = localShippingRules(anOrder.country);
19 | if (shippingRules < 0) return shippingRules; // 오류 전파
20 | // 관련 없는 로직
21 | }
22 |
23 | const status = calculateShippingCosts(orderData);
24 | if (status < 0) errorList.push({ order: orderData, errorCode: status });
25 | ```
26 |
27 | ### 😍 To be
28 |
29 | ```typescript
30 | function localShippingRules(country: string) {
31 | const data = countryData.shippingRules[country];
32 | if (data) return new ShippingRules(data);
33 | else throw new OrderProceessingError(-23);
34 | }
35 |
36 | function calculateShippingCosts(anOrder: object) {
37 | // 관련 없는 로직
38 | const shippingRules = localShippingRules(anOrder.country);
39 | // 관련 없는 로직
40 | }
41 |
42 | try {
43 | calculateShippingCosts(orderData);
44 | } catch (e) {
45 | if (e instanceof OrderProcessingError)
46 | errorList.push({ order: orderData, errorCode: status });
47 | else throw e;
48 | }
49 | ```
50 |
51 | ### 📋 상세
52 |
53 | 예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다. 오류가 발견되면 예외를 던진다. 그러면 적절한 예외 핸들러를 찾을 때까지 콜스택을 타고 위로 전파된다. 예외를 사용하면 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경쓰지 않아도 된다.
54 | 예외는 정교한 메커니즘이지만 대다수의 다른 정교한 메커니즘과 같이 정확하게 사용할 때만 최고의 효과를 낸다. 예외는 정확히 예상 밖의 동작일 때만 쓰여야 한다.
55 |
56 | ### ⚙️ 절차
57 |
58 | 1. 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
59 |
60 | → 이 핸들러는 처음에는 모든 예외를 다시 던지게 해둔다.
61 |
62 | → 적절한 처리를 해주는 핸들러가 이미 있다면 지금의 콜스택도 처리할 수 있도록 확장한다.
63 |
64 | 2. 테스트한다.
65 | 3. 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
66 |
67 | → 사용하는 프로그래밍 언어에 맞게 선택하면 된다. 대부분 언어에서는 서브클래스를 사용하면 될 것이다.
68 |
69 | 4. 정적 검사를 수행한다.
70 | 5. catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
71 | 6. 테스트한다.
72 | 7. 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
73 | 8. 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.
74 |
75 | → 먼저 오류 코드를 검사하는 부분을 함정으로 바꾼 다음, 함정에 걸려들지 않는지 테스트한 후 제거하는 전략을 권한다. 함정에 걸려드는 곳이 있다면 오류 코드를 검사하는 코드가 아직 남아있다는 뜻이다. 함정을 무사히 피했다면 안심하고 본문을 정리하자.
76 |
77 | ## 📝메모
78 |
79 | 저자는 예외 메커니즘을 사용하려면 예상 밖의 동작일 때에만 사용해야 한다고 강조한다.
80 | 즉, 정상 동작 범주에 드는 오류를 처리할 떄는 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.
81 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-exception-with-pre-check.md:
--------------------------------------------------------------------------------
1 | # 11.13 예외를 사전 확인으로 바꾸기 (Replace Exception with Precheck)
2 |
3 | 예외(예외 처리)라는 개념은 프로그래밍 언어의 발전에 의미 있는 한걸음이었다. 오류 코드를 연쇄적으로 전파하던 긴 코드를 예외 처리로 바꿔 깔끔히 제거할 수 있게 되었다. 하지만 예외 처리는 말그대로 예외적인 상황을 처리하는데 사용해야 하며, 예외 처리를 과용해서는 안된다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```java
10 | double getValueForPeriod(int periodNumber) {
11 | try {
12 | return values[periodNumber];
13 | } catch (ArrayIndexOutOfBoundsException e) {
14 | return 0;
15 | }
16 | }
17 | ```
18 |
19 | ### 😍 To be
20 |
21 | ```java
22 | double getValueForPeriod(int periodNumber) {
23 | return (periodNumber >= values.length) ? 0 : values[periodNumber];
24 | }
25 | ```
26 |
27 | ### 📋 상세
28 |
29 | 만약 함수 수행 시 문제가 될 수 있는 조건을 함수 호출전에 검사할 수 있다면, 예외를 던지는 대신에 호출하는 곳에서 조건을 검사하도록 해야 한다.
30 |
31 | ### ⚙️ 절차
32 |
33 | 1. 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. `catch` 블록의 코드를 조건문의 조건절 중 하나로 옮기고, 남은 `try` 블록의 코드를 다른 조건절로 옮긴다.
34 | 2. `catch` 블록에 어서션을 추가하고 테스트한다.
35 |
36 | (예외 상황에서 `catch` 블록에 도달하지 않는지 테스트하기 위함)
37 |
38 | 3. `try` 문과 `catch` 블록을 제거한다.
39 | 4. 테스트한다.
40 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-function-with-command.md:
--------------------------------------------------------------------------------
1 | # 11.9 함수를 명령으로 바꾸기(Replace Function With Command)
2 |
3 | ## 🗣 설명
4 |
5 | 함수를 그 함수만을 위한 객체 안으로 캡슐화 하여 명령 객체로 만드는 행위이다.
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | function score(candidate, medicalExam, scoringGuide) {
11 | let result = 0;
12 | let healthLevel = 0;
13 | // 긴 코드 생략
14 | }
15 | ```
16 |
17 | ### 😍 To be
18 |
19 | ```ts
20 | class Scorer {
21 | constructor(candidate, medicalExam, scoringGuide) {
22 | this._candidate = candidate;
23 | this._medicalExam = medicalExam;
24 | this._scoringGuide = scoringGuide;
25 | }
26 |
27 | public execute() {
28 | this._result = 0;
29 | this._healthLevel = 0;
30 | // 긴 코드 생략
31 | }
32 | }
33 | ```
34 |
35 | ### 📋 상세
36 |
37 | 명령 객체는 평범한 함수보다 훨씬 유연하게 함수를 제어하고 표현할 수 있다. 또한, 객체는 지원하지만 일급 함수를 지원하지 않는 언어에서 그 대체로 사용할 수 있다. 대신 이 리팩터링은 유연성을 얻는 대신 복잡성을 증가시키므로 그 대가에 대하여 잘 생각하고 실행해야한다. 저자의 경우에는 일반적인 경우에는 순수함수가 더 좋으며, 명령을 선택할때는 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 있을 때 명령으로 작성한다고한다.
38 |
39 | ### ⚙️ 절차
40 |
41 | 1. 대상 함수의 기능을 옮길 빈 클래스를 만든다. 클래스 이름은 함수의 이름에 기초해 짓는다.
42 | 2. 방금 생성한 빈 클래스로 함수를 옮긴다.
43 | - 리팩터링이 끝날 때까지는 원래 함수를 전달 함수 역할로 남겨두자
44 | - 명령 관련 이름은 사용되는 프로그래밍 언어의 명명 규칙을 따른다. 규칙이 딱히 없다면 `execute`나 `call`같이 명령의 실행 함수에 흔히 쓰이는 이름을 택하자
45 | 3. 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.
46 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-inline-code-with-function-call.md:
--------------------------------------------------------------------------------
1 | # 8.5 인라인 코드를 함수 호출로 바꾸기
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```js
8 | const appliesToMass = false;
9 | for (const s of states) {
10 | if (s === 'MA') applicesToMass = true;
11 | }
12 | ```
13 |
14 | ### 😍 To be
15 |
16 | ```js
17 | appliesToMass = states.includes('MA');
18 | ```
19 |
20 | ### 📋 상세
21 |
22 | 일반적으로 이미 존재하는 함수와 똑같은 일을 하는 인라인 코드를 발견하면 적용한다. 예외적으로는 기존 함수의 코드를 수정하더라도 인라인 코드의 동작은 바뀌지 않아야 할 때에는 적용하지 않는다.
23 |
24 | ### ⚙️ 절차
25 |
26 | 1. 인라인 코드를 함수 호출로 대체한다.
27 | 2. 테스트한다.
28 |
29 | ## 📝 메모
30 |
31 | 앞선 함수 추출하기(6.1절)과 다른 점은 인라인 코드를 대체할 함수가 있느냐이다. 없다면 함수 추출하기이고, 있다면 함수 호출로 바꾸기이다.
32 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-loop-with-pipeline.md:
--------------------------------------------------------------------------------
1 | # 반복문을 파이프라인으로 바꾸기 (Replace Loop with Pipeline)
2 |
3 | 반복문 내의 처리를 파이프라인으로 표현해 논리를 이해하기 쉬어지도록 하는 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | const names = [];
11 | for (const i of input) {
12 | if (i.job === "programmer") names.push(i.name);
13 | }
14 | ```
15 |
16 | ### 😍 To be
17 |
18 | ```js
19 | const names = input
20 | .filter((i) => i.job === "programmer")
21 | .map((i) => i.name);
22 | ```
23 |
24 | ### 📋 상세
25 |
26 | 대표적인 파이프라인은 map과 filter. 객체가 파이프라인을 따라 흐르며 어떻게 처리되는지를 읽을 수 있기 때문에 파이프라인이 반복문보다 이해하기가 쉬움.
27 |
28 | ### ⚙️ 절차
29 |
30 | 1. 반복문에서 사용하는 컬렉션을 기리키는 변수를 하나 만든다. (기존 변수를 단순히 복하한 것일 수도 있다.)
31 | 2. 반복문의 첫 줄부터 시작해서, 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다. 이때 컬레션 파이프라인 연산은 1에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다. 하나를 대체할 때마다 테스트한다.
32 | 3. 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.
33 | -> 반복문이 결과를 누적 변수(accumulator)에 대입했다면 파이프라인의 결과를 그 누적 변수에 대입한다.
34 |
35 | ```js
36 | const lines = input.splite("\n");
37 | let firstLine = true;
38 | const result = [];
39 | for (const line of lines) {
40 | if (firstLine) {
41 | firstLine = false;
42 | continue;
43 | }
44 | if (line.trim() === "") continue;
45 | const record = line.split(",");
46 | if (record[1].trim() === "India") {
47 | result.push({ city: record[0].trim(), phone: record[2].trim() });
48 | }
49 | }
50 | ```
51 |
52 | 처음에 이런 코드가 있다면
53 | 첫번째 줄을 넘기는 로직은 slice로 빈 스트링을 넘기는 로직은 filter를 사용해서 단계별로 변환하면 된다.
54 |
55 | ```js
56 | const lines = input.splite("\n");
57 | const result = [];
58 | const loopItems = lines.slice(1).filter((line) => line.trim() !== "");
59 |
60 | for (const line of loopItmes) {
61 | const record = line.split(",");
62 | if (record[1].trim() === "India") {
63 | result.push({ city: record[0].trim(), phone: record[2].trim() });
64 | }
65 | }
66 | ```
67 |
68 | 최종적으로는 이렇게 변화하게 된다.
69 |
70 | ```js
71 | const lines = input.splite("\n");
72 | const result = lines
73 | .slice(1)
74 | .filter((line) => line.trim() !== "")
75 | .map((line) => line.split(","))
76 | .filter((record) => record[1].trim() === "India")
77 | .map((record) => ({ city: record[0].trim(), phone: record[2].trim() }));
78 |
79 | return result;
80 | ```
81 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-magic-literal.md:
--------------------------------------------------------------------------------
1 | # 매직 리터럴 바꾸기 (Replace Magic Literal)
2 |
3 | 매직 리터럴이란 소스 코드에 등장하는 일반적인 리터럴 값.
4 | 매직 리터럴 만으로는 명확한 의미를 전달하기 힘들기 때문에 토드 자체에서 분명한 뜻을 나타내는 상수로 정의를 하고 사용한다.
5 |
6 | ## 🗣 설명
7 |
8 | ### 🧐 As is
9 |
10 | ```js
11 | function potentialEnergy(mess, height) {
12 | return mass * 9.81 * height;
13 | }
14 | ```
15 |
16 | ### 😍 To be
17 |
18 | ```js
19 | const STANDARD_GRAVITY = 9.81;
20 | function potentialEnergy(mess, height) {
21 | return mass * STANDARD_GRAVITY * height;
22 | }
23 | ```
24 |
25 | ### 📋 상세
26 |
27 | - 소스코드에 사용 된 코드만으로 의미가 불분명한 매직 리터럴을 상수로 정의하고 그 상수를 사용하도록 리팩토링 한다.
28 | - 상수가 특별한 로직에 쓰이는 경우에는 함수로 바꿀 수 있다.
29 | ```js
30 | if(aValue === "M")
31 |
32 | ->
33 | const MALE_GENDER = "M";
34 | if(aValue === MALE_GENDER)
35 |
36 | ->
37 | if(isMale(aValue))
38 | ```
39 | - 값자체로 의미를 전달 할 수 있거나, 값이 달라질 가능성이 없는 경우 그리고 리터럴이 함수 하나에서만 쓰이면서 맥락 정보를 제공 할 수 있다면 굳이 상수로 바꾸지 않는다.
40 |
41 | ### ⚙️ 절차
42 |
43 | 1. 상수를 선언하고 매직 리터럴을 대입한다.
44 | 2. 해당 리터럴이 사용되는 곳을 모두 찾는다.
45 | 3. 찾은 곳 각각에서 리터럴이 새 상수와 똑같은 의미로 쓰였는지 확인하여, 같은 의미라면 상수로 대체 한 후 테스트한다.
46 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-nested-conditional-with-guard-clasuses.md:
--------------------------------------------------------------------------------
1 | # 10.3 중첩 조건문을 보호 구문으로 바꾸기 (Replace Nested Conditional with Guard Clauses)
2 |
3 | 조건문의 중첩이 너무 많아져서 코드를 알아보기 힘든 경우, 코드의 의도를 부각하기 위해서 보호 구문으로 바꾸는 리팩터링
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```javascript
10 | function someFunction() {
11 | let result;
12 | if (isA) {
13 | result = "A";
14 | } else {
15 | if (isB) {
16 | result = "B";
17 | } else {
18 | if (isC) {
19 | result = "C";
20 | } else {
21 | result = "Z";
22 | }
23 | }
24 | }
25 | return result;
26 | }
27 | ```
28 |
29 | ### 😍 To be
30 |
31 | ```javascript
32 | function someFunction() {
33 | if (isA) return "A";
34 | if (isB) return "B";
35 | if (isC) return "C";
36 | else
37 | return "Z";
38 | }
39 | ```
40 |
41 | ### 📋 상세
42 |
43 | 조건문은 주로 아래와 같은 두 가지 형태로 쓰인다.
44 |
45 | 1. 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태
46 | 2. 한쪽만 정상인 형태
47 |
48 | 1번은 if-then-else절을 사용하여 표현할 수 있고, 2번의 경우에는 보호 구문이라는 형식으로 검사하는 편이 좋다.
49 |
50 | 여기서 보호 구문은 "이건 이 함수의 핵심이 아니다. 이 일이 일어나면 무언가 조치를 취한 후 함수에서 빠져나온다."라고 이야기하는 구문이다.
51 |
52 | if-then-else절의 형태를 사용하면서 반환점이 맨 마지막 하나여야만 하는 규칙은 유용하지 않기 때문에 보호 구문으로 리팩터링할 수 있다.
53 |
54 | ### ⚙️ 절차
55 |
56 | 1. 교체해야할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꾼다.
57 | 2. 테스트한다.
58 | 3. 1~2 과정을 필요한 만큼 반복한다.
59 | 4. 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.
60 |
61 | ## 📝 메모
62 |
63 | - 이 리팩터링을 보니 Callback Hell과 유사한 점이 많이 보였다.
64 |
65 | ex. 코드의 구조, 해결하는 방법(보호 구문, Promise)
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-parameter-with-query.md:
--------------------------------------------------------------------------------
1 | # 11.5 매개변수를 질의 함수로 바꾸기 (Replace Parameter with Query)
2 |
3 | 매개변수는 함수의 동작에 변화를 줄수 있는 수단이다. 만약 매개변수가 여러 개 있다면 짧게 하는 것이 좋고, 의미없는 중복을 제거하는 것이 좋다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```jsx
10 | class Order {
11 | get discountLevel() {
12 | return this.quantity > 100 ? 2 : 1;
13 | }
14 |
15 | get finalPrice() {
16 | const basePrice = this.quantity * this.itemPrice;
17 | return this.discountedPrice(basePrice, this.discountLevel);
18 | }
19 |
20 | discountedPrice(basePrice, discountLevel) {
21 | switch (discountLevel) {
22 | case 1:
23 | return basePrice * 0.95;
24 | case 2:
25 | return basePrice * 0.9;
26 | }
27 | }
28 | }
29 | ```
30 |
31 | ### 😍 To be
32 |
33 | ```jsx
34 | class Order {
35 | get discountLevel() {
36 | return this.quantity > 100 ? 2 : 1;
37 | }
38 |
39 | get finalPrice() {
40 | const basePrice = this.quantity * this.itemPrice;
41 | return this.discountedPrice(basePrice);
42 | }
43 |
44 | discountedPrice(basePrice) {
45 | switch (this.discountLevel) {
46 | case 1:
47 | return basePrice * 0.95;
48 | case 2:
49 | return basePrice * 0.9;
50 | }
51 | }
52 | }
53 | ```
54 |
55 | ### 📋 상세
56 |
57 | 매개변수끼리의 중복은 피하는게 좋다. 예를 들어서 피호출 함수가 충분히 알아낼 수 있는 값을 굳이 건내주는 것도 일종의 중복이다.
58 |
59 | 물론 매개변수를 질의 함수로 바꾸지 말아야 할 상황도 있다. 대표적으로 매개변수를 제거하면 피호출 함수에 원치 않는 의존성이 생길 때다.
60 |
61 | 만약 매개변수를 제거하더라도 다른 의존성이 생기지 않는다면 안심하고 질의 함수로 바꿔도 된다. 다른 매개변수에서 얻을 수 있는 값을 별도로 전달하는 것은 아무 의미가 없다.
62 |
63 | ### ⚙️ 절차
64 |
65 | 1. 필요하다면 대상 매개변수의 값을 계산하는 코드를 별도 함수로 추출해놓는다.
66 | 2. 함수 본문에서 대상 매개변수로의 참조를 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다. 하나 수정할 때마다 테스트한다.
67 | 3. 함수 선언 바꾸기로 대상 매개변수를 없앤다.
68 |
69 | ## 📝 메모
70 |
71 | ### ⚠️ 주의 사항
72 |
73 | 리팩터링 대상이 되는 함수는 참조 투명(referential transparency)해야 한다. 참조 투명이란 '함수에 똑같은 값을 건네 호출하면 항상 똑같이 동작한다'는 뜻이다. 매개변수를 지웠을 때 또 다른 의존성이 생긴다면, 똑같은 값을 건네 호출하더라도 결과가 다르므로 참조 투명하지 않은 함수가 되므로 주의해야 한다.
74 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-primitive-with-object.md:
--------------------------------------------------------------------------------
1 | # 기본형을 객체로 바꾸기(Replace Primitive with Object)
2 |
3 | 저장되는 기본형 데이터를 객체화해서, 해당 데이터에 대한 연산처리 / 특별한 동작 등을 사용하기 쉽도록 한다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```javascript
10 | orders.filter((o) => "high" === o.priority || "rush" === o.priority);
11 | ```
12 |
13 | ### 😍 To be
14 |
15 | ```javascript
16 | orders.filter((o) => o.priority.higherThan(new Priority("normal")));
17 | ```
18 |
19 | ### 📋 상세
20 |
21 | 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의한다.
22 | 시작은 데이터를 감싼 것 뿐이라 큰 차이가 없지만,
23 | 특별한 동작이 추가되면 될수록 직관적이고 효율적인 도구로서 기능한다.
24 |
25 | ### ⚙️ 절차
26 |
27 | 1. 아직 변수를 캡슐화하지 않았다면 캡슐화 한다.
28 |
29 | ```javascript
30 | get prioirty() {return this._priority}
31 | set prioirty(aString) { this._priority = aString}
32 | ```
33 |
34 | 2. 값 클래스를 만든다 (ex: Priority)
35 |
36 | ```javascript
37 | class Priority {
38 | constructor(value) {
39 | this._value = value;
40 | }
41 | toString() {
42 | return this._value;
43 | }
44 | }
45 | ```
46 |
47 | 3. 정적 검사 수행
48 | 4. 클래스를 사용하도록 세터 수정
49 | 5. 클래스를 사용하도록 게터 수정
50 |
51 | ```javascript
52 | get priority(){return this._prioirty.toString()};
53 | set priority(aString){this._prioirty = new Priority(aString)}
54 | ```
55 |
56 | 6. 테스트한다
57 |
58 | 7. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.
59 |
60 | ```javascript
61 | get priorityString() {return this._priority.toString();}
62 | ```
63 |
64 | ### 😍 To be 에 사용된 기능이 추가된 형태
65 |
66 | ```javascript
67 | class Priority {
68 | constructor(value) {
69 | this._value = value;
70 | }
71 | toString() {
72 | return this._value;
73 | }
74 | get _index(){return Priority.legalValues().findIndex(s=> s=== this._value);}
75 | static legalValues(){return ['low', 'normal', 'high', 'rush'];}
76 |
77 | higherThan(other){returns this._index > other._index;}
78 | }
79 | ```
80 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-query-with-parameter.md:
--------------------------------------------------------------------------------
1 | # 11.6 질의 함수를 매개변수로 바꾸기(Replace Query With Parameter)
2 |
3 | 함수 내부에서 외부의 변수나 함수를 직접 참조하고 있을 때, 매개변수로 값을 전달 받도록 코드를 수정 기법.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```jsx
10 | targetTemperature(aPlan)
11 |
12 | function targetTemperature(aPlan) {
13 | currentTemperature = thermostat.currentTemperature;
14 | // ... 나머지 코드
15 | }
16 | ```
17 |
18 | ### 😍 To be
19 |
20 | ```jsx
21 | // thermostat.currentTemperature를 매개변수로 전달
22 | targetTemperature(aPlan, thermostat.currentTemperature);
23 |
24 | function targetTemperature(aPlan, currentTemperature) {
25 | // ... 나머지 코드
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 함수가 외부 변수나 함수를 직접 참조하면 함수가 반환하는 값을 예측하기가 어려움. 의존하는 외부 값이나 함수의 상태가 `부수 효과(Side Effect)`를 만들기 때문. 동일한 매개 변수를 전달하여 함수를 호출할 때, 함수가 항상 동일한 결과를 반환해야 함수의 동작을 이해하기가 더 쉬움. 이런 함수의 성격을 `참조 투명성(referential transparency)`이라고 함.
32 |
33 | 아래의 함수는 참조 투명성이 훼손된 함수임. 함수 외부에 있는 변수 `b`의 값을 인지하고 있어야 `sum` 함수가 반환할 값을 예측할 수가 있음.
34 |
35 | ```jsx
36 | let b = 20;
37 |
38 | function sum(a) {
39 | return a + b;
40 | }
41 |
42 | sum(10); // 30을 반환
43 | b = 50;
44 | sum(10); // 얼마를 반환할까?
45 | ```
46 |
47 | 질의 함수를 의존하는 경우에도 마찬가지로 의존하는 함수에 부수 효과가 발생하면 같은 문제를 만날 수 있음.
48 |
49 | ```jsx
50 | let b = () => 20;
51 |
52 | function sum(a) {
53 | return a + b();
54 | }
55 |
56 | sum(10); // 30을 반환
57 |
58 | b = () => 50; // 사이드 이펙트 발생
59 | sum(10); // 얼마를 반환할까? 함수 내부를 이해해야만 알 수 있음
60 | ```
61 |
62 | `질의 함수를 매개변수로 바꾸기` 기법은 부수 효과에 대한 책임을 함수를 호출하는 고객(Client)으로 넘김.
63 |
64 | ```jsx
65 | function sum(a, b) {
66 | return a + b;
67 | }
68 |
69 | let b = () => 20;
70 | sum(10, b); // 30을 반환
71 |
72 | b = () => 50; // 사이드 이펙트 발생
73 | sum(10, b); // 60을 반환할 것임을 예측할 수 있음
74 | ```
75 |
76 | `sum`을 호출하는 고객은 이제 두 번째 인자의 존재를 이해해야만 하지만, 대신에 함수를 더 안전하고 예측 가능한 방향으로 사용할 수 있음.
77 |
78 | ### ⚙️ 절차
79 |
80 | 1. 질의하는 코드를 변수 추출하기로 함수 본문에서 분리한다.
81 | 2. 함수 추출하기로 질의하는 코드를 제외한 나머지 코드를 새로운 함수로 분리한다.
82 | 3. 1번에서 만든 변수에 변수 인라인하기를 적용한다.
83 | 4. 원본 함수에 함수 인라인하기를 적용한다.
84 | 5. 새로운 함수의 이름을 원본 함수의 이름으로 변경한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-subclass-with-delegate.md:
--------------------------------------------------------------------------------
1 | # 12.10 서브클래스를 위임으로 바꾸기(Replace Subclass With Delegate)
2 |
3 | ## 🗣 설명
4 |
5 | 상속으로 구현된 데이터를 위임으로 변경하여 객체에 유연성을 추가해준다.
6 |
7 | ### 🧐 As is
8 |
9 | ```ts
10 | class Order {
11 | get daysToShip() {
12 | return this._warehouse.daysToShip;
13 | }
14 | }
15 |
16 | class PriorityOrder extends Order {
17 | get daysToShip() {
18 | return this._priorityPlan.daysToShip;
19 | }
20 | }
21 | ```
22 |
23 | ### 😍 To be
24 |
25 | ```ts
26 | class Order {
27 | get daysToShip() {
28 | return this._priorityDelegate
29 | ? this._priorityDelegate.daysToShip
30 | : this._warehouse.daysToShip;
31 | }
32 | }
33 |
34 | class PriorityOrderDelegate {
35 | get daysToShip() {
36 | return this._priorityPlan.daysToShip;
37 | }
38 | }
39 | ```
40 |
41 | ### 📋 상세
42 |
43 | 상속은 속한 갈래에 따라 동작이 달라지는 객체들의 문제를 해결해주지만 한 번만 사용이 가능하다는 점이 문제이다. 무언가가 달라져야 하는 이유가 여러개여도 상속은 그 중 단 하나의 이유만 선택해 기준으로 삼아야 한다.
44 |
45 | 하지만 위임은 다르다. 다양한 클래스에 다양한 이유로 위임할 수 있다. 각 객체간 통신을 위해서 인터페이스를 명확히 정의하므로 상속보다 결합도가 훨씬 약하다는 장점이 있다.
46 |
47 | "(클래스) 상속보다는 (객체) 컴포지션을 사용해라!" 여기서 컴포지션은 사실상 위임과 같은 의미이다. 이 말은 상속을 사용하지 말라는 것이 아니라 주의해서 사용하라는 의미이다. 따라서 일단은 상속으로 접근하고, 나중에라도 필요하면 언제든지 서브클래스를 위임으로 바꿀 준비가 되어있어야 한다.
48 |
49 | ### ⚙️ 절차
50 |
51 | 1. 생성자를 호출하는 곳이 많다면 생성자를 팩터리 함수로 바꾼다.
52 | 2. 위임으로 활용할 빈 클래스를 만든다. 이 클래스의 생성자는 서브클래스에 특화된 데이터를 전부 받아야 하며, 보통은 슈퍼클래스를 가리키는 역참조도 필요하다.
53 | 3. 위임을 저장할 필드를 슈퍼클래스에 추가한다.
54 | 4. 서브클래스 생성코드를 수정하여 위임 인스턴스를 생성하고 위임 필드에 대입해 초기화한다.
55 | - 이 작업은 팩터리 함수가 수행한다. 혹은 생성자가 정확한 위임 인스턴스를 생성할 수 있는게 확실하다면 생성자에서 수행할 수도 있다.
56 | 5. 서브클래스의 메서드 중 위임 클래스로 이동할 것을 고른다.
57 | 6. 함수 옮기기를 적용해 위임 클래스로 옮긴다. 원래 매서드에서 위임하는 코드는 지우지 않는다.
58 | - 이 메서드가 사용하는 원소 중 위임으로 옮겨야 하는게 있다면 함께 옮긴다. 슈퍼클래스에 유지해야 할 원소를 참조한다면 슈퍼클래스를 참조하는 필드를 위임에 추가한다.
59 | 7. 서브클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 슈퍼클래스로 옮긴다. 이때 위임이 존재하는지를 검사하는 보호 코드로 감싸야 한다. 호출하는 외부 코드가 없다면 원래 메서드는 죽은 코드가 되므로 제거한다.
60 | - 서브 클래스가 둘 이상이고 서브클래스들에서 중복이 생겨나기 시작했다면 슈퍼클래스를 추출한다. 이렇게 하여 기본 동작이 위임 슈퍼클래스로 옮겨졌다면 슈퍼클래스의 위임 메서드들에는 보호 코드가 필요없다.
61 | 8. 테스트한다.
62 | 9. 서브클래스의 모든 메서드가 옮겨질 떄까지 5~8 과정을 반복한다.
63 | 10. 서브클래스들의 생성자를 호출하는 코드를 찾아서 슈퍼클래스의 생성자를 사용하도록 수정한다.
64 | 11. 테스트한다.
65 | 12. 서브클래스를 삭제한다.
66 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-superclass-with-delegate.md:
--------------------------------------------------------------------------------
1 | # 12.11 슈퍼 클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)
2 |
3 | 객체 지향 프로그래밍에서 상속은 기존 기능을 재활용하는 강력하고 손쉬운 수단이다. 하지만 상속이 혼란과 복잡도를 키우는 방식으로 이뤄지기도 한다.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```jsx
10 | class List { ... }
11 | class Stack extends List { ...}
12 | ```
13 |
14 | ### 😍 To be
15 |
16 | ```jsx
17 | class Stack {
18 | constructor() {
19 | this._storage = new List();
20 | }
21 | }
22 |
23 | class List { ... }
24 | ```
25 |
26 | ### 📋 상세
27 |
28 | 상속을 잘못 적용한 예로는 자바의 스택 클래스가 유명하다. 자바의 스택은 리스트를 상속하고 있는데, 데이터를 저장하고 조작하는 리스트의 기능을 재활용하겠다는 생각이 초래한 결과다. 하지만 이 상속에는 문제가 있는데, 리스트의 연산 중에는 스택에서 적용되지 않는 게 많음에도 모든 연산이 스택의 인터페이스에 그대로 노출된다. 이보다는 스택에서 리스트 객체를 필드에 저장해두고, 필요한 기능만 위임했다면 더 멋졌을 것이다. 슈퍼 클래스의 기능들이 서브 클래스에 어울리지 않는다면, 그 기능들을 상속을 통해 이용하면 안된다는 신호다.
29 |
30 | 서브클래스 방식 모델링이 합리적일 때라도 슈퍼 클래스를 위임으로 바꾸기도 한다. 슈퍼/서브 클래스는 강하게 결합된 관계라서 슈퍼 클래스를 수정하면 서브 클래스가 망가지기 쉽기 때문이다.
31 |
32 | 물론 위임에도 단점은 있다. 위임의 기능을 이용할 호스트 함수 모두를 전달 함수(forwarding function)로 만들어야 한다는 점이다. 전달 함수를 작성하기란 지루한 일이다. 하지만 아주 단순해서 문제가 생길 가능성은 적다.
33 |
34 | ### ⚙️ 절차
35 |
36 | 1. 슈퍼클래스 객체를 참조하는 필드를 서브 클래스에 만든다(리팩터링이 끝나면 슈퍼 클래스가 위임 객체가 될 것이므로 이 필드를 '위임 참조'라 부른다). 위임 참조를 새로운 슈퍼클래스 인스턴스로 초기화한다.
37 |
38 | ```jsx
39 | class CatalogItem { ... }
40 |
41 | class Scroll extends CatalogItem {
42 | constructor(id, title, tags, dateLastCleaned) {
43 | super(id, title, tags);
44 | this._catalogItem = new CatalogItem(id, title, tags); // 추가
45 | this._lastCleaned = dateLastCleaned;
46 | }
47 | }
48 | ```
49 |
50 | 2. 슈퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다(물론 위임 참조로 전달한다). 서로 관련된 함수끼리 그룹으로 묶어 진행하며, 그룹을 하나씩 만들 때마다 테스트한다.
51 |
52 | ```jsx
53 | class Scroll extends CatalogItem {
54 | // 생략...
55 | get id() {
56 | return this._catalogItem.id;
57 | }
58 | get title() {
59 | return this._catalogItem.title;
60 | }
61 | hasTag(aString) {
62 | return this._catalogItem.hasTag(aString);
63 | }
64 | }
65 | ```
66 |
67 | 3. 슈퍼클래스의 동작 모두가 전달 함수로 오버라이드되었다면 상속 관계를 끊는다.
68 |
69 | ```jsx
70 | // class Scroll extends CatalogItem {
71 | class Scroll {
72 | constructor(id, title, tags, dateLastCleaned) {
73 | // super(id, title, tags)
74 | this._catalogItem = new CatalogItem(id, title, tags);
75 | this._lastCleaned = dateLastCleaned;
76 | }
77 | }
78 | ```
79 |
80 | 슈퍼 클래스를 위임으로 바꾸는 리팩터링은 끝이 났지만, 지금 예시에서는 할 일이 더 남았다. 현재 각각의 스크롤은 다른 카탈로그 아이템 객체를 바라보고 있다. 카탈로그 아이템이 변경되면, 각각의 스크롤을 모두 업데이트 해줘야 한다. 따라서 단 하나의 카탈로그 아이템을 바라보게 만드는 것이 더 좋다.
81 |
82 | 4. 카탈로그 아이템을 참조로 바꾸려면, 스크롤은 카탈로그 아이템의 ID를 자신의 ID로 쓰는 것이 아니라 자신만의 ID를 가져야 한다.
83 |
84 | ```jsx
85 | class Scroll extends CatalogItem {
86 | constructor(id, title, tags, dateLastCleaned) {
87 | this._id = id; // 고유 ID 세팅
88 | this._catalogItem = new CatalogItem(null, title, tags); // 카탈로그 ID로 null 전달
89 | this._lastCleaned = dateLastCleaned;
90 | }
91 |
92 | get id() {
93 | return this._id;
94 | }
95 | }
96 | ```
97 |
98 | 5. 이제 카탈로그 아이템을 생성하는 대신, 인수로 카탈로그 ID와 카탈로그 저장소를 전달받아서, 카탈로그 아이템을 얻어오게 한다. (참고: [**값을 참조로 바꾸기**](./change-value-to-reference.md))
99 |
100 | ```jsx
101 | class Scroll extends CatalogItem {
102 | // constructor(id, title, tags, dateLastCleaned) {
103 | constructor(id, dateLastCleaned, catalogID, catalog) {
104 | this._id = id;
105 | this._catalogItem = catalog.get(catalogID);
106 | this._lastCleaned = dateLastCleaned;
107 | }
108 | }
109 | ```
110 |
111 | ## 📝 메모
112 |
113 | ### ⚠️ 주의 사항
114 |
115 | - "상속을 절대 사용하지 말라"고 조언하는 사람도 있다. 하지만 상위 타입의 모든 메서드가 하위 타입에도 적용되고, 하위 타입의 모든 인스턴스가 상위 인스턴스도 되는 등, 의미상 **적합한** 조건이라면 상속은 간단하고 효과적인 메커니즘이다.
116 | - 그래서 (왠만하면) 상속을 먼저 적용하고, (만일) 나중에 문제가 생기면 슈퍼 클래스를 위임으로 바꾸는 것이 좋다.
117 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/replace-temp-with-query.md:
--------------------------------------------------------------------------------
1 | # 임시 변수를 질의 함수로 바꾸기 (Replace Temp with Query)
2 |
3 | 임시 변수를 값을 반환하는 질의 함수 호출로 대체하는 기법.
4 |
5 | 
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```js
12 | const basePrice = this._quantity * this._itemPrice;
13 |
14 | if(basePrice < 1000)
15 | return basePrice * 0.95;
16 | else
17 | return basePrice * 0.98;
18 | ```
19 |
20 | ### 😍 To be
21 |
22 | ```js
23 | function basePrice() { this._quantity * this._itemPrice; }
24 |
25 | ...
26 |
27 | if(basePrice() > 1000)
28 | return this._basePrice * 0.95;
29 | else
30 | return this.basePrice * 0.98;
31 | ```
32 |
33 | ### 📋 상세
34 |
35 | 긴 함수의 일부 로직을 별도의 함수로 분리할 때, 분리하려는 로직이 참고하는 지역 변수가 많으면 로직을 분리하기가 어렵다. 의존 관계가 생겨서 관계를 깔끔하게 정리하기가 어렵기 때문. 이 때 임시 변수를 질의 함수로 바꿔서 변수를 줄이면 훨씬 수월하게 리팩터링을 할 수 있다.
36 |
37 | ### ⚙️ 절차
38 |
39 | 1. 변수를 사용하기 전에 값을 결정하는지, 변수를 사용할 때마다 계산 로직이 매번 다른 값을 반환하는지 확인한다.
40 | 2. 읽기 전용으로 만들 수 있는 변수는 읽기 전용으로 만든다.
41 | 3. 테스트를 한다.
42 | 4. 변수 대입문을 함수로 추출한다.
43 | 5. 테스트를 한다.
44 | 6. 변수 인라인하기로 임시 변수를 제거한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/return-modified-value.md:
--------------------------------------------------------------------------------
1 | # 11.11 수정된 값 반환하기 (Return Modified Value)
2 |
3 | 데이터가 변수에 저장되어있고 다른 함수가 해당 변수에 접근해 값을 수정할 때, 함수가 변수에 접근하지 않고 수정되는 값을 반환시키게 해 변수의 값이 수정되는 부분을 명확히 분리시키는 리팩토링.
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```js
10 | let paymentRequiredCash = 0;
11 | calculatePaymentRequiredCash();
12 |
13 | function calculatePaymentRequiredCash() {
14 | for (let history of order.paymentHistories) {
15 | if (history.method !== PaymentMethod.CASH) {
16 | continue;
17 | }
18 | paymentRequiredCash += (history.type === PaymentType.DEPOSIT ? 1 : -1) * history.amount;
19 | }
20 | }
21 | ```
22 |
23 | ### 😍 To be
24 |
25 | ```js
26 | const paymentRequiredCash = calculatePaymentRequiredCash();
27 |
28 | function calculatePaymentRequiredCash() {
29 | let paymentRequiredCash = 0;
30 |
31 | for (let history of order.paymentHistories) {
32 | if (history.method !== PaymentMethod.CASH) {
33 | continue;
34 | }
35 | paymentRequiredCash += (history.type === PaymentType.DEPOSIT ? 1 : -1) * history.amount;
36 | }
37 |
38 | return paymentRequiredCash;
39 | }
40 | ```
41 |
42 | ### 📋 상세
43 |
44 | 가변형 변수의 가장 큰 단점은 해당 변수의 scope가 커지면 커질수록 어디서 이 값이 추적되는지 알기 힘들다는 것이다. 데이터 변경 흐름의 추적은 안그래도 어려운 일인데, 변수를 수정하는 함수가 여기저기 흩어져 있다면 코드를 이해하는 것은 더욱 어렵다.
45 | 이를 해결하기 위해 데이터가 갱신되는 로직을 작은 함수에 넣고, 값의 갱신이 끝날 때 값을 반환하게 하면 데이터 변경 흐름 추적이 조금 더 쉬워진다.
46 |
47 | 이 리팩터링은 값 하나를 계산하는 분명한 목적이 있는 함수에는 효과적이나, 여러 개를 갱신하는 함수에는 효과적이지 않다. 다른 리팩터링(함수 옮기기)의 준비 작업으로도 좋다.
48 |
49 | ### ⚙️ 절차
50 |
51 | 1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 하고, 테스트한다.
52 | 2. 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언하고, 테스트한다.
53 | * 이 값이 의도대로 이뤄졌는지 검사하고 싶으면 호출자에서 초깃값을 수정해본다. 제대로 됐으면 수정이 무시될 것이다.
54 | 3. 계산이 선언과 동시에 이뤄지도록 통합하고, 테스트한다.
55 | * 변수 선언에 함수 실행을 대입하라는 의미.
56 | * 언어가 지원하면 변수를 불변으로 지정한다.
57 | 4. 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꾸고, 테스트한다.
58 |
59 | ## 📝 메모
60 |
61 | ### 😍 반복문을 파이프라인으로 변경해 변수를 완전히 제거한 To be
62 |
63 | 작은 함수로 데이터 변경 로직을 몰아넣는다면, 함수의 수정 방향이 명확하게 보이기도 한다. 실제로 위의 예제는 다른 리팩토링을 적용해 이렇게 변경하여 변수를 완전히 제거할 수 있다.
64 |
65 | ```js
66 | const paymentRequiredCash = calculatePaymentRequiredCash();
67 |
68 | function calculatePaymentRequiredCash() {
69 | return order.paymentHistories
70 | .filter(history => history.method === PaymentMethod.CASH)
71 | .reduce(
72 | (sum, history) =>
73 | sum += (history.type === PaymentType.DEPOSIT ? 1 : -1) * history.amount
74 | , 0
75 | );
76 | }
77 | ```
78 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/separate-query-from-modifier.md:
--------------------------------------------------------------------------------
1 | # 11.1 질의 함수와 변경 함수 분리하기
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```js
8 | function getTotalOutstandingAndSendBill() {
9 | const result = customer.invoices.reduce(
10 | (total, each) => each.amount + total,
11 | 0
12 | );
13 | sendBill();
14 | return result;
15 | }
16 | ```
17 |
18 | ### 😍 To be
19 |
20 | ```js
21 | function totalOutstanding() {
22 | return customer.invoices.reduce((total, each) => each.amount + total));
23 | }
24 | function sendBill() {
25 | emailGateway.send(formatBill(customer));
26 | }
27 | ```
28 |
29 | ### 📋 상세
30 |
31 | 외부에서 관찰할 수 있는 겉보기 부수 효과(Observable Side Effect)가 전혀 없이 값을 반환해주는 순수 함수를 추구해야 한다. 이런 함수는 언제든 호출할 수 있고 옮기기도 자유로우며 테스트하기도 쉽다. 겉보기 부수 효과가 있는 함수와 없는 함수를 명확히 구분하기 위해서는 "질의 함수는 모두 부수 효과가 없어야 한다"는 규칙을 따르는것이 좋다. 이를 **명령-질의 분리**라고 한다.
32 |
33 | 이때, 그냥 부수 효과가 아닌 겉보기 부수 효과라고 한 이유는 값을 빠르게 반환하기 위해서 캐싱하는 함수도 객체의 상태를 변경하지만 객체의 상태 변화를 밖에서 관찰할 수 없는 함수이기 때문이다.
34 |
35 | ### ⚙️ 절차
36 |
37 | 1. 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
38 | 2. 새 질의 함수에서 부수효과를 모두 제거한다.
39 | 3. 정적 검사를 수행한다.
40 | 4. 원래 함수(변경함수)를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값으로 사용한다면 질의함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할 때마다 테스트한다.
41 | 5. 원래 함수에서 질의 관련 코드를 제거한다.
42 | 6. 테스트한다.
43 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/split-loop.md:
--------------------------------------------------------------------------------
1 | # 8.7 반복문 쪼개기(Split Loop)
2 |
3 | 두 가지 이상의 일을 수행하는 반복문을 각각의 반복문으로 분리하는 기법.
4 |
5 | 
6 |
7 | ## 🗣 설명
8 |
9 | ### 🧐 As is
10 |
11 | ```jsx
12 | let averageAge = 0;
13 | let totalSalary = 0;
14 | for (const p of people) {
15 | averageAge += p.age;
16 | totalSalary += p.salary;
17 | }
18 | averageAge = averageAge / people.length
19 | ```
20 |
21 | ### 😍 To be
22 |
23 | ```jsx
24 | let totalSalary = 0;
25 | for (const p of people) {
26 | totalSalary += p.salary;
27 | }
28 |
29 | let averageAge = 0;
30 | for (const p of people) {
31 | averageAge += p.age;
32 | }
33 | averageAge = averageAge / people.length
34 | ```
35 |
36 | ### 📋 상세
37 |
38 | 하나의 반복문 안에서 여러 가지 일을 처리하는 경우에 아래의 문제가 발생할 수 있음. 반복문 쪼개기는 이 문제를 해결할 때 사용하는 기법.
39 |
40 | - 반복문을 수정해야 할 때마다, 반복문 안에서 일어나는 모든 일을 이해해야 한다.
41 | - 서로 다른 일이 하나의 함수 안에서 이뤄지고 있다는 신호일 수 있다.
42 | - 반복문 안에서 구조체나 지역 변수를 사용하고 있을 가능성이 높고, 이 경우 리팩터링을 하기 어려워진다.
43 |
44 | 성능에 문제는 없을까? 반복을 여러 번 수행하면 당연히 성능은 떨어진다. 하지만 성능이 큰 병목이 되지 않는 상황이라면, 성능 보다는 오히려 코드를 변경하기 쉬운 구조로 만드는 게 더 이득이 크다.
45 |
46 | ### ⚙️ 절차
47 |
48 | 1. 반복문을 복사한다.
49 | 2. 하나의 반복문이 하나의 관심사를 처리하도록 수정한다.
50 | 3. 테스트한다.
51 | 4. 각 반복문을 함수로 추출할지 검토한다.
--------------------------------------------------------------------------------
/study/refactoring/catalogs/split-variable.md:
--------------------------------------------------------------------------------
1 | # 9.1 변수 쪼개기(Split Variable)
2 |
3 | 변수에 할당이 여러번 이뤄진다면, 각각의 역할을 하나의 변수로 쪼개는 기법
4 |
5 | ## 🗣 설명
6 |
7 | ### 🧐 As is
8 |
9 | ```typescript
10 | let temp = 2 * (height + width);
11 | console.log(temp);
12 | temp = height * width;
13 | console.log(temp);
14 | ```
15 |
16 | ### 😍 To be
17 |
18 | ```typescript
19 | const perimeter = 2 * (height + width);
20 | console.log(perimeter);
21 | const area = height * width;
22 | console.log(area);
23 | ```
24 |
25 | ### 📋 상세
26 |
27 | 변수는 긴 표현식의 결과값을 쉽게 참조할 수 있도록 저장하려는 목적으로 사용된다. 따라서 한 변수에 여러번 할당이 이뤄지면 코드를 읽을 때 혼란을 주게 된다. 각 변수는 한가지의 역할만 담당하도록 분리되어야 한다. 예외적으로 한 변수에 여러번 다른 값을 할당하려는 목적으로 사용되는 경우가 있다. 이는 반복문에 쓰이는 루프 변수(loop variable)나 메서드 동작의 중간중간에 값을 보관하기 위한 용도인 수집변수(collecting variable)이다.
28 |
29 | ### ⚙️ 절차
30 | 1. 변수를 선언한 곳과 값을 처음 대입하는 곳에서 변수 이름을 바꾸고 불변으로 선언한다.
31 | 2. 이 변수에 두 번째로 값을 대입하는 곳 전까지 변수를 참조하는 모든 곳을 새로운 변수로 변경한다.
32 | 3. 두 번째로 대입하는 곳에서 변수를 다시 선언해준다.
33 | 4. 테스트 한 뒤 1~4를 반복한다.
34 |
--------------------------------------------------------------------------------
/study/refactoring/catalogs/substitute-algorithm.md:
--------------------------------------------------------------------------------
1 | # 7.9 알고리즘 교체하기(Substitute Algorithm)
2 | 문제를 훨씬 쉽게 해결할 수 있는 알고리즘으로 대상을 통째로 교체하는 방법
3 |
4 | ## 🗣 설명
5 | ### 🧐 As is
6 | ```typescript
7 | data[rowIndex].sort((a, b) => {
8 | if (a.startTime === undefined) {
9 | return -1;
10 | }
11 | if (b.startTime === undefined) {
12 | return 1;
13 | }
14 | if (a.startTime > b.startTime) {
15 | return 1;
16 | }
17 | if (a.startTime < b.startTime) {
18 | return -1;
19 | }
20 | return 0;
21 | });
22 | ```
23 |
24 | ### 😍 To be
25 | ```typescript
26 | data[rowIndex].sort((a, b) => {
27 | return (a.startTime || "").localeCompare(b.startTime || "");
28 | })
29 | ```
30 |
31 | ### 📋 상세
32 | 같은 기능을 제공하는 라이브러리를 찾았거나 좀 더 간결하고 쉬운 문제 해결방식을 찾았을 경우 로직을 통째로 쉬운 알고리즘으로 교체하는 경우이다. 쉬운 알고리즘으로 교체하면 다른 방식으로 동작하도록 알고리즘을 변경해야 할 경우에도 더 빠르게 대응할 수 있다.
33 |
34 | ### ⚙️ 절차
35 | 1. 교체할 코드를 하나의 함수에 모아 유닛 테스트를 작성한다.
36 | 2. 대체할 알고리즘을 준비해서 정적 검사를 수행한다.
37 | 3. 기존 알고리즘과 새 알고리즘의 결과를 비교, 테스트 한다.
38 | 4. 두 결과가 같을때까지 테스트하고 디버깅 한다.
39 |
40 | ## 📝 메모
41 | 알고리즘 교체는 도메인 로직을 잘 이해할수록 더 간단한 접근법을 찾기 쉽다는 점에서 기존의 리팩터링 기법들과 차이가 있다. 이미 구현이 완료되었고, 작은 단위로 잘 나눠져 있더라도 다시 한번 더 간결한 알고리즘을 고민해 볼 여지를 갖게 한다는 점에서 인상 깊은 기법이다.
42 | ### ⚠️ 주의할 점
43 | - 알고리즘을 교체하기 전, 메서드를 가능한 작은 단위로 나눠서 간소화하는 작업을 권장
44 | - 교체할 대상에 대한 테스트를 반드시 작성해서 전, 후 결과가 달라지지 않는지 엄격히 테스트 해야 한다.
45 |
--------------------------------------------------------------------------------
/study/refactoring/index.md:
--------------------------------------------------------------------------------
1 | # 리팩터링
2 |
3 | ## 리팩터링 카탈로그
4 |
5 | 리팩터링 기본
6 |
7 | - [함수 추출하기](./catalogs/extract-function.md)
8 | - [함수 인라인하기](./catalogs/inline-function.md)
9 | - 변수 추출하기
10 | - 변수 인라인하기
11 | - [함수 선언 바꾸기](./catalogs/change-function-declaration.md)
12 | - 변수 캡슐화하기
13 | - [변수 이름 바꾸기](./catalogs/rename-variable.md)
14 | - [매개변수 객체 만들기](./catalogs/introduce-parameter-object.md)
15 | - 여러 함수를 클래스로 묶기
16 | - 여러 함수를 변환 함수로 묶기
17 | - 단계 쪼개기
18 |
19 | 캡슐화
20 |
21 | - [레코드 캡슐화하기](./catalogs/encapsulate-record.md)
22 | - [컬렉션 캡슐화하기](./catalogs/encapsulate-collection.md)
23 | - [기본형을 객체로 바꾸기](./catalogs/replace-primitive-with-object.md)
24 | - [임시 변수를 질의 함수로 바꾸기](./catalogs/replace-temp-with-query.md)
25 | - [클래스 추출하기](./catalogs/extract-class.md)
26 | - [클래스 인라인하기](./catalogs/inline-class.md)
27 | - [위임 숨기기](./catalogs/hide-delegate.md)
28 | - [중개자 제거하기](./catalogs/remove-intermediary.md)
29 | - [알고리즘 교체하기](./catalogs/substitute-algorithm.md)
30 |
31 | 기능 이동
32 |
33 | - [함수 옮기기](./catalogs/move-function.md)
34 | - [필드 옮기기](./catalogs/move-field.md)
35 | - [문장을 함수로 옮기기](./catalogs/move-statements-into-function.md)
36 | - [문장을 호출한 곳으로 옮기기](./catalogs/move-statements-to-callers.md)
37 | - [인라인 코드를 함수 호출로 바꾸기](./catalogs/replace-inline-code-with-function-call.md)
38 | - 문장 슬라이드하기
39 | - [반복문 쪼개기](./catalogs/split-loop.md)
40 | - [반복문을 파이프라인으로 바꾸기](./catalogs/replace-loop-with-pipeline.md)
41 | - [죽은 코드 제거하기](./catalogs/remove-dead-code.md)
42 |
43 | 데이터 조직화
44 |
45 | - [변수 쪼개기](./catalogs/split-variable.md)
46 | - [필드 이름 바꾸기](./catalogs/ename-field.md)
47 | - [파생 변수를 질의 함수로 바꾸기](./catalogs/replace-derived-variable-with-query.md)
48 | - [참조를 값으로 바꾸기](./catalogs/change-reference-to-value.md)
49 | - [값을 참조로 바꾸기](./catalogs/change-value-to-reference.md)
50 | - [매직 리터럴 바꾸기](./catalogs/replace-magic-literal.md)
51 |
52 | 조건부 로직 간소화
53 |
54 | - [조건문 분해하기](./catalogs/decompose-conditional.md)
55 | - [조건식 통합하기](./catalogs/consolidate-conditional-expression.md)
56 | - [중첩 조건문을 보호 구문으로 바꾸기](./catalogs/replace-nested-conditional-with-guard-clasuses.md)
57 | - [조건부 로직을 다형성으로 바꾸기](replace-conditional-with-polymorphism.md)
58 | - [특이 케이스 추가하기](./catalogs/introduce-special-case.md)
59 | - [어서션 추가하기](./catalogs/introduce-assertion.md)
60 | - [제어 플래그를 탈출문으로 바꾸기](./catalogs/replace-control-flag-with-break.md)
61 |
62 | API 리팩터링
63 |
64 | - [질의 함수와 변경 함수 분리하기](./catalogs/separate-query-from-modifier.md)
65 | - [함수 매개변수화하기](./catalogs/parameterize-function.md)
66 | - [플래그 인수 제거하기](./catalogs/remove-flag-argument.md)
67 | - [객체 통째로 넘기기](./catalogs/preserve-whole-object.md)
68 | - [매개변수를 질의 함수로 바꾸기](./catalogs/replace-parameter-with-query-function.md)
69 | - [질의 함수를 매개변수로 바꾸기](./catalogs/replace-query-with-parameter.md)
70 | - [세터 제거하기](./catalogs/remove-setting-method.md)
71 | - [생성자를 팩터리 함수로 바꾸기](./catalogs/replace-constructor-with-factory-function.md)
72 | - [함수를 명령으로 바꾸기](./catalogs/replace-function-with-command.md)
73 | - [명령을 함수로 바꾸기](./catalogs/replace-command-with-function.md)
74 | - [수정된 값 반환하기](./catalogs/return-modified-value.md)
75 | - [오류 코드를 예외 코드로 바꾸기](./catalogs/replace-error-code-with-exception.md)
76 | - [예외를 사전확인으로 바꾸기](./catalogs/replace-exception-handling-with-pre-checking.md)
77 |
78 | 상속 다루기
79 |
80 | - [메서드 올리기](./catalogs/pull-up-method.md)
81 | - [필드 올리기](./catalogs/pull-up-field.md)
82 | - [생성자 본문 올리기](./catalogs/pull-up-constructor-body.md)
83 | - [메서드 내리기](./catalogs/push-down-method.md)
84 | - [필드 내리기](./catalogs/push-down-field.md)
85 | - [타입 코드를 서브클래스로 바꾸기](./catalogs/replace-type-code-with-subclasses.md)
86 | - 서브클래스 제거하기
87 | - [슈퍼클래스 추출하기](./catalogs/extract-superclass.md)
88 | - [계층 합치기](./catalogs/collapse-hierarchy.md)
89 | - [서브클래스를 위임으로 바꾸기](./catalogs/replace-subclass-with-delegate.md)
90 | - [슈퍼클래스를 위임으로 바꾸기](./catalogs/replace-superclass-with-delegate.md)
91 |
92 | ## 리팩터링 적용 사례
93 |
94 | - [[Automata] 클래스 구조 변경](./practices/automata-complex-class.md)
95 | - [[당일배송 Admin] AuthStore 리팩터링](./practices/refactor-authstore-in-vroong-urban-web.md)
96 | - [[당일배송 Admin] TransportOrdersAddStore 리팩터링](./practices/urban-transport-orders-add-store.md)
97 | - [[메쉬원] EditableEditorPolygon & DrawingEditorPolygon 리팩터링](./practices/region-polygon-class.md)
98 | - [[메쉬원] UserTable.tsx 내의 handleOnClickAdd 함수 리팩토링](./practices/user-table-handle-click-add.md)
99 | - [[메쉬원] 공통 컴포넌트 생성 하기(관리 내역, 담당자 연락처)](./practices/create-common-component.md)
100 |
--------------------------------------------------------------------------------
/study/refactoring/practices/automata-complex-class.md:
--------------------------------------------------------------------------------
1 | # [Automata] 클래스 구조 변경
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```typescript
8 | class Automata {
9 | // 사장님사이트 접속 -> 로그인 -> JQuery Selector를 사용하여 값 추출 -> 앱으로 전송
10 | }
11 |
12 | class Extractor {
13 | // 주문 링크(문자) 접속 -> Jquery Selector를 사용하여 값 추출 -> 앱으로 전송
14 | }
15 |
16 | class BaeminAutomata extends Automata {
17 | /* ... */
18 | }
19 | class BaedaltongAutomata extends Automata {
20 | /* ... */
21 | }
22 | class YogiyoExtractor extends Extractor {
23 | /* ... */
24 | }
25 |
26 | // 여기서부터는 Webpack Entry
27 | // Baemin.ts
28 | window.automata = new BaeminAutomata();
29 |
30 | // Baedaltong.ts
31 | window.automata = new BaedaltongAutomata();
32 |
33 | // Yogiyo.ts
34 | window.extractor = new YogiyoExtractor();
35 | ```
36 |
37 | ### 😍 To be
38 |
39 | ```typescript
40 | abstract class Crawler {
41 | public abstract accessPage(): void;
42 | public abstract login(): void;
43 | public abstract crawl(): void;
44 | public abstract validate(): void;
45 | }
46 |
47 | class BaeminCrawler extends Crawler {
48 | /* abstract를 구현 */
49 | }
50 | class BaedaltongCrawler extends Crawler {
51 | /* abstract를 구현 */
52 | }
53 | class YogiyoCrawler extends Crawler {
54 | /* abstract를 구현 */
55 | }
56 |
57 | // Entry
58 | class Runner {
59 | private crawler: Crawler;
60 |
61 | constructor(type: string) {
62 | this.crawler = createCrawler(type); // 팩토리 메서드
63 | }
64 |
65 | public run() {
66 | try {
67 | crawler.accessPage();
68 | crawler.login();
69 | crawler.crawl();
70 | crawler.validate();
71 | } catch {
72 | // 오류 전송 로직
73 | Sentry.captureException();
74 | }
75 | }
76 | }
77 | ```
78 |
79 | ### 📋 상세(현재 구조)
80 |
81 | - Baemin과 Baedaltong은 사장님사이트로 접근 후 로그인을 거쳐 정보를 추출한다.
82 | - 반면 Yogiyo는 상점주의 스마트폰으로 오는 배송 정보(URL)가 포함된 문자를 클릭하여 정보를 추출한다.
83 | - 두 구조가 분리되어 있어서 전자는 Automata로, 후자는 Extractor로 분리되어 있는 상황.
84 | - 프로덕트 별로 Entry를 따로 만들어서 빌드할 때마다 3개의 결과물이 등장한다.
85 | - 공통 로직이 모두 클래스 내에 따로 구현되어 있다보니 동일한 로직을 수정하더라도 3개의 변경이 필요하다.
86 |
87 | ex) Sentry, 앱으로 Callback을 전송
88 |
89 | ### ✨목표
90 |
91 | - 크롤링을 하는 클래스는 크롤링만 하도록 유도하자.
92 |
93 | - 하나의 Entry에서 타입에 맞는 크롤러를 생성하도록 변경하자.(이후 다른 제품에 대한 크롤링이 필요하더라도 유기적으로 추가 가능)
94 |
95 | - Sentry로 에러를 전송하거나, 앱으로 결과 Callback을 보내는 별도의 비즈니스 로직은 따로 처리하자.
96 |
97 | ### ⚙️ 절차
98 |
99 | 1. 모든 크롤링을 구현할 수 있도록 돕는 `Crawler` 추상 클래스를 생성한다.
100 | 2. 공통(비즈니스) 로직을 관리하고 크롤링을 실행하는 `Runner` 클래스를 만든다.
101 | 3. 프로덕트 별로 `Crawler`를 구현한다. 새로운 프로덕트별로 이 과정은 반복된다.
102 | (1) 기존 메서드와 로직을 적절한 위치의 메서드(`accessPage`, `login`, ...)로 옮긴다. 이 과정에서 사용하지 않는 메서드는 빈 메서드로 남겨놓는다.
103 |
104 | (2) 로직을 옮기다가 비즈니스 로직을 발견한 경우, `Runner`로 옮기고 제거한다.
105 |
106 | (3) 각 기능을 옮길때마다 기존에 작동하던 결과와 같은지 테스트한다.
107 |
108 | (4) 테스트에 성공했다면 기존 클래스는 제거한다.
109 |
110 | ## 📝메모
111 |
112 | ### Thanks to
113 |
114 | - 설계에 많은 도움을 주신 훈민님, 용준님 감사합니다.
115 |
116 | ### 소감
117 |
118 | 아는 내용, 당연하다고 생각하는 내용이라도 어디에 적용해야할지, 어떻게 적용해야할지는 직접 경험해보지 않으면 알 수 없다.
119 |
120 | 이런 기회를 만들어준 준모님께 매우 감사드립니다.
121 |
--------------------------------------------------------------------------------
/study/refactoring/practices/create-common-component.md:
--------------------------------------------------------------------------------
1 | # [메쉬원] 공통 컴포넌트 생성 하기(관리 내역, 담당자 연락처)
2 |
3 | ## **🗣 설명**
4 |
5 | ### **🧐 As is**
6 |
7 | ```html
8 | 관리 내역
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
30 | 입력
31 |
32 |
33 |
34 |
35 |
36 | ...
37 |
38 | 상점 담당자 정보
39 |
40 |
41 |
46 |
47 |
48 |
54 |
55 |
56 | ```
57 |
58 | ### **😍 To be**
59 |
60 | ### **📋 상세**
61 |
62 | - 공통으로 사용되는 `관리 내역`(상점 관리, OMS), `담당자 연락망`(OMS)의 공통 컴포넌트화
63 |
64 | ### ✨목표
65 |
66 | - `관리 내역`, `담당자 연락망` 항목을 공통 컴포넌트화 하여 코드수를 줄이고 관리한다.
67 |
68 | ### **⚙️ 절차**
69 |
70 | 1. 관리 내역과 담당자 연락망이 공통 형태로 사용되는 화면을 찾는다.(현재 찾은 페이지는 상점관리, OMS)
71 | 2. 컴포넌트 생성 시 필요한 Parameter를 추출한다.
72 | 3. 관리 내역, 담당자 연락망 컴포넌트를 생성한다.
73 | 4. 원본 코드에서 해당 부분을 주석처리하고 새로 만든 컴포넌트로 바꿔준다.
74 | 5. 각 페이지에서 테스트를 하고 문제가 없다면 주석처리 한 원본 코드를 삭제한다.
75 |
--------------------------------------------------------------------------------
/study/refactoring/practices/refactor-authstore-in-vroong-urban-web.md:
--------------------------------------------------------------------------------
1 | # [당일배송] AuthStore 리팩터링
2 |
3 | ## 🗣 설명
4 |
5 | ### 🧐 As is
6 |
7 | ```tsx
8 | class AuthStore {
9 | // 토큰 관리를 위한 필드 & 메서드
10 | private tokenTomer: number;
11 | public startUpdateTokenTimer() { ... }
12 | public loadAccessToken() { ... }
13 |
14 | // 브라우저 storage API를 사용하는 메서드들
15 | private sessionEventListener() {...}
16 | private watchStorageEvent() {...}
17 | private unwatchStorageEvent() {...}
18 |
19 | // UI 단에서 일어나는 action을 처리하기 위한 메서드
20 | public async redirectToUaaLogin() { ... }
21 | public async redirectToUaaLogout() { ... }
22 | }
23 | ```
24 |
25 | ### 😍 To be
26 |
27 | ### 📋 상세
28 |
29 | ```tsx
30 | class AuthStore {
31 | // UI 단에서 일어나는 action을 처리하기 위한 메서드
32 | public async redirectToUaaLogin() { ... }
33 | public async redirectToUaaLogout() { ... }
34 | }
35 |
36 | class TokenRepository {
37 | // 토큰 관리를 위한 필드 & 메서드
38 | private tokenTomer: number;
39 | private startUpdateTokenTimer() { ... }
40 | public loadAccessToken() { ... }
41 | }
42 |
43 | class AuthService {
44 | // 브라우저 storage API를 사용하는 메서드들
45 | private sessionEventListener() {...}
46 | public watchStorageEvent() {...}
47 | public unwatchStorageEvent() {...}
48 | }
49 | ```
50 |
51 | - `AuthStore`에서 처리하는 기능은 크게 다음으로 나눌 수 있다.
52 | - 로그인, 로그아웃 처리
53 | - 액세스 토큰 fetch, 갱신, 저장, 폐기
54 | - 브라우저 스토리지 API로 탭 간 세션 동기화
55 | - 브라우저 스토리지에 사용자 데이터 저장, load 후 검증
56 | - `AuthStore`에서는 너무 많은 역할을 담당하고 있고, 여러가지 관심사가 섞여있다.
57 | - 관심사가 섞이고 비대해진 클래스는 쉽게 이해하기가 힘들다.
58 |
59 | ### ✨목표
60 |
61 | TMS 매니저 웹에서 개선된 설계를 참고한다.
62 |
63 | - `AuthStore`에서 토큰 관련 데이터, 메서드들은 **`TokenRepository`**로 분리한다.
64 | - `AuthStore`에서 스토리지 API와 관련된 것들을 **`AuthStorageService`**로 분리한다.
65 |
66 | ### ⚙️ 절차
67 |
68 | 1. 새로운 `TokenRepository`, `AuthStorageService` 클래스를 만든다.
69 | 2. `AuthStore` 에 `TokenRepository`, `AuthStorageService` 인스턴스를 담기 위한 2개 필드를 선언하고, `AuthStore` 생성자에서 필드들을 초기화 해준다.
70 | 3. `AuthStore`에서 토큰 관련된 필드들을 `TokenRepository`로 옮긴다.
71 | 4. 토큰 관련돤 필드들을 get/set 할 때는 `TokenRepository`에 있는 것을 사용하도록 `AuthStore`을 변경하고, 테스트한다.
72 | 5. `AuthStore`에서 토큰 관련된 메서드들을 `TokenRepository`로 옮긴다.
73 | 6. 토큰 관련된 메서드들을 사용할 때는 `TokenRepository`에 있는 것을 사용하도록 `AuthStore`을 변경하고, 테스트한다.
74 | 7. `TokenRepository` 분리 작업이 완료되면, 스토리지 관련 필드 & 메서드들을 `AuthStorageService`로 옮긴다. 옮길 때는 "2"~"6" 과정에서 옮긴 순서대로 진행한다.
75 |
76 | ## 📝메모
77 |
78 | ### 소감
79 |
80 | 회사안에서 이렇게 완주하는 스터디를 할 수 있다는 것이 놀랍다. 리팩터링 책을 한번 읽는다고 리팩터링에 대해서 완전히 이해하는 것은 힘들겠지만, 첫걸음을 뗄 수 있는 스터디가 되었다.
81 |
--------------------------------------------------------------------------------
/technical-debt/README.md:
--------------------------------------------------------------------------------
1 | # 기술부채 관리 전략
2 |
3 | # 0. 목차
4 |
5 | 1. [기술부채?](#1. 기술부채?)
6 | 2. [기본 전략](#2. 기본 전략)
7 | 3. [상세 프로세스](#3. 상세 프로세스)
8 |
9 | # 1. 기술부채?
10 |
11 | 기술부채는, 어떤 이유로 하지 못하고 뒤로 미루거나 수준을 낮춰서 해결한 기술적인 문제를 말합니다. 기술부채는 워드 커닝햄(Ward Cunningham)이 처음 사용한 용어로, 소프트웨어의 내적품질 수준에 따라 운영 단계에서 쏟아야 하는 노력의 크기가 달라진다는 걸 표현할 때 사용하는 용어입니다.
12 |
13 | 금융 부채를 지면 이자를 내듯이, 내적품질 수준이 떨어지는 소프트웨어는 운영 비용을 증가시킨다는 걸 은유적으로 표현한 개념입니다.
14 |
15 | - [디지털 탈바꿈 과제: 기술부채(Technical Debt)](https://www.2e.co.kr/news/articleView.html?idxno=207765)
16 | - [TechnicalDebt](https://martinfowler.com/bliki/TechnicalDebt.html)
17 |
18 | # 2. 기본 전략
19 |
20 | 기술부채를 백로그에 모으고, 기술부채의 총량을 수치화하여, 정기적으로 꾸준히 조금씩 해소합니다.
21 |
22 | - 누구나, 언제든, 사소한 거라도 일단 백로그에 등록합니다.
23 | - 정기적으로 모여서 이슈를 리뷰하고 추정한 후 우선순위를 결정합니다.
24 | - 우선순위가 높은 티켓 부터 구체적인 처리방향(담당자, 처리방식)을 논의합니다.
25 |
26 | # 3. 상세 프로세스
27 |
28 | ## 백로그 등록
29 |
30 | 1. 기술부채 식별 즉시, 운영 프로젝트 백로그 티켓 등록 → 문제, 상세 설명, 제안 항목 필수 입력
31 | 2. 등록한 티켓은 기술부채 백로그 에픽과 연결
32 |
33 | ## 기술부채 관리 미팅
34 |
35 | 1. 한 달에 한 번 정기 진행 → 캘린더 일정 등록
36 | 2. 지난 한 달간의 기술부채 해소 노력 회고
37 | 3. 새로운 기술부채 백로그 리뷰
38 | - 추정
39 | - 우선순위 판단 후 정렬
40 | 4. 해결 범위 결정
41 | 5. 처리방식 논의 → 독립 진행 또는 과제 병합 후 진행 결정
42 | 6. 담당자 할당
43 | 7. 완료 목표일 지정
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | .history
--------------------------------------------------------------------------------
/tests/describe_fully.md:
--------------------------------------------------------------------------------
1 | # 명세는 테스트 코드를 충분히 설명할 수 있어야 합니다.
2 |
3 | 테스트의 명세는 테스트의 방향을 설정합니다. 테스트의 명세는 세 가지를 설명할 수 있어야 합니다.
4 |
5 | - 어떤 조건에서 테스트를 실행하나요?
6 | - 무엇을 테스트하나요?
7 | - 어떤 결과를 기대하나요?
8 |
9 | 위의 세 가지 중에 어느 하나라도 명세가 제대로 설명하지 못한다면, 테스트를 이해하기가 어려워집니다.
10 |
11 | ### 테스트를 설명하지 못하는 명세 예시
12 |
13 | 아래의 코드는 명세만 읽어서는 테스트의 목적을 이해하기가 어렵습니다. 기본 케이스의 의미를 알 수 없고, 기대하는 결과에 담긴 의도를 이해하기도 어렵습니다. 심지어 여러 행위를 하나의 테스트 케이스에서 검증하고 있어서 단언이 장황합니다.
14 |
15 | ```typescript
16 | describe("normalizeToServerTimeTable", () => {
17 | it("기본 케이스", () => {
18 | // given
19 | const data = {
20 | "0": [{ startTime: undefined, endTime: undefined }],
21 | "1": [{ startTime: ":", endTime: ":" }],
22 | "2": [{ startTime: "00:00", endTime: "23:59" }],
23 | "3": [{ startTime: "00:00", endTime: "24:00" }],
24 | "4": [{ startTime: "00:00", endTime: "25:00" }],
25 | };
26 |
27 | // when
28 | const output = normalizeToServerTimeTable(data);
29 |
30 | // then
31 | expect(output["0"]).toEqual([]);
32 | expect(output["1"]).toEqual([]);
33 | expect(output["2"]).toEqual([{ startTime: "00:00", endTime: "23:59" }]);
34 | expect(output["3"]).toEqual([{ startTime: "00:00", endTime: "23:59" }]);
35 | expect(output["4"]).toEqual([
36 | {
37 | startTime: "00:00",
38 | endTime: "00:00",
39 | },
40 | {
41 | startTime: "00:00",
42 | endTime: "23:59",
43 | },
44 | ]);
45 | });
46 | });
47 | ```
48 |
49 | ### 모호한 명세를 개선하기
50 |
51 | 이 문제를 해결하려면 명세와 테스트 코드 사이의 관계를 더 선명하게 만들어야 합니다. 요구사항을 문장으로 정리하여 "기본 케이스"라는 모호한 명세를 대체합시다.
52 |
53 | - 시작 시간과 종료 시간이 undefined이면 빈 배열 반환
54 | - 시작 시간과 종료 시간이 ':'이면 빈 배열을 반환
55 | - 종료 시간이 23:59이면 입력한 값을 유지
56 | - 종료 시간이 24:00이면 종료 시간을 23:59로 변경, 다음 날의 첫 번째 인덱스에 1분 추가
57 |
58 | 이제 각 문장을 명세로 하는 테스트 케이스를 만들어서 기존의 테스트를 해체합니다.
59 |
60 | ```typescript
61 | describe("normalizeToServerTimeTable", () => {
62 | let dummyTimeTable;
63 |
64 | it("일요일의 시작 시간과 종료 시간이 undefined이면 빈 배열을 반환해야 한다.", () => {
65 | // given
66 | dummyTimeTable = {
67 | [WeekDay.SUNDAY]: [{ startTime: undefined, endTime: undefined }],
68 | };
69 |
70 | // when
71 | const output = normalizeToServerTimeTable(dummyTimeTable);
72 |
73 | // then
74 | expect(output[WeekDay.SUNDAY]).toEqual([]);
75 | });
76 |
77 | it("월요일의 시작 시간과 종료 시간이 ':'이면 빈 배열을 반환해야 한다.", () => {
78 | // given
79 | dummyTimeTable = {
80 | [WeekDay.MONDAY]: [{ startTime: ":", endTime: ":" }],
81 | };
82 |
83 | // when
84 | const output = normalizeToServerTimeTable(dummyTimeTable);
85 |
86 | // then
87 | expect(output[WeekDay.MONDAY]).toEqual([]);
88 | });
89 |
90 | it("화요일의 종료 시간이 23:59이면 입력한 값을 유지해야 한다.", () => {
91 | // given
92 | dummyTimeTable = {
93 | [WeekDay.TUESDAY]: [{ startTime: "00:00", endTime: "23:59" }],
94 | };
95 |
96 | // when
97 | const output = normalizeToServerTimeTable(dummyTimeTable);
98 |
99 | // then
100 | expect(output[WeekDay.TUESDAY]).toEqual([
101 | { startTime: "00:00", endTime: "23:59" },
102 | ]);
103 | });
104 |
105 | it("수요일의 종료 시간이 24:00이면 종료 시간을 23:59로 변경하고 목요일의 첫 번째 인덱스에 1분을 추가해야 한다.", () => {
106 | // given
107 | dummyTimeTable = {
108 | [WeekDay.WEDNESDAY]: [{ startTime: "00:00", endTime: "24:00" }],
109 | };
110 |
111 | // when
112 | const output = normalizeToServerTimeTable(dummyTimeTable);
113 |
114 | // then
115 | const timesOfWednesday = [{ startTime: "00:00", endTime: "23:59" }];
116 | const timesOfThursday = [{ startTime: "00:00",endTime: "00:00" }];
117 | expect(output[WeekDay.WEDNESDAY]).toEqual(timesOfWednesday);
118 | expect(output[WeekDay.THURSDAY][0]).toEqual(timesOfThursday);
119 | });
120 |
121 | .
122 | .
123 | .
124 |
125 | });
126 | ```
127 |
128 | 명세와 코드의 간극이 줄어 훨씬 코드를 이해하기가 쉽지 않나요?
--------------------------------------------------------------------------------
/tests/do_not_dependent_on_ui_structure.md:
--------------------------------------------------------------------------------
1 | # UI의 구조에 의존하지 마세요.
2 |
3 | UI 구조는 테스트에게 숨겨야 할 세부 구현 사항입니다. 테스트가 상세한 UI 구조를 알수록 구현 코드와 테스트 코드 사이의 의존성은 강해집니다.
4 |
5 | ### UI 구조에 의존하는 테스트
6 |
7 | 아래의 테스트는 `div > button`이라는 셀렉터를 사용함으로써 UI 구조에 의존합니다.
8 |
9 | ```typescript
10 | export default function Card(props) {
11 | return [1, 2, 3, 4].map(choice => (
12 |
59 | ));
60 | }
61 |
62 | it("두 번째 버튼을 클릭하면 onSelect를 호출하며 전달인자로 2를 넘겨야 한다.", () => {
63 | // given
64 | const container = document.createElement("div");
65 | const onSelect = jest.fn();
66 | act(() => {
67 | render(, container);
68 | });
69 |
70 | // when
71 | act(() => {
72 | container
73 | .querySelector("[data-testid=2]")
74 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
75 | });
76 |
77 | // then
78 | expect(onSelect).toHaveBeenCalledWith(2);
79 | });
80 | ```
81 |
82 | `data-testid`는 셀렉터를 지정할 수 있는 사용자 정의 속성입니다. 셀렉팅 하려는 대상에 미리 표시를 해두는 방식입니다. 표시에 의존하지만 구조에 의존하지 않을 수 있다는 점에서 변경에 조금 더 유리합니다.
83 |
84 | 테스트만을 위한 속성을 추가해야 하는 게 아쉽지만 테스트가 취약해지는 것 보다는 낫습니다. React가 `data-testid`를 제공하기 전에도 일부 개발자는 `data-attribute`를 이용해서 이 문제를 해결하였습니다. 관용적으로 사용하던 해결 방식을 라이브러리가 인정한 사례입니다.
--------------------------------------------------------------------------------
/tests/do_not_miss_await_keyword.md:
--------------------------------------------------------------------------------
1 | # async 함수를 테스트 할 때는 await를 반드시 붙여주세요.
2 |
3 | 테스트에서 async 함수를 호출할 때는 await를 붙여주는 게 좋습니다. await를 명시하지 않으면 테스트 프레임워크가, 실패하는 테스트를 통과된 것으로 판단해버릴 수 있습니다.
4 |
5 | ### 실패한 테스트를 통과한 것으로 간주하는 예
6 |
7 | 아래와 같은 테스트 코드가 있다고 합시다. productList.fetch는 비동기 함수입니다. 제품 목록을 서버에 요청했지만 가져올 목록이 없을 때 productList를 isEmpty 상태로 변경하는 게 이 테스트의 목적입니다.
8 |
9 | ```typescript
10 | describe("제품 목록을 요청했는데", () => {
11 | it("제품 목록이 비었다면 isEmpty가 false이어야 한다.", () => {
12 | // given
13 | const productList = new ProductList();
14 |
15 | // when
16 | productList.fetch(); // 비동기 코드
17 |
18 | // then
19 | expect(productList.isEmpty).toBeFalsy();
20 | });
21 | });
22 | ```
23 |
24 | fetch를 실행했습니다. 비동기 요청을 수행중이고 아직 실행이 완료되지 않은 상황이라면 어떨까요? 그리고 isEmpty의 기본값이 false라면? productList.fetch()를 요청하자마자 제어는 단언으로 이동을 해버립니다. 그리고 테스트가 통과를 할 겁니다.
25 |
26 | 바보 같은 실수라고 생각하겠지만 쉽게 할 수 있는 실수입니다.
27 |
28 | ### async 키워드를 붙여서 해결하기
29 |
30 | 아래와 같이 호출하는 async 함수 앞에 await 키워드를 붙여주면 이 문제를 방지할 수 있습니다.
31 |
32 | ```typescript
33 | describe("제품 목록을 요청했는데", () => {
34 | it("제품 목록이 비었다면 isEmpty가 false이어야 한다.", async () => {
35 | // given
36 | const productList = new ProductList();
37 |
38 | // when
39 | await productList.fetch();
40 |
41 | // then
42 | expect(productList.isEmpty).toBeFalsy();
43 | });
44 | });
45 | ```
46 |
47 | await 대신에 Promise를 사용한다면 반드시 Promise를 반환해줘야 합니다. 그래야 테스트 프레임워크가 Promise 체인에서 발생하는 예외를 인지할 수 있습니다.
48 |
49 | ```typescript
50 | describe("제품 목록을 요청했는데", () => {
51 | it("제품 목록이 비었다면 isEmpty가 false이어야 한다.", () => {
52 | // given
53 | const productList = new ProductList();
54 |
55 | // when
56 | return productList
57 | .fetch()
58 | .then(() => {
59 | // then
60 | expect(productList.isEmpty).toBeFalsy();
61 | })
62 | });
63 | });
64 | ```
--------------------------------------------------------------------------------
/tests/do_not_use_too_much_test_doubles.md:
--------------------------------------------------------------------------------
1 | # 테스트 대역(Test Double)을 무분별하게 사용하지 않습니다.
2 |
3 | ### 테스트 대역이란?
4 |
5 | 테스트 대역이란 실제 구현체를 테스트하기가 곤란할 때 대신 사용하는 대역을 뜻합니다. 마틴 파울러는 [자신의 글](https://martinfowler.com/articles/mocksArentStubs.html)에서 테스트 대역을 Dummy, Fake, Stub, Mock의 네 가지로 구분한 적이 있습니다.
6 |
7 | - Dummy: 보통 빈 몸체를 가지며 아무 것도 하지 않음. 컴파일이나 참조 에러를 막는 용도.
8 | - Fake: Dummy와 다르게 몸체가 있지만 거짓으로 구현. 제품(Production) 레벨에서 쓰일 수는 없는 의사(Pseudo) 구현체.
9 | - Stub: 미리 정의한 응답을 일관성있게 반환하는 함수. 어떤 Stub은 호출과 응답을 기록하기도 함.
10 | - Mock: 입력에 대응하는 기대 결과를 동적으로 설정할 수 있는 개체나 함수.
11 |
12 | 어디까지나 참고할 기준일 뿐 각 테스트 대역 간의 경계는 분명하지 않습니다. 사람마다 다르게 정의하기도 합니다. 호출과 응답을 기록하는 테스트 대역을 어떤 사람은 Spy로 구분해서 부르기도 합니다.
13 |
14 | 풍부한 테스트 대역을 제공하는 JavaScript 라이브러리로 [Sinon.js](https://sinonjs.org/)가 있습니다.
15 |
16 | ### 테스트 대역을 사용할 때 주의할 점!
17 |
18 | 테스트 대역을 사용하면 테스트를 하기 곤란한 상황을 간단하게 벗어날 수 있는 이점이 있습니다.
19 |
20 | 하지만 공짜는 없습니다. 테스트 대역은 구현 코드의 대체물을 만듦으로써 테스트 코드와 구현 코드 사이의 의존성을 높입니다. 그리고 중복 코드를 작성하게 만듭니다. 구현 코드에 변경이 생기면 테스트 대역도 변경을 해줘야 하기에 번거로운 일이 됩니다.
21 |
22 | 실제로 동기화를 해야하는 걸 까먹고 테스트를 방치하는 걸 현업에서 대단히 여러 번 보았습니다.
23 |
24 | 테스트 대역은 개발자를 안티 패턴으로 유도합니다. Spy로 내부 의존 개체의 호출 횟수를 확인하는 경우 처럼, 테스트 대상의 인터페이스가 아닌 상세 구현을 테스트하는 경우가 그렇습니다.
25 |
26 | 테스트 대역은 정말 필요한 순간에만 최소한으로 사용하세요!
27 |
--------------------------------------------------------------------------------
/tests/given_when_then.md:
--------------------------------------------------------------------------------
1 | # 1. Given/When/Then에 맞춰 테스트 코드를 작성합니다.
2 |
3 | Given/When/Then 템플릿은 어떤 상황(Given)에서, 무엇을 하면(When) 무엇이 되어야 한다(Then)"라는 논리에 맞춰 코드를 작성하도록 유도합니다. 템플릿은 테스트를 다음의 세 구역으로 나눕니다.
4 |
5 | - Given: 테스트를 수행하는 조건 또는 상황
6 | - When: 테스트를 할 행위
7 | - Then: 기대하는 결과
8 |
9 | ### Given/When/Then에 맞춰 작성한 예제
10 |
11 | ```typescript
12 | it("주문대기 건수(awaitingOrdersCount)가 1이면 주문대기 상태 또한 1이어야 한다.", () => {
13 | // given
14 | const awaitingOrdersCount = 1;
15 | const orderCountDto: OrderCountDto = {
16 | awaitingOrdersCount,
17 | };
18 |
19 | // when
20 | const output = convertOrderCount(orderCountDto);
21 |
22 | // then
23 | expect(output[OrderStatus.AWAITING]).toBe(1);
24 | });
25 | ```
26 |
27 | 이 템플릿을 이용하면 테스트를 작성하기 전에 테스트 하려는 대상과 방향을 정리할 수 있고, 코드가 놓인 맥락을 조금 더 선명하게 만들어 가독성을 향상시킬 수 있습니다.
28 |
29 | 이 외에도 비슷한 테스트 작성 스타일로 아래의 것들이 있습니다.
30 |
31 | - [Setup - Exercise - Verify - Teardown](http://xunitpatterns.com/Four%20Phase%20Test.html)
32 | - [Arrange - Act - Assert](https://xp123.com/articles/3a-arrange-act-assert/)
33 |
34 | 각 단계를 다른 단어로 표현할 뿐 의도하는 바는 같습니다.
35 |
36 | ### 너무 번거롭지 않아요?
37 |
38 | 이 템플릿에 맞춰 코드를 작성하는 게 익숙해지면 Given/When/Then 주석을 작성하는 게 번거로운 요식행위로 느껴질 수 있습니다. 주석 없이도 의미를 잘 전달할 수 있다면 형식에 너무 얽매이지 않아도 좋습니다. 동료와 상의해서 방향을 결정하세요.
39 |
40 |
--------------------------------------------------------------------------------
/tests/images/state-transition-tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/tests/images/state-transition-tree.png
--------------------------------------------------------------------------------
/tests/images/state-transition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meshkorea/front-end-engineering/5360d73c41883eb9a0878b78b52c017ef941e9a4/tests/images/state-transition.png
--------------------------------------------------------------------------------
/tests/index.md:
--------------------------------------------------------------------------------
1 | # 테스트 작성 가이드
2 |
3 | 테스트를 작성하면서 겪었던 경험을 토대로 좋은 테스트를 작성하기 위해서 생각해 보아야 할 내용을 안내합니다. 여기에 적힌 내용이 "진리"는 아니며 테스트 작성자가 처한 상황과 환경에 따라 다른 결론을 얻을 수도 있습니다. 새로운 아이디어가 생길 때마다 문서를 계속 업데이트 할 생각입니다.
4 |
5 |
6 |
7 | ### 이해하기 쉬운 테스트
8 |
9 | 1. [Given/When/Then에 맞춰 테스트 코드를 작성합니다.](./given_when_then.md)
10 | 2. [명세는 테스트 코드를 충분히 설명할 수 있어야 합니다.](./describe_fully.md)
11 | 3. [관련성을 기준으로, 테스트를 그룹으로 묶어주세요.](./group_by_relevance.md)
12 | 4. [테스트가 한 가지 일에만 관심을 갖게 해주세요.](./single_responsibility.md)
13 | 5. [지나친 잔소리는 좋지 않습니다.](./not_too_much_nipticking.md)
14 | 6. [테스트를 하는 게 의미가 있는 지점을 테스트합니다.](./test_meaningful_points.md)
15 |
16 | ### 견고한 테스트
17 |
18 | 1. [구현이 아닌 인터페이스를 테스트합니다.](./test_interface.md)
19 | 2. [UI의 구조에 의존하지 마세요.](./do_not_dependent_on_ui_structure.md)
20 | 3. [테스트 대역(Test Double)을 무분별하게 사용하지 않습니다.](./do_not_use_too_much_test_doubles.md)
21 | 4. [에러를 잡지(catch)말고 기대(expect)하세요.](./not_catch_exception_but_expect.md)
22 |
23 | ### 믿을 수 있는 테스트
24 |
25 | 1. [async 함수를 테스트 할 때는 await를 반드시 붙여주세요.](./do_not_miss_await_keyword.md)
26 | 2. [테스트 케이스를 충분히 작성하되, 엣지(Edge) 케이스도 생각하세요!](./write_enough_and_edge_cases.md)
27 | 3. [입력 케이스가 너무 많을 때는 경계를 테스트하세요.](./test_your_boundaries.md)
28 | 4. [테스트 케이스 설계 기법을 알아두면 좋아요.](./test_case_design_methods.md)
--------------------------------------------------------------------------------
/tests/not_catch_exception_but_expect.md:
--------------------------------------------------------------------------------
1 | # 에러를 잡지(catch)말고 기대(expect)하세요.
2 |
3 | 테스트 대상에서 에러가 발생했을 때 예외 처리를 잘 하고 있는지 테스트를 하고 싶을 때가 있습니다.
4 |
5 | ### 예외 처리 테스트
6 |
7 | ```typescript
8 | it("존재하지 않는 상품 ID를 받으면 Error 객체가 throw된다.", async () => {
9 | // given
10 | const product = new ProductService();
11 | try {
12 | // when
13 | await product.getProduct("1234");
14 | } catch (e) {
15 | // then
16 | expect((e as Error).message).toBe("product is not exist");
17 | }
18 | });
19 | ```
20 |
21 | when 절에서 에러가 발생하지 않으면 코드의 실행 흐름은 catch 블록으로 이동하지 않을 겁니다. catch 블록으로 제어가 이동하지 않으면 어떠한 단언문도 실행되지 않습니다. 어떠한 단언문도 실행되지 않으면 테스트는 성공한 것으로 간주됩니다.
22 |
23 | 이 테스트는 예외가 발생하는 걸 기대하고 있고, 예외가 발생하지 않는다면 테스트는 실패해야 합니다.
24 |
25 | ### fail 함수를 이용해서 개선하기
26 |
27 | 강제로 테스트를 실패하게 만드는 fail과 같은 함수를 이용할 수 있습니다.
28 |
29 | ```typescript
30 | it("존재하지 않는 상품 ID를 받으면 Error 객체가 throw된다.", async () => {
31 | // given
32 | const product = new ProductService();
33 | try {
34 | // when
35 | await product.getProduct("1234");
36 | fail(); // 위의 when 코드가 실패하지 않을 경우, 테스트 무조건 실패
37 | } catch (e) {
38 | // then
39 | expect((e as Error).message).toBe("product is not exist");
40 | }
41 | });
42 | ```
43 |
44 | ### Jest의 API 이용하기
45 |
46 | 위의 코드는 조금 장황합니다. Jest에서 제공하는 rejects와 toThrow를 이용하면 코드를 더 단순하게 만들 수 있습니다.
47 |
48 | ```typescript
49 | it("존재하지 않는 상품 ID를 받으면 Error 객체가 throw된다.", async () => {
50 | // given
51 | const productService = new ProductService();
52 | // when
53 | const promise = productService.getProduct("1234");
54 | // then
55 | await expect(promise).rejects.toThrow("product is not exist");
56 | });
57 | ```
58 |
59 | try/catch가 없어지면서 테스트 코드가 더 간단해졌습니다. when 절에서 예외가 발생하지 않으면 테스트는 실패합니다.
--------------------------------------------------------------------------------
/tests/not_too_much_nipticking.md:
--------------------------------------------------------------------------------
1 | # 지나친 잔소리는 좋지 않습니다.
2 |
3 | 테스트를 작성하다보면 아주 세밀한 동작까지 검사를 하고 싶은 유혹에 빠져서 불필요한 단언을 작성할 때가 있습니다. 테스트 하려는 대상의 아주 작은 결과 하나까지도 확인하려는 단언을 광역 단언이라고 합니다. 잔소리를 지나치게 하는 단언입니다.
4 |
5 | 이런 단언은 테스트의 의도를 읽기 어렵게 만들고, 테스트 코드가 구현 코드를 강하게 의존하게 만듭니다.
6 |
7 | ## 잔소리가 지나친 테스트 1
8 |
9 | 아래의 테스트는 값이 1씩 증가한다는 걸 확인하기 위해서 스파이 개체를 이용합니다.
10 |
11 | 스파이를 심어서 호출 횟수는 물론 전달 인자까지 확인을 하고 있습니다.
12 |
13 | ```typescript
14 | describe("증가 버튼 >", () => {
15 | it("클릭할 때마다 값을 1씩 증가시켜야 한다.", () => {
16 | // given
17 | const spinbox = new Spinbox(1);
18 | const setValueSpy = sinon.spy(spinbox, "setValue");
19 |
20 | // when
21 | spinbox.$increment.click(); // 2
22 | spinbox.$increment.click(); // 3
23 |
24 | // then
25 | expect(setValueSpy.callCount).toBe(2);
26 | expect(setValueSpy.args[0][0]).toBe(2);
27 | expect(setValueSpy.args[1][0]).toBe(3);
28 | expect(spinbox.getValue()).toBe(3);
29 | });
30 | });
31 | ```
32 |
33 | 테스트를 통해서 확인을 해야 할 대상은 입력과 출력입니다. 그 중간 과정을 집요하게 테스트하는 일은 큰 비용을 수반합니다.
34 |
35 |
36 |
37 | 우선, 단언이 복잡해졌습니다. 그래서 확인하고 싶은 결과가 무엇인지 정확하게 이해하기가 어렵습니다. 두 번째로 구현 코드와 테스트 코드 사이에 강한 의존성이 생겼습니다. setValue의 구현 방식을 테스트가 알아야만 합니다.
38 |
39 | 이 문제는 "[구현이 아닌 인터페이스를 테스트합니다.](./test_interface.md)"와도 관련이 있으니 참고하세요.
40 |
41 | ### 잔소리 줄이기
42 |
43 | 이 문제를 해결하기 위해서 필요한 최소한의 단언만 남깁니다.
44 |
45 | ```typescript
46 | describe("증가 버튼 >", () => {
47 | it("한 번 클릭할 때마다 값을 1씩 증가시켜야 한다.", () => {
48 | // given
49 | const spinbox = new Spinbox(1);
50 |
51 | // when
52 | spinbox.$increment.click(); // 2
53 | spinbox.$increment.click(); // 3
54 |
55 | // then
56 | expect(spinbox.getValue()).toBe(3);
57 | });
58 | });
59 | ```
60 |
61 | 하지만 이 테스트는 조금 아쉽습니다. 클릭당 값이 1증가해야 한다는 요구사항을 만족시키고 있다는 걸 증명하지 못하기 때문입니다. 그저 버튼을 두 번 클릭한 결과가 3이라는 걸 확인할 뿐이죠.
62 |
63 | 테스트는 케이스를 하나 더 추가해서 이 문제를 해결할 수 있습니다.
64 |
65 | ```typescript
66 | describe("증가 버튼", () => {
67 | it("한 번 클릭하면 값을 1 증가시켜야 한다.", () => {
68 | // given
69 | const spinbox = new Spinbox(1);
70 |
71 | // when
72 | spinbox.$increment.click();
73 |
74 | // then
75 | expect(spinbox.getValue()).toBe(2);
76 | });
77 |
78 | it("두 번 클릭하면 값을 2 증가시켜야 한다.", () => {
79 | // given
80 | const spinbox = new Spinbox(1);
81 |
82 | // when
83 | spinbox.$increment.click();
84 | spinbox.$increment.click();
85 |
86 | // then
87 | expect(spinbox.getValue()).toBe(3);
88 | });
89 | });
90 | ```
91 |
92 | 하나였던 테스트 케이스가 둘이 되었지만 명세와 테스트 코드 사이의 관계가 훨씬 분명해졌습니다.
93 |
94 | ## **잔소리가 지나친 테스트 2**
95 |
96 | 아래의 테스트 코드는 서버에서 불러온 데이터를 개체의 프로퍼티 단위로 확인을 합니다.
97 |
98 | ```typescript
99 | it("서버에서 적절한 데이터를 가져와야 한다.", async () => {
100 | // given
101 | // when
102 | const storeId = 112;
103 | await autoChargeStore.load(storeId);
104 |
105 | // then
106 | expect(autoChargeStore.autoChargeSubscriber).toBeTruthy();
107 | expect(autoChargeStore.autoChargeSubscriber.serviceStatus).toEqual("이용중");
108 | expect(autoChargeStore.autoChargeSubscriber.triggeringAmount).toEqual(100000);
109 | expect(autoChargeStore.autoChargeSubscriber.desiredDeactivationDate).toEqual("2019-12-03T03:26:15Z");
110 | expect(autoChargeStore.autoChargeSubscriber.allowBilling).toBeTruthy();
111 | expect(autoChargeStore.autoChargeSubscriber.chargeAmount).toEqual(500000);
112 | });
113 | ```
114 |
115 | 테스트 명세의 "적절한"이 의미하는 바가 모호합니다. 테스트 하려는 개체(autoChargeSubscriber)의 캡슐화를 깨뜨림으로써 테스트와 구현 코드 사이에 강한 의존성이 생겼습니다. 프로퍼티를 하나씩 단언으로 확인함으로써 pointAutoChargeSubscriber 개체의 설계에 작은 변경만 발생해도 이 테스트는 깨질 수 있습니다.
116 |
117 | ### 잔소리 줄이기
118 |
119 | 요구사항을 더 선명하게 식별해서 "적절한"이 의미하는 바를 분명하게 드러냅니다. 그리고 기대하는 결과를 표현하는 개체를 만들어서 개체 비교를 하면 의존성을 줄일 수 있습니다.
120 |
121 | ```typescript
122 | it("상점 번호에 해당하는 자동충전 구독자를 가져와야 한다.", async () => {
123 | // given
124 | // when
125 | const storeId = 112;
126 | await autoChargeStore.load(storeId);
127 |
128 | // then
129 | const expectedSubscriber = new autoChargeSubscriber({ storeId, ...successfulDummyResponse });
130 | expect(autoChargeStore.autoChargeSubscriber).toEqual(expectedSubscriber);
131 | });
132 | ```
--------------------------------------------------------------------------------
/tests/single_responsibility.md:
--------------------------------------------------------------------------------
1 | # 테스트가 한 가지 일에만 관심을 갖게 해주세요.
2 |
3 | 테스트의 관심사는 테스트 하려는 대상입니다. 하나의 테스트가 두 개 이상의 관심사를 가지면 테스트 코드가 쉽게 복잡해집니다. 코드가 복잡해지면 테스트에 실패했을때 때 원인을 찾기가 어렵겠죠. 그래서 테스트에도 단일 책임을 적용하는 게 좋습니다.
4 |
5 | 하나의 관심사를 가져야 한다는 것이 테스트 당 하나의 단언만 가져야 한다는 뜻은 아닙니다. 하나의 관심사를 테스트 하는 데에 여러 개의 단언이 필요할 수도 있습니다. 관심사의 폭을 가능한 좁게 잡는 게 좋다는 뜻입니다.
6 |
7 | ### 많은 관심사를 가진 테스트
8 |
9 | 아래의 테스트는 1) 초기 값, 2) 값 증가, 3) 값 감소라는 세 개의 관심사를 가지고 있습니다. 그래서 단언이 테스트 안에서 여기저기 흩어졌습니다. 각각의 단언을 이해하려면 코드를 자세히 읽어야만 합니다.
10 |
11 | ```typescript
12 | it("스핀박스를 초기화하면 값이 0이 되어야 하고, 증가 버튼을 클릭하면 값이 1이 증가하고, 감소 버튼을 클릭하면 값이 1이 감소해야 한다.", () => {
13 | const spinbox = new Spinbox();
14 |
15 | spinbox.setValue(100);
16 | spinbox.reset();
17 | expect(spinbox.getValue()).toBe(0);
18 |
19 | spinbox.setValue(1);
20 | spinbox.reset();
21 | expect(spinbox.getValue()).toBe(0);
22 |
23 | spinbox.$increment.click();
24 | expect(spinbox.getValue()).toBe(1);
25 |
26 | spinbox.$decrement.click();
27 | expect(spinbox.getValue()).toBe(0);
28 | });
29 | ```
30 |
31 | 단순한 예제이기에 그리 복잡해보이지 않을 수 있지만 현실의 문제는 훨씬 복잡하기에 더 신경을 써야 합니다.
32 |
33 | ### 관심사 분리하기
34 |
35 | 각각의 관심사를 개별 테스트로 분리하면 코드를 아래와 같이 바꿀 수 있습니다. 테스트의 갯수는 늘었지만 테스트가 검증하려는 행위와 기대하는 결과를 훨씬 쉽게 파악할 수 있습니다.
36 |
37 | ```typescript
38 | describe("스핀박스", () => {
39 | let spinbox;
40 |
41 | beforeEach(() => {
42 | spinbox = new Spinbox();
43 | });
44 |
45 | it("값이 100인 스핀박스를 초기화하면 값이 0이 되어야 한다.", () => {
46 | // given
47 | spinbox.setValue(100);
48 |
49 | // when
50 | spinbox.reset();
51 |
52 | // then
53 | expect(spinbox.getValue()).toBe(0);
54 | });
55 |
56 | it("값이 1인 스핀박스를 초기화하면 값이 0이 되어야 한다.", () => {
57 | // given
58 | spinbox.setValue(1);
59 |
60 | // when
61 | spinbox.reset();
62 |
63 | // then
64 | expect(spinbox.getValue()).toBe(0);
65 | });
66 |
67 | it("값이 0인 스핀박스의 증가 버튼을 클릭하면 값이 1이 증가해야 한다.", () => {
68 | // given
69 | spinbox.setValue(0);
70 |
71 | // when
72 | spinbox.$increment.click();
73 |
74 | // then
75 | expect(spinbox.getValue()).toBe(1);
76 | });
77 |
78 | it("값이 2인 스핀박스의 감소 버튼을 클릭하면 값이 1이 되어야 한다.", () => {
79 | // given
80 | spinbox.setValue(2);
81 |
82 | // when
83 | spinbox.$decrement.click();
84 |
85 | // then
86 | expect(spinbox.getValue()).toBe(1);
87 | });
88 | });
89 | ```
--------------------------------------------------------------------------------
/tests/test_interface.md:
--------------------------------------------------------------------------------
1 | # 구현이 아닌 인터페이스를 테스트합니다.
2 |
3 | 테스트 코드는 구현 코드를 의존합니다. 테스트 코드가 구현 코드의 속 사정을 많이 알면 알수록 둘 사이의 의존성은 강해집니다. 강한 의존성은 변경에 취약한 구조를 만듭니다. 작은 변경에도 쉽게 테스트 코드가 깨질 때 "테스트가 취약(fragile)"하다고 합니다. 변경에 강한 테스트를 작성하려면 구현 코드의 상세 구현이 아닌 인터페이스를 테스트해야 합니다.
4 |
5 | ### 변경에 취약한 테스트 코드
6 |
7 | 아래의 코드는 단언이 세부 구현을 너무 많이 알고 있습니다. `_items` 프로퍼티의 타입을 변경하거나, 이름을 변경한다면 테스트도 영향을 받을 겁니다.
8 |
9 | ```typescript
10 | it("새로운 아이템을 목록에 추가해야 한다.", () => {
11 | // when
12 | list._items.push("A");
13 | list._items.push("B");
14 |
15 | // then
16 | expect(list._items[0]).toBe("A");
17 | expect(list._items[1]).toBe("B");
18 | });
19 | ```
20 |
21 | 변경에 취약한 테스트가 많아지면 리팩터링이 부담스러워 집니다. 리팩터링을 할 때마다 테스트는 깨질 것이고, 깨지는 테스트가 많아지면 리팩터링을 하기가 두려워지기 때문입니다. 그러다가 애꿎은 테스트를 욕합니다. "테스트가 우리를 더 느리게 만든다!"고 말이죠.
22 |
23 | ### 인터페이스를 테스트하기
24 |
25 | list 개체의 공용 인터페이스를 테스트에 이용함으로써 테스트를 변경에 덜 취약하게 만들 수 있습니다.
26 |
27 | ```typescript
28 | it("새로운 아이템을 목록에 추가해야 한다.", () => {
29 | list.addItem("A");
30 | list.addItem("B");
31 |
32 | expect(list.toArray()).toBe(["A", "B"]);
33 | });
34 | ```
35 |
36 | 테스트도 코드이기에 일반 설계 원칙은 테스트 코드를 작성할 때도 유효합니다.
--------------------------------------------------------------------------------
/tests/test_meaningful_points.md:
--------------------------------------------------------------------------------
1 | # 테스트를 하는 게 의미가 있는 지점을 테스트합니다.
2 |
3 | 작성한 테스트는 계속해서 안고 가야할 자산이 됩니다. 가진 자산 만큼 관리 비용을 지불해야 합니다. 그래서 테스트가 많다고 무조건 좋은 것은 아닙니다. 결국 효율을 고민해야 합니다. 테스트 작성의 효율을 높이려면, "테스트를 하는 게 의미가 있는 지점"을 테스트 해야 합니다.
4 |
5 | 아래의 테스트 코드는 작성해서 얻을 수 있는 이점이 적습니다. 구현 코드가 너무 단순해서 개발자가 실수를 할 확률이 매우 적기 때문입니다. 컴포넌트 렌더링 여부는 다른 테스트 과정에서 쉽게 문제를 검출할 수 있는 부분이기도 합니다.
6 |
7 | ```typescript
8 | const Spinbox = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | describe("Spinbox >", () => {
19 | it("마운트를 하면 렌더링되어야 한다.", () => {
20 | // given
21 | // when
22 | const wrapper = mount();
23 |
24 | // then
25 | expect(wrapper.find("div").exists()).toBeTruthy();
26 | });
27 | });
28 | ```
29 |
30 | TDD를 하고 있다면, 거쳐가는 하나의 과정으로 이런 테스트를 작성하는 게 의미가 있을 수도 있습니다. 하지만 그게 아니라면 "테스트를 하는 게 의미가 있는 지점"을 찾는 데에 더 집중하세요.
31 |
32 |
--------------------------------------------------------------------------------
/tests/test_your_boundaries.md:
--------------------------------------------------------------------------------
1 | # 입력 케이스가 너무 많을 때는 경계를 테스트하세요.
2 |
3 | 입력할 값의 범위가 정해져 있고, 입력 케이스가 많은 경우에는 입력 값의 경계를 테스트하여 테스트 비용을 줄일 수 있습니다.
4 |
5 | ### 예시
6 |
7 | 태어난 년도를 선택할 수 있는 셀렉트 박스가 있습니다. 이 셀렉트 박스는 옵션으로 1900 ~ 2019의 값을 제공합니다. 옵션을 선택하면 셀렉트 박스에 값이 제대로 설정되는지 확인을 하고 싶습니다.
8 |
9 | 아래와 같이 모든 입력 값을 테스트를 하기에는 너무 번거롭습니다.
10 |
11 | ```typescript
12 | describe("태어난 년도 입력 >", () => {
13 | let container;
14 |
15 | beforeEach(() => {
16 | container = document.createElement("div");
17 |
18 | act(() => {
19 | render(, container);
20 | });
21 | });
22 |
23 | it("1900년을 선택할 수 있어야 한다.", () => {
24 | // given
25 | const value = 1900;
26 |
27 | // when
28 | container
29 | .querySelector(`[data-value=${value}]`)
30 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
31 |
32 | // then
33 | const selectBoxNode = getByTestId(container, "year");
34 | expect(selectBoxNode.value).toBe(value);
35 | });
36 |
37 | it("1901년을 선택할 수 있어야 한다.", () => {
38 | // given
39 | const value = 1901;
40 |
41 | // when
42 | container
43 | .querySelector(`[data-value=${value}]`)
44 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
45 |
46 | // then
47 | const selectBoxNode = getByTestId(container, "year");
48 | expect(selectBoxNode.value).toBe(value);
49 | });
50 |
51 | .
52 | .
53 | .
54 |
55 | it("2019년을 선택할 수 있어야 한다.", () => {
56 | // given
57 | const value = 2019;
58 |
59 | // when
60 | container
61 | .querySelector(`[data-value=${value}]`)
62 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
63 |
64 | // then
65 | const selectBoxNode = getByTestId(container, "year");
66 | expect(selectBoxNode.value).toBe(value);
67 | });
68 | });
69 | ```
70 |
71 | ### for 문을 이용하여 개선하기
72 |
73 | 조금 영리하게 for 문을 이용하여 이 문제를 완화할 수 있습니다.
74 |
75 | ```typescript
76 | it("1900 ~ 2019년을 선택할 수 있어야 한다.", () => {
77 | // given
78 | const startYear = 1900;
79 | const endYear = 2019;
80 |
81 | for(let value = startYear; value >= endYear; value++) {
82 | // when
83 | container
84 | .querySelector(`[data-value=${value}]`)
85 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
86 |
87 | // then
88 | const selectBoxNode = getByTestId(container, "year");
89 | expect(selectBoxNode.value).toBe(value);
90 | }
91 | });``}``});
92 | ```
93 |
94 | 하지만 이 방법은 권장하지 않습니다. 테스트 코드에 반복 또는 조건 로직이 들어가면 테스트를 이해하기가 어려워지기 때문입니다. 테스트에 복잡성을 가진 로직이 생기면 테스트를 테스트 해야 하는 문제도 발생합니다.
95 |
96 | ### 경계 값을 테스트하기
97 |
98 | 경계 값 테스트는 양 극단의 경계에 있는 값을 테스트 하는 걸로 "적당히 만족"합니다. 적은 테스트 비용으로 최대의 효용을 끌어냅니다.
99 |
100 | ```typescript
101 | it("1899년은 옵션에 노출하지 않아야 한다.", () => {
102 | // given
103 | const value = 1899;
104 |
105 | // when
106 | const node = container.querySelector(`[data-value=${value}]`);
107 |
108 | // then
109 | expect(node).toBeUndefined;
110 | });
111 |
112 | it("1900년을 선택할 수 있어야 한다.", () => {
113 | // given
114 | const value = 1900;
115 |
116 | // when
117 | container
118 | .querySelector(`[data-value=${value}]`)
119 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
120 |
121 | // then
122 | const selectBoxNode = getByTestId(container, "year");
123 | expect(selectBoxNode.value).toBe(value);
124 | });
125 |
126 | it("2019년을 선택할 수 있어야 한다.", () => {
127 | // given
128 | const value = 2019;
129 |
130 | // when
131 | container
132 | .querySelector(`[data-value=${value}]`)
133 | .dispatchEvent(new MouseEvent("click", { bubbles: true }));
134 |
135 | // then
136 | const selectBoxNode = getByTestId(container, "year");
137 | expect(selectBoxNode.value).toBe(value);
138 | });
139 |
140 | it("2020년은 옵션에 노출하지 않아야 한다.", () => {
141 | // given
142 | const value = 2020;
143 |
144 | // when
145 | const node = container.querySelector(`[data-value=${value}]`);
146 |
147 | // then
148 | expect(node).toBeUndefined;
149 | });``});
150 | ```
--------------------------------------------------------------------------------
/tests/write_enough_and_edge_cases.md:
--------------------------------------------------------------------------------
1 | # 테스트 케이스를 충분히 작성하되, 엣지(Edge) 케이스도 생각하세요!
2 |
3 | 테스트를 작성할 때, 테스트 케이스를 충분히 작성해야 합니다. 특히, 사후에 테스트를 작성할 때 단순히 함수를 호출하는 정도로만 테스트를 작성하고 넘기기 쉽습니다.
4 |
5 | ### 테스트 케이스가 부족한 코드
6 |
7 | 아래에 보이는 테스트는 한 자리 계정 번호를, 여섯 자리의 일련 번호 문자열로 잘 변환하는 함수를 검증합니다.
8 |
9 | ```typescript
10 | describe("등록 URL을 생성", () => {
11 | it("계정 번호를 빈 자리가 0인 6자리의 문자열의 일련번호로 변환하여 query string에 파라미터로 추가해야 한다.", () => {
12 | // given
13 | const accountNumber = 5;
14 |
15 | // when
16 | const url = createRegistrationUrl(accountNumber);
17 |
18 | // then
19 | const params = parse(url.split("?").pop());
20 | expect(params.serialNo).toBe("000005");
21 | });
22 | });
23 | ```
24 |
25 | 이 테스트는 createRegistrationUrl 함수가 아래와 같이 구현이 되어 있는 경우에도 통과합니다.
26 |
27 | ```typescript
28 | const createRegistrationUrl = () => {
29 | return "http://blahblah.com?serialNo=000005";
30 | };
31 | ```
32 |
33 | createRegistrationUrl 함수는 요구사항을 충분히 충족하지 못하고 있지만 이를 검증하는 테스트가 없습니다.
34 |
35 | ### 테스트 케이스를 추가한 코드
36 |
37 | 아래의 코드는 새로운 테스트 케이스를 추가하여 이 문제를 해결합니다.
38 |
39 | ```typescript
40 | describe("등록 URL을 생성", () => {
41 | it("한 자리 계정 번호를 빈 자리가 0인 6자리의 문자열의 일련번호로 변환하여 query string에 파라미터로 추가해야 한다.", () => {
42 | // given
43 | const oneDigitAccountNumber = 5;
44 |
45 | // when
46 | const url = createRegistrationUrl(oneDigitAccountNumber);
47 |
48 | // then
49 | const params = parse(url.split("?").pop());
50 | expect(params.serialNo).toBe("000005");
51 | });
52 |
53 | it("여섯 자리 계정 번호를 6자리의 문자열인 일련번호로 변환하여 query string에 파라미터로 추가해야 한다.", () => {
54 | // given
55 | const sixDigitAccountNumber = 123456;
56 |
57 | // when
58 | const url = createRegistrationUrl(sixDigitAccountNumber);
59 |
60 | // then
61 | const params = parse(url.split("?").pop());
62 | expect(params.serialNo).toBe("123456");
63 | });
64 | });
65 | ```
66 |
67 | 테스트 케이스를 생각할 때 "정상 입력"만을 생각할 때가 종종 있습니다. 만약 계정 번호에 어떤 이유로 빈 문자열이 설정될 가능성이 있다면 어떨까요? 많은 개발자가 예외 상황을 고려하지 않았다가 QA에 들어가서 쏟아지는 버그 리포트로 고생을 해 본 경험이 있을 겁니다.
68 |
69 | 정상적인 입력 시나리오는 쉽게 떠올릴 수 있기 때문에 실수할 가능성이 상대적으로 낮습니다. 우리가 테스트를 작성할 때 주의를 해야하는 지점은 정상 입력과 비정상 입력의 경계입니다.
70 |
71 | ### 엣지(Edge) 케이스를 추가한 코드
72 |
73 | 정상 입력과 비정상 입력의 경계에 위치하는 상황을 엣지 케이스라고 합니다. 요구사항을 분석하거나 테스트를 작성할 때는 엣지 케이스를 세심하게 고려해야 합니다. 세심하게 보지 않으면 놓치기 쉽기 때문입니다.
74 |
75 | 테스트를 작성하는 일은, 단순히 코드를 작성하는 것을 넘어 "요구사항을 확인하고 검증"하는 일이기도 합니다. 아래의 코드는 계정 번호에 빈 문자열이 설정되는 상황을 테스트합니다.
76 |
77 | ```typescript
78 | it("빈 문자열인 계정 번호는 '000000'인 일련번호로 변환하여 query string에 파라미터로 추가해야 한다.", () => {
79 | // given
80 | const accountNumber = "0";
81 |
82 | // when
83 | const url = createRegistrationUrl(accountNumber);
84 |
85 | // then
86 | const params = parse(url.split("?").pop());
87 | expect(params.serialNo).toBe("000000");
88 | });
89 | ```
--------------------------------------------------------------------------------