-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 39.3 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 39.3 KB
1
{"meta":{"title":"휴지블로그","subtitle":null,"description":null,"author":"huusz","url":"http://huusz.github.io"},"pages":[{"title":"About","date":"2018-07-16T12:28:06.676Z","updated":"2018-07-16T12:28:06.676Z","comments":true,"path":"about/index.html","permalink":"http://huusz.github.io/about/index.html","excerpt":"","text":""},{"title":"Project","date":"2018-07-16T12:28:06.684Z","updated":"2018-07-16T12:28:06.684Z","comments":true,"path":"project/index.html","permalink":"http://huusz.github.io/project/index.html","excerpt":"","text":""},{"title":"Tags","date":"2018-07-16T12:28:06.685Z","updated":"2018-07-16T12:28:06.685Z","comments":true,"path":"tags/index.html","permalink":"http://huusz.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"테스트 하지 않던 코드 테스트 하기","slug":"how-to-test","date":"2018-07-02T14:04:53.000Z","updated":"2018-10-27T14:53:49.315Z","comments":true,"path":"2018/Test/how-to-test/","link":"","permalink":"http://huusz.github.io/2018/Test/how-to-test/","excerpt":"","text":"도입내가 테스트를 작성하기 시작한 것은 약 2달 전이다. 그 전까지는 테스트 코드를 작성하지 않았다. 해본 적이 없었고, 어떻게 시작하는 지 몰라서였다. 구글에 검색해서 나오는 글들은 대부분 간단하고 부작용(side effect)이 없는 순수 함수들을 예제로 하고 있어서, 그렇지 않은 코드가 더 많은 실무 코드에 활용하기가 쉽지 않았다. 이 글에서는 내가 실제로 *테스트 없이 작성되어 있는 기존 코드에 어떻게 테스트를 추가하고, 동시에 TDD를 했는 지 이야기 하려고 한다. 참고로 예제 코드는 React로 작성되었고, 테스트 프레임 워크로는 Jest를 사용하였다.*테스트: 이 글에서 말하는 테스트는 단위 테스트(unit test)이다.1. 함수로 분리하자.프로젝트의 전반적인 컴포넌트 구조는[부모] Redux와 API 요청 및 React 라이프 사이클 함수를 호출하는 Container 컴포넌트[자식] 실제 View를 반환하는 순수 함수로 작성된 Presentational 컴포넌트이렇게 두 컴포넌트가 중심이 된다.사이드 이펙트가 발생할 수 있는 모든 요소는 Container 컴포넌트에 있고, Presentational 컴포넌트는 최대한 순수하게 유지하고 있다. 그렇게 하다보니, Container 컴포넌트가 정말 길고 복잡하고, 가독성도 매우 떨어졌다. 내가 테스트를 위해 가장 먼저 한 일은 Container 컴포넌트에 숨어 있는 비즈니스 로직을 순수 함수로 추출하는 일이다. 비즈니스 로직은 DOM 조작이 필요하지 않기 때문에 순수 함수로 분리해낸다면 비교적 쉽게 테스트 할 수 있다.아래는 앞으로 글 전반에서 사용하는 예제의 풀 버전이다. 안 보고 넘어가도 글의 흐름을 이해하는 데 문제는 없다. 예제 코드는 실제 코드의 복잡도를 재현하고 싶어서 최대한 작성해 보았지만 100% 동작하는 코드는 아니다. API 요청은 fake 함수로 대체하였다.1) 탐색123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051// RegistrationFormContainer.jsimport React, { Component } from 'react';// (이하 생략)class RegistrationFormContainer extends Component { handleCloseModal = e => { e.preventDefault(); UserActions.closeModal(); }; handleSubmit = e => { e.preventDefault(); this.form.validateFields(async (err, validValues) => { if (err) { return; } // ************* 함수로 추출할 부분은 바로 여기 이다!! ***************** const phone = validValues.phone ? '+82' + validValues.phone.slice(1) : null; const command = { username: validValues.username || validValues.email, email: validValues.email, password: validValues.password, phone: phone || null, agreement: validValues.agreement, }; // ************************************************************ try { await UserActions.postRegistration(command); // API 요청 this.handleCloseModal(); // 요청이 성공하면 모달을 닫는다. } catch (err) { Modal.error({ // 요청이 실패하면 에러 모달을 트리거한다. message: 'Register failed.', icon: true, }); } }); }; render() { return ( /* UI */ ); }}export default connect(({ user }) => ({ modalVisible: user.getIn(['modal', 'visible']),}))(RegistrationFormContainer);2) 분리processRegistrationCommand 라는 이름의 함수를 외부 파일로 생성하고, 일단 발견한 부분을 무작정 추출해온다. 당연히 코드는 정상적으로 돌아가지 않을 것이다. 아무것도 수정하지 않는다. 다만, 어떤 것을 리턴할 것인지만 정한다.1234567891011121314// processRegistrationCommand.jsexport default function processRegistrationCommand() { const phone = validValues.phone ? '+82' + validValues.phone.slice(1) : null; const command = { username: validValues.username || validValues.email, email: validValues.email, password: validValues.password, phone: phone || null, agreement: validValues.agreement, }; return command;}아까의 RegistrationFormContainer 컴포넌트에서 새로 만든 함수를 불러와서 적용한다.123456789101112131415161718192021222324252627282930// RegistrationFormContainer.jsimport processRegistrationCommand from '../business/processRegistrationCommand';// (생략)class RegistrationFormContainer extends Component { // (생략) handleSubmit = e => { e.preventDefault(); this.form.validateFields(async (err, validValues) => { if (err) { return; } // ***************************** 함수 적용 ********************** const command = processRegistrationCommand(); // ************************************************************ try { await UserActions.postRegistration(command); // API request this.handleCloseModal(); } catch (err) { Modal.error({ message: 'Register failed.', icon: true, }); } }); }; // (생략)}2. 함수 스펙을 작성한다.이제 테스트 파일을 만든다. 그리고 아까 추출한 processRegistrationCommand 함수를 불러온다. 그리고 describe, test 구문을 이용해 스펙을 작성한다. 처음부터 모든 시나리오를 빠짐없이 적으려 할 필요는 없다. 천천히 하나씩 스펙을 추가해도 상관없다. 시작은 주로 핵심 기능으로 시작하는 데, 이는 대부분 Input, Output에 드러나있다. 아래는 테스트는 핵심 기능에 대한 스펙들이 미리 추가된 것이다.1234567891011121314151617// processRegistrationCommand.test.jsimport processRegistrationCommand from './processRegistrationCommand';describe('processRegistrationCommand 함수는', () => { test('오류를 던지지 않는다.', () => { expect(processRegistrationCommand).not.toThrowError(); }); describe('Registration 요청 커맨드 객체 형태로 정제하여 반환하는 데, ', () => { test('username 속성의 default 값은 email이다.'); test('email 속성은 입력 값과 동일하다.'); test('email 속성은 입력 값과 동일하다.'); test('phone 속성은 옵션 값이며, default 값은 null이다.'); test('phone 속성은 첫 번째 0 대신 +82가 포함된 문자열이다.'); test('agreement 속성은 입력 값과 동일하다.'); });});3. 테스트 코드를 작성해보자!1) 실패하는 테스트현재 첫 번째 테스트는 실패하고 있다. processRegistrationCommand 함수가 현재 오류를 던지고 있기 때문이다. 테스트가 통과하도록 프로덕션 코드를 수정해준다. validValues 변수가 해당 함수의 스코프 내에서 정의되지 않아 오류가 발생한 것이기 때문에, 해당 변수를 파라미터로 받게 하여 오류를 제거한다.12345678910111213export default function processRegistrationCommand(validValues = {}) { const phone = validValues.phone ? '+82' + validValues.phone.slice(1) : null; const command = { username: validValues.username || validValues.email, email: validValues.email, password: validValues.password, phone: phone || null, agreement: validValues.agreement, }; return command;}2) 성공하는 테스트나머지 테스트 코드를 하나씩 채워볼 것이다. 두번째 테스트는 실패하지 않을 것이다. 이미 모두 구현되어 있기 때문이다.1234567891011121314151617181920212223import processRegistrationCommand from './processRegistrationCommand';describe('processRegistrationCommand 함수는', () => { test('오류를 던지지 않는다.', () => { expect(processRegistrationCommand).not.toThrowError(); }); describe('Registration 요청 커맨드 객체 형태로 정제하여 반환하는 데, ', () => { test('username 속성의 default 값은 email이다.', () => { const param = { username: undefined, email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], }; const actual = processRegistrationCommand(param); expect(actual.username).toBe(param.email); }); // ... });});세번째, 네번째 테스트도 추가한다. 물론 성공하는 테스트이다.12345678910111213141516171819202122232425test('email 속성은 입력 값과 동일하다.', () => { const param = { username: 'username', email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], }; const actual = processRegistrationCommand(param); expect(actual.email).toBe(param.email);});test('password 속성은 입력 값과 동일하다.', () => { const param = { username: 'username', email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], }; const actual = processRegistrationCommand(param); expect(actual.password).toBe(param.password);});이쯤하면 약간 거슬리는 부분이 생긴다. 바로, 동일한 param 객체를 반복해서 생성하고 있는 부분이다. param 객체와 같이 테스트를 위해 생성되는 데이터를 Fixture 라고 한다. 위와 같이 여러 테스트에서 반복적으로 사용되는 Fixture는 좀 더 상위 스코프에 선언하는 편이 낫다.12345678910111213141516171819202122232425262728293031323334describe('processRegistrationCommand 함수는', () => { // ... describe('Registration 요청 커맨드 객체 형태로 정제하여 반환하는 데, ', () => { const param = { username: 'username', email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], }; test('username 속성의 default 값은 email이다.', () => { const actual = processRegistrationCommand({ ...param, username: undefined, }); // <- 테스트가 검증하고자 하는 바가 username이 undefined 일 때 임이 한눈에 보인다. expect(actual.username).toBe(param.email); }); test('email 속성은 입력 값과 동일하다.', () => { const actual = processRegistrationCommand(param); expect(actual.email).toBe(param.email); }); test('password 속성은 입력 값과 동일하다.', () => { const actual = processRegistrationCommand(param); expect(actual.password).toBe(param.password); }); test('phone 속성은 옵션 값이며, default 값은 null이다.'); test('phone 속성은 첫 번째 0 대신 +82가 포함된 문자열이다.'); test('agreement 속성은 입력 값과 동일하다.'); });});이렇게 Fixture를 상위 스코프에 선언해주면, 동일한 또는 유사한 Fixture를 반복적으로 생성하지 않아도 되기 때문에 코드가 훨씬 간결해질 뿐 아니라, 테스트에서 검증하고자 하는 특정 값이 더 명확하게 보여 테스트의 가독성을 높이는 효과를 얻을 수 있다.이제 나머지 테스트도 채워보자. 이번에도 모두 성공하는 테스트이다.1234567891011121314test('phone 속성은 옵션 값이며, default 값은 null이다.', () => { const actual = processRegistrationCommand({ ...param, phone: undefined }); expect(actual.phone).toBeNull();});test('phone 속성은 첫 번째 0 대신 +82가 포함된 문자열이다.', () => { const actual = processRegistrationCommand(param); expect(actual.phone).toBe(`+82${param.phone.slice(1)}`);});test('agreement 속성은 입력 값과 동일하다.', () => { const actual = processRegistrationCommand(param); expect(actual.agreement).toBe(param.agreement);});전체적으로 한번 쭉 보니, 대부분의 테스트가 동일한 패턴으로 반복되고 있다. 동일하게 반복되는 이러한 패턴에 맘에 들지 않는다면, 이것도 간략하게 정리할 수 있다. 아래는 정리하고 난 후의 전체 코드이다.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import processRegistrationCommand from './processRegistrationCommand';describe('processRegistrationCommand 함수는', () => { test('오류를 던지지 않는다.', () => { expect(processRegistrationCommand).not.toThrowError(); }); describe('Registration 요청 커맨드 객체 형태로 정제하여 반환하는 데, ', () => { const param = { username: 'username', email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], }; const equalToParam = ['name', 'password', 'agreement']; equalToParam.forEach(prop => { test(`${prop} 속성은 입력 값과 동일하다.`, () => { const actual = processRegistrationCommand(param); expect(actual[prop]).toBe(param[prop]); }); }); const defaultOf = [ { key: 'username', expected: param.email, }, { key: 'phone', expected: null, }, ]; defaultOf.forEach(({ key, expected }) => { test(`${key} 속성은 옵션 값이며, default 값은 ${expected}이다.`, () => { const actual = processRegistrationCommand({ ...param, [key]: undefined, }); expect(actual[key]).toBe(expected); }); }); test('phone 속성은 첫 번째 0 대신 +82가 포함된 문자열이다.', () => { const actual = processRegistrationCommand(param); expect(actual.phone).toBe(`+82${param.phone.slice(1)}`); }); });});이런 식으로 반복되는 테스트 코드의 패턴을 파악해서 코드의 양을 줄일 수 있을 뿐 아니라, equalToParam이나 defaultOf 같은 Fixture만 보고도 대략적인 내용을 파악할 수 있게 되어 가독성 면에서도 더 나아진다. 하지만 이 가독성이라는 게 다소 상대적인 것이라, 이전의 나열 방식이 더 낫다고 생각할 수도 있다. 그리고 이처럼 Fixture와 반복문을 이용해 중복을 제거하는 방식이 항상 가독성이 더 좋은 것도 아니다. 상황에 따라, 선호도에 따라, 적절히 잘 선택하면 된다.3) Refactor - 테스트가 실패하지 않는 범위 내에서 코드 개선하기테스트를 마쳤고 모든 테스트가 성공하는 것을 확인했으니 이제 리팩터링을 할 수 있다. 리팩터링 단계에서 중요한 것은, 테스트가 실패하지 않는 범위 내에서만 코드를 개선하는 것이다. 이 말은, 리팩터링 하다가 테스트가 실패하면 테스트를 고치라는 것이 아니다. 리팩터링을 하는 중에는 절대 테스트에 아무런 변화도 가해선 안 된다. 리팩터링을 하다가 테스트가 실패한다면, 분명 프로덕션 코드에 버그를 만든 것이니 하던 것을 중단하고 코드를 다시 되돌려 놓아야 한다. 아래는 현재 프로덕션 코드이다.1234567891011121314// processRegistrationCommand.jsexport default function processRegistrationCommand(validValues = {}) { const phone = validValues.phone ? '+82' + validValues.phone.slice(1) : null; const command = { username: validValues.username || validValues.email, email: validValues.email, password: validValues.password, phone: phone || null, agreement: validValues.agreement, }; return command;}먼저, validValues 라는 파라미터 명이 좀 별로인 것 같다. validValues인지 아닌지는 이 함수는 몰라도 된다. 그냥 source로 바꿔준다.12345678910111213export default function processRegistrationCommand(source = {}) { const phone = source.phone ? '+82' + source.phone.slice(1) : null; const command = { username: source.username || source.email, email: source.email, password: source.password, phone: phone || null, agreement: source.agreement, }; return command;}그 다음, 어차피 리턴해 줄 command 객체를 굳이 변수 선언 해주는 것도 별로인 것 같다. 변수로 선언하지 않고, 곧장 리턴해준다.1234567891011export default function processRegistrationCommand(source = {}) { const phone = source.phone ? '+82' + source.phone.slice(1) : null; return { username: source.username || source.email, email: source.email, password: source.password, phone: phone || null, agreement: source.agreement, };}source.이 계속 반복 되는 것도 좀 별로다. 없애면 커맨드 객체 부분이 더 깔끔해질 것 같다.12345678910111213export default function processRegistrationCommand(source = {}) { const { username, email, password, phone, agreement } = source; // phone 변수 명이 중복되어 formattedPhone로 변경해주었다. const formattedPhone = phone ? '+82' + phone.slice(1) : null; return { username: username || email, email: email, password: password, phone: formattedPhone || null, agreement: agreement, };}formattedPhone도 굳이 변수 선언해주지 않고 바로 적용해도 괜찮을 것 같다. 게다가 :null과 ||null 부분 동일한 로직이 두 번 들어가 있다. 하나는 없애도 무방하다.1234567891011export default function processRegistrationCommand(source = {}) { const { username, email, password, phone, agreement } = source; return { username: username || email, email: email, password: password, phone: phone ? '+82' + phone.slice(1) : null, agreement: agreement, };}phone 속성의 '+82' + phone.slice(1) 를 템플릿 리터럴(Template literals)로 변경해줄 수도 있다. (이 부분은 개인적인 취향이므로 해도 그만, 안 해도 그만이다.)1234567891011export default function processRegistrationCommand(source = {}) { const { username, email, password, phone, agreement } = source; return { username: username || email, email: email, password: password, phone: phone ? `+82${phone.slice(1)}` : null, agreement: agreement, };}리팩터링이 끝났다. 코드가 처음보다 훨씬 깔끔하고 간결해졌다! 사실 첫 코드 자체가 그리 복잡하지도 지저분하지도 않았어서, 여지껏 테스트하고, 리팩터링하고 하며 들인 노력에 비해 크게 개선된 것 같지 않아 보일 수도 있겠다. 하지만 우리의 프로덕션 코드에 항상 이런 간략하고 복잡하지 않은 코드만 있는 것은 아니니까.. 분명 이렇게 테스트와 리팩터링을 반복하다보면 코드가 점점 직관적이게, 그리고 깔끔하게 변하는 것을 경험 하게 된다.++ 4. 스펙을 추가하자! (여기부터 TDD이다)새로운 비즈니스 요구 사항은 언제나 들어올 수 있다. 새로운 요구사항은 곧 새로운 기능이고, 기존에 없던 새로운 코드가 만들어져야 하는 순간이다. 이 때가 바로 TDD를 할 수 있는 황금 같은 기회이다. 아래와 같은 요구사항이 추가되었다고 가정해보자.유저로부터 국가 정보를 입력 받는다. 중국이면 +86을, 한국이면 +82를 적용한다.1. 먼저 테스트 코드부터 작성한다. (RED)1234567891011121314 // ... const param = { username: 'username', email: 'user@email.com', password: 'password', phone: '01040022068', agreement: [true, true, true], country: 'China', // 유저로부터 입력 받는 값이 추가 되었다. };// ... test('phone 속성은 국가 정보가 중국이면, +86이 포함된 문자열이다.', () => { const actual = processRegistrationCommand({ ...param, country: 'China' }); expect(actual.phone).toBe(`+86${param.phone.slice(1)}`); });유저로부터 입력 받는 값에 국가 정보가 추가 되었으므로, param Fixture에 속성을 추가한다. 그리고 새로운 요구사항을 스펙으로 추가하였다. 테스트 먼저 작성되었고 구현코드는 존재하지 않으니 테스트는 당연히 실패한다. TDD의 첫 번째 단계, Red이다.2. 프로덕션 코드를 수정한다. (GREEN)123456789101112131415161718192021222324// processRegistrationCommand.jsexport default function processRegistrationCommand(source = {}) { // source에 country 속성이 추가되었다. const { username, email, password, phone, agreement, country } = source; let countryCodeAdded; // country 속성의 값에 따라 국가 번호를 다르게 적용한다. if (!phone) { countryCodeAdded = null; } else if (country === 'China') { countryCodeAdded = `+86${phone.slice(1)}`; } else { countryCodeAdded = `+82${phone.slice(1)}`; } return { username: username || email, email: email, password: password, phone: countryCodeAdded, agreement: agreement, };}이렇게 프로덕션 코드를 추가해서 테스트를 성공시킨다. 이게 TDD의 두 번째, Green 단계이다. 코드가 좀 더럽더라도 참아야 한다. Green 단계에서 반드시 지켜야 하는 원칙이 하나 있는 데, 테스트가 성공할 만큼만 코딩하는 것이다. 리팩터링 단계가 괜히 있는 게 아니다. 이 단계에서는 많은 생각을 하지 않고, 테스트를 성공시키는 것에만 집중한다.3. 리팩터링 한다.(REFACTOR)드디어 리팩터링 단계이다. 위 코드를 좀 깨끗하게 정리해보자. 먼저, else if 문을 없애고 싶다.123456789101112131415161718192021222324export default function processRegistrationCommand(source = {}) { const { username, email, password, phone, agreement, country } = source; const countryCodes = { Korea: 82, China: 86, }; let countryCodeAdded; if (!phone) { countryCodeAdded = null; } else { countryCodeAdded = `+${countryCodes[country]}${phone.slice(1)}`; } return { username: username || email, email: email, password: password, phone: countryCodeAdded, agreement: agreement, };}country 속성을 통해 넘어오는 국가 명을 key로, 국가 번호를 value로 하는 객체를 생성하여 else if 문을 제거해주었다. 다음으로, if 문을 제거해준다.12345678910111213141516export default function processRegistrationCommand(source = {}) { const { username, email, password, phone, agreement, country } = source; const countryCodes = { Korea: 82, China: 86, }; return { username: username || email, email: email, password: password, phone: phone ? `+${countryCodes[country]}${phone.slice(1)}` : null, agreement: agreement, };}여기서 리팩터링한 phone 속성 부분은 본래 삼항연산자를 사용해 if문 없이 작성되어 있었는 데, 스펙이 추가되면서 오히려 지저분해졌었다. 이처럼 요구사항이 추가되면서 깔끔했던 코드가 오히려 퇴보하는 일은 생각보다 흔하게 발생한다. 이때 테스트가 있다면 리팩터링을 쉽고 안정적으로 할 수 있겠지만 테스트가 없다면? 혹여나 버그가 생길까 굉장히 두려움에 떨며 리팩터링을 하거나, 혹은 위험 요소를 만들지 않기 위해 아예 리팩터링을 포기해버릴 수도 있을 것이다. 포기를 선택한다면, 당연히, 요구 사항이 추가될 때 마다 코드는 끝도 없이 지저분해질 것이다. 물론 TDD를 한다면 걱정할 필요가 없다.완성된 코드는 아래에서 볼 수 있다.맺음이 글을 통해 말하고자 했던 바는 “TDD를 하세요!” 가 아니다. 테스트를 처음 한다면, 그리고 어디서부터 시작해야 할 지 모르겠다면, 기존에 이미 작성되어 있는 코드에 테스트를 추가하는 것부터 시작한다. 이 방법은 실제로 내가 시작했던 방식이고, 지금도 여전히 하고 있는 방법이다. 나는 아직도 테스트가 무지 어렵다. 테스트를 할 수는 있지만, 잘 하는 방법은 아직 잘 모른다. 그래도 계속 하고 있다. 뻘짓도 해보고 삽질도 해보면서, 깨달음도 얻고 더 좋은 방법을 터득해 나가는 중이다. 막막하고 모르겠다고 시작하지 않으면, 은퇴하는 그 날까지도 시작할 수 없을 지 모른다. 일단 시작부터 하자! 그리고 조급하게 생각하지 말자! 경력 30년된 우리 회사 CTO님이 테스트 케이스 만 개 정도는 작성해봐야 한다고 했다!","categories":[{"name":"Test","slug":"Test","permalink":"http://huusz.github.io/categories/Test/"}],"tags":[{"name":"Unit test","slug":"Unit-test","permalink":"http://huusz.github.io/tags/Unit-test/"},{"name":"React test","slug":"React-test","permalink":"http://huusz.github.io/tags/React-test/"},{"name":"TDD","slug":"TDD","permalink":"http://huusz.github.io/tags/TDD/"}]},{"title":"Unit test style","slug":"TIL180529-unit-test-pattern","date":"2018-05-29T14:18:53.000Z","updated":"2018-10-27T14:57:30.303Z","comments":true,"path":"2018/TIL/TIL180529-unit-test-pattern/","link":"","permalink":"http://huusz.github.io/2018/TIL/TIL180529-unit-test-pattern/","excerpt":"","text":"Setup, Exercise, Verify and Teardown (Four-phases test pattern)Given, When, Then (BDD)Arrange, Act, Assert(AAA)모두 기본적인 아이디어는 같다.[Setup/ Given/ Arrange] 테스트 할 대상에게, 테스트를 위해 사전에 필요한 조건들을 사전에 갖추게 하고 (기본 값, 파라미터, 선행 되어야 할 함수 실행 등)[Exercise/ When/ Act]테스트 대상 함수를 호출하고[Verify/ Then/ Assert]테스트 대상이 예상한 대로 작동하는지 확인한다.* Four-phases test pattern의 Teardown은 테스트에 의해 만들어진 fixture를 해제하는 단계로 필수는 아니다.ReferenceMeszaros - Four-phases test patternBill Wake - 3A(AAA)MS docs: AAA exampleBDD - Given When Thenmartinfowler.com: Given-When-Then","categories":[{"name":"TIL","slug":"TIL","permalink":"http://huusz.github.io/categories/TIL/"}],"tags":[{"name":"Unit test","slug":"Unit-test","permalink":"http://huusz.github.io/tags/Unit-test/"},{"name":"Test style","slug":"Test-style","permalink":"http://huusz.github.io/tags/Test-style/"},{"name":"AAA(3A)","slug":"AAA-3A","permalink":"http://huusz.github.io/tags/AAA-3A/"},{"name":"Arrange Act Assert","slug":"Arrange-Act-Assert","permalink":"http://huusz.github.io/tags/Arrange-Act-Assert/"},{"name":"Given When Then","slug":"Given-When-Then","permalink":"http://huusz.github.io/tags/Given-When-Then/"}]},{"title":"Semantic-ui-react와 css-module을 같이 사용할 수 없을까?","slug":"Log/01.dada-log-01","date":"2017-10-02T07:31:11.000Z","updated":"2018-07-16T12:28:06.674Z","comments":true,"path":"2017/Log/Log/01.dada-log-01/","link":"","permalink":"http://huusz.github.io/2017/Log/Log/01.dada-log-01/","excerpt":"","text":"삽질의 발단…팀 프로젝트를 시작하고 채택한 스타일링 방식은 CSS Framework인 semantic-ui-react로 큰 틀을 잡고, CSS로 세부적인 디자인을 하는 것이었다. 지금까지 css 작업을 누군가와 같이 해본 경험이 없었기 때문에, 몇 가지 우려되는 것이 있었다. 각각 다른 css파일에서 중복되는 클래스 네임이 존재하는 상황 같은 것 말이다 (…) 이런 상황을 사전에 방지하고자, className 앞에 파일명(컴포넌트 이름)을 붙이기로 하였다. 근데 … 너무 귀찮을 것 같았다. 모든 클래스네임을 수동으로 home-header home-header-search 이런 식으로 지정해 주는, 이런 걸 반복 해야 하다니?자동으로 해주는 게 없을까?css를 자동으로 모듈화 해주는 방법에 css-module이라는게 있다는 것을 알게 되었고, webpack에서 css-loader에 module: true 설정을 추가해주었다.CSS 프레임워크와는 공존할 수 없다.eject로 webpack config에 직접 설정을 추가하는 방식이었는데, semantic-ui-css가 적용되지 않는 문제가 생겼다.module: true 때문이다.How to use css-modules with other global css (discussion please don’t merge it)웹팩 설정에서 css-loader 설정을 module:true로 바꾸면, node-modules에 있는 semantic-ui-css(global css)도 component에 따라 class 이름을 변경하기 때문에 전혀 다른 클래스가 되어버리므로 (ex. .ui.icon-k1t4h#9) 결국 semantic-ui-css와의 연결이 끊긴다. 이 때문에 에러는 안 나지만, 전혀 다른 클래스를 적용한 꼴이 되어버리므로 시맨틱 스타일 적용한 것들이 없어져 버린 것..윗 글에서는 css-loader에 두 가지로 분류해서 로딩하는 방식으로 약간의 꼼수(?)를 사용했다. global로 쓸 css는 (내 경우 semantic-ui-css) 그대로 하되, local 한 모듈들에 쓸 css들은 app.module.css 와 같이 명명하여 별도로 처리하게 하는 방식이다.선택지는..1. Semantic-ui를 쓴다.semantic-ui + pure css를 쓴다. 클래스 네임을 직접. 수동으로. 명명해서 css를 모듈화 한다.semantic-ui + styled-components를 쓴다. (웹 팩 설정이 필요 없다.)위 방법 처럼 꼼수(?)를 써서 css-module + semantic-ui를 쓴다.2. Semnatic-ui를 안 쓴다.모든 것이 평화롭게 해결된다.사실 semantic-ui를 안 쓰고 싶지만, 당장 4일 내로 정적 페이지 제작이 끝나야 하므로 스타일링에 많은 시간을 쏟을 수 없었다. 프레임워크는 그대로 쓰기로 했다. 그러면 남은 선택지 중에서…2. styled-components?styled-components는 기존의 css문법과 약간 다르다. react와 css를 섞어 놓은 듯한 문법이라 학습+적응 시간이 필요할 것 같은데, 빠르게 페이지를 만들어야 하는 지금 상황에서 그다지 좋은 선택지는 아닌 것 같다. 게다가 시맨틱도 컴포넌트이고 styled-components도 컴포넌트라서 시맨틱 컴포넌트 내부에 스타일링을 적용해야 하는 경우에는 결국 인라인스타일을 쓰게 된다. 이런게 많아지면, styled-components를 적용하는 의미가 없어지는 것 같았다. 그럴바엔 그냥 전부 inline style로 하면 되는 것 아닌가? 라는 생각이 들었다.3. css-module + semantic-ui?어차피 css를 쓸거라면 위 방법을 써서 css-module을 사용하는 것도 좋겠지만, 사실 이것 하나 때문에 eject로 웹팩 config 파일 및 script 설정 파일 등등을 풀어헤친다는 게 석연치 않았다. (불안하기도 했고..)결국 처음 고민하던 시점으로 돌아가, pure css + css 프레임워크를 쓰기로 했다.프로젝트가 끝나가는 시점에서..위에서도 말했듯, 처음 고민하던 시점으로 돌아가서 pure css + css 프레임워크를 쓰려고 하였고 적용해보려는 시도도 했다. 그런데 결국 채택한 방식은 컴포넌트 인라인 스타일 + pure css + css 프레임워크 방식이다. styled 컴포넌츠를 약간 (정말 약간) 모방하여 Styled***.js라는 이름으로 인라인 스타일 객체만 모아둔 파일을 각 페이지 폴더 내부에 하나씩 두는 방식으로 하였다. 여기까지 오기까지 나름 치열한(?) 고민을 했기 때문에, 기록해두려고 한다.인라인 스타일 방식을 선택한 이유는,일단 className을 부여하는 방식으로는 semantic의 기본 스타일을 커스터마이징하는 데 한계가 있었다. semantic ui가 스타일링 하는 방식을 개발자 도구로 열어보면, 아래처럼 클래스 여러개를 중첩하여 선언하는 방식이다.12/* semantic-ui class(className) */.ui.icon.input input여기다가 직접 커스텀한 className을 부여하여도,1<Input className = \"foodSearchInput\" ... />123456789/* custom class(className) 추가하는 경우 */.foodSearchInput { border-radius: 100px; /* 이 코드는 무시된다. */}/* 중첩 선언 방식이 명시도 면에서 더 우위를 가져간다. */.ui.icon.input input { border-radius: .28571429rem; /* 이 코드가 적용된다. */}결국 캐스캐이딩(cascading) 우선 순위에서 밀리기 때문에 (명시도 면에서 중첩 선언된 기존의 semantic 방식보다 더 하위가 된다.) 적용되지 않는 경우가 많았다.커스텀 css의 캐스캐이딩 우선 순위가 semantic-ui 프레임워크보다 항상 우위에 있도록 할 수 있는 방법은 없을까?일단 cascading의 명시도 면에서 !important > 인라인 스타일 > 아이디 선택자 > 클래스/어트리뷰트/가상 선택자 > 태그 선택자 > 전체 선택자 > 상위 요소에 의해 상속된 속성 순으로 우선 순위에서 우위를 가져간다. 여기서, !important를 사용하는 방법은 최대한 배제하기로 하였다. 그러면 남은 것은 인라인 스타일과 아이디 선택자를 사용하는 방법이 남는다.아이디 선택자 (X)id 선택자는 단 하나의 요소에만 부여할 수 있는 고유한 것이다.따라서 코드의 재사용이 불가함은 물론이고, 유사 스타일에 대한 확장 가능성이 전혀 없기 때문에 인라인 스타일에 비해 비효율적이다.프로젝트 내에서 예를 들면, 검색창과 같은 경우, 유사한 스타일 코드를 다른 페이지의 input 요소들과 공유하게 되는데, id선택자를 이용하면 같은 스타일을 여러번 반복해서 선언해주어야 한다.1234567891011121314151617/* 아이디 선택자를 사용할 경우 같은 혹은 유사 스타일 일지라도 반복해서 선언해주어야 한다. */#searchInput{ width: 400px; margin: 20px 10px; border-radius: 100px; position: absolute; left:300px;}#searchResultInput{ width: 800px; margin: 20px 10px; border-radius: 100px; position: absolute; left: 100px; top: 400px;}123456789101112131415// 인라인 스타일의 경우 spread 문법을 이용한 스타일의 확장이 가능하다.export const searchInput { width: '400', margin: '20 10', borderRadius: '100', position: 'absolute', left: '300',}export const searchResultInput { ...searchInput, width: '800', left: '300', top: '400'}인라인 스타일 (O)인라인 스타일은 캐스캐이딩에서 !important를 제외하고 두 번째 우선순위를 갖는다. 따라서 중첩된 클래스보다 항상 우위에 있을 것이고, 위 예제처럼 아이디 선택자에 비해 재사용 및 확장이 용이하기 때문에 효율적이고 문제가 없을 것이라 생각했다. 그런데 여기에도 문제가 있었다.대부분의 컴포넌트가 div로 실제 시맨틱 태그를 감싸고 있어 인라인 스타일을 주어도 적용되지 않는 경우가 많았다. 예를 들어 Input이나 Search 컴포넌트의 경우, <input> 태그를 <div></div>가 감싸고 있다.123<div> <input type=\"text\"/></div>이런 식이라 인라인으로 스타일을 부여하면 <input>이 아니라 가장 바깥의 <div>로 스타일 코드가 적용되었다.123<div style=\"border: 1px solid blue;\"> <input type=\"text\"/></div>따라서 상속이 되는 css 속성에 한해서만 (ex. font/color/text-align 등 비교적 레이아웃과 관련되지 않는 속성들) 적용이 되었고, 정작 필요한 border 속성이나 margin, padding, position과 같은 속성은 적용할 수 없었다.즉, 인라인 스타일이나 className 중 하나만 선택하는 방식으로는 발생하는 모든 문제를 해결할 수 없었다. 그래서, 그나마 문제가 거의 발생하지 않는 인라인 스타일을 주 스타일링 방식으로 채택하였다. 그리고 인라인으로 해결되지 않는 몇몇 문제들에 한해서만, semantic ui에서 정의한 클래스의 css를 아래와 같이 커스텀 css에서 재정의해주는 방식을 채택하였다.1234567891011/* search.css(custom css) */.ui.icon.input input{ border-radius: 100px /* 이 코드가 적용된다. */}/* semantic.min.css */.ui.icon.input input { padding-right: 2.67142857em!important; /* ... */ border-radius: .28571429rem /* 이 코드는 무시된다. */}사실 Object 형태의 인라인 스타일 방식을 주 스타일링 방식으로 채택하게 되기까지 React.js + CSS라는 글이 꽤나 큰 동기 부여가 되기도 하였다. styled-components의 존재를 알게 된 시점부터 css-in-js 라는 스타일링 방식이 어떤 이유로 나오게 되었으며, 어떤 이점을 갖는 지에 대해 궁금했었고, 윗 글이 어느 정도 대답이 되었기 때문이다.이 방식을 채택함으로써 가장 큰 수혜(?)를 받은 부분이 Navigation 컴포넌트였다. 프로젝트의 디자인 시안 상으로, 페이지 상단 네비게이션(메뉴)바의 폰트와 보더(border) 색상을 적용함에 있어 두 가지 케이스가 있었다.흰 배경인 경우: #16325c사진이나 동영상이 배경인 경우: #fff1234567// components/Navigation/index.js// default Props: 컬러 반전이 없는 일반 페이지의 default color// props를 전달하지 않는 경우 color: '#16325c'Navigation.defaultProps = { color: '#16325c'}디자인 상 대부분의 화면이 흰 배경이므로, 이를 기본으로 설정하기 위해 위와 같이 defaultProps로 color 스타일을 지정하였다. 그리고 아래처럼 특정 조건에 따라 color와 borderColor를 다르게 적용하도록 하였다.12// pages/HomePage/index.js<Navigation color=\"#fff\" />123456789101112131415161718192021222324// components/Navigation/RightMenu/index.js<Menu.Item style={ // active일때만 border-color 적용 isLinkMatched ? { ...linkTagWrap, // linkTagWrap은 아래 참고 borderColor: this.props.color, //#fff } : { linkTagWrap } active={isLinkMatched}> <Link style={{ ..linkTag, color: this.props.color, // #fff }} to={route.linkTo} > {route.linkLabel} </Link></Menu.Item>123456// components/Navigation/StyledNavigation.jsexport const linkTagWrap = { padding: '7px', marginLeft: '37px', marginBottom: '6px',}이렇게 css를 변수(또는 props)로 직접 이식하는 부분이 굉장히 매력적이었다. 그리고 이런 방식은 굳이 css-in-js방식의 라이브러리를 사용하지 않아도 가능하였고, 이보다 더 복잡하고 많은 스타일링이 필요할만큼 앱이 거대하지도 않았다. 어찌됐든, 이러한 고민들을 거쳐 최종적으로 인라인 스타일 + css 방식을 사용하였다.언젠간 css와 js 스타일링 방식의 차이에 대해서 나름대로 정리한 내용을 기록하려고 한다. (언제가 될지는 모르겠다..) css-in-js 방식을 차용한 스타일링 라이브러리들을 찾아보면서 Aphrodite, CSS in JS, Emotion.js 등등 다양한 라이브러리들을 알게 되었는데, 개인적으로 Emotion.js나 Styled-components를 꼭 한번 사용해보고 싶다.","categories":[{"name":"Log","slug":"Log","permalink":"http://huusz.github.io/categories/Log/"}],"tags":[{"name":"semantic-ui-react","slug":"semantic-ui-react","permalink":"http://huusz.github.io/tags/semantic-ui-react/"}]}]}