일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- max-width
- 그럼에도불구하고
- 코드업
- @media
- node
- JavaScript
- TypeScript
- webpack
- 변수
- 코딩테스트
- react-router-dom
- github
- react
- coding
- HTML
- 자바문제풀이
- frontend
- JS
- 자바
- cleancode
- CSS
- 그럼에도 불구하고
- 프론트엔드
- git
- media query
- 반응형 페이지
- node.js
- redux
- Servlet
- java
- Today
- Total
그럼에도 불구하고
[CleanCode] 객체 다루기 본문
오늘은 객체를 다루는 여러 가지 방법에 대해 알아보겠습니다.
목차
[ Shortan Properties Name ]
객체를 사용하여 key : value를 지정할 때 변경 전과 같이 지정하는 경우를 종종 볼 수 있는데요. ( 저만 그런가요..? ㅎㅎ )
변경 후와 같이 더 깔끔하게 지정해 줄 수 있습니다.
그리고 ES2015 이후부터는 key와 value의 이름이 같다면 한 번만 써도 된다는 거 잊지 말아 주세요!!
📌 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 변경 전 const person = { name : name, age : age, getInfo : getInfo, }; // 변경 후 const person = { name, age, getInfo }; | cs |
[ Computed Property Name ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * Computed Property Name */ import React, { useState, Fragment } from 'react'; const [state, setState] = useState({ id: '', password: '', }); const handleChange = e => { setState({ [e.target.name] : e.target.value, // [e.target.name]과 같이 property를 동적으로 다룰 수 있음 }); }; return ( <React.Fragment> <input value={state.id} onChange={handleChange} name="password"/> <input value={state.password} onChange={handleChange} name="name"/> </React.Fragment> ) | cs |
react에서 자주 사용하는 구문의 예시를 가져와봤습니다.
handleChange 메서드의 setState안에 지정하는 객체의 값을 보면 key를 [e.target.name]과 같이 property를 동적으로 다루는 것을 볼 수 있습니다.
이렇게 하면 input태그에 각각 onChange 메서드를 만들어 사용할 필요가 없이 하나의 메서드로 둘 다 이용할 수 있게 됩니다.
이 방법은 react의 reducer에서도 마찬가지입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * Computed Property Name */ const noop = createAction('INCREMENT'); const reducer = handleActions( { [noop]: (state, action) => ({ counter : state.counter + action.payload, }), }, { counter : 0}, ); | cs |
[ Lookup Table (순람표) ]
혹시 엑셀의 Lookup Table을 본 적 있으신가요?
엑셀을 사용해보지 않으셨을 분들을 위해 간단하게 말씀드리자면 Lookup Table은 key와 그에 따른 value가 정해진 테이블을 말합니다.
즉. 데이터의 구조가 key와 value로 나열된 것을 말합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | /** * Object Lookup Table */ // 변경 전 function getUserType1(type) { if (type === "ADMIN") { return "관리자"; } else if (type === "INSTRUCTOR") { return "강사"; } else if (type === "STUDENT") { return "수강생"; } else { return "해당 없음"; } } // 개선 1 (보다 깔끔하지만 case가 추가될 때마다 나열되는건 다름없다.) function getUserType2(type) { switch (key) { case "ADMIN": return "관리자"; case "INSTRUCTOR": return "강사"; case "STUDENT": return "수강생"; case "STUDENT2": return "수강생"; case "STUDENT3": return "수강생"; default: return "해당 없음"; } } // 개선 2 (개인적으로 이게 가장 좋다.) function getUserType3(type) { const USER_TYPE = { ADMIN: "관리자", INSTRUCTOR: "수강생", STUDENT: "학생", UNDEFINED: "해당 없음", }; return USER_TYPE[type] ?? USER_TYPE.UNDEFINED; // ver 1 // return USER_TYPE[type] ?? USER_TYPE["UNDEFINED"]; ver 2 } console.log(getUserType3("ADMIN")); // 관리자 console.log(getUserType3("qesf")); // 해당 없음 // 개선 3 function getUserType4(type) { return { ADMIN: "관리자", INSTRUCTOR: "수강생", STUDENT: "학생", }[type] ?? "해당 없음"; } console.log(getUserType4("ADMIN")); // 관리자 console.log(getUserType4("qesf")); // 해당 없음 | cs |
위와 같이 type에 따른 출력값을 반환해주어야 할 때, 처음 코딩을 배워보는 개발자나 학생들의 경우 보통 else if 구문을 많이 이용합니다.
하지만 type이 추가됨에 따라 계속되는 else if의 추가는, 명시적으로 봤을 때 바람직하지는 않습니다. 그렇기 때문에 보완하고자 switch문을 사용하면 보다 깔끔하지만, 이 역시 case가 추가될 때마다 나열되는 건 다름없습니다.
그럴 때는 객체를 이용하면 보다 깔끔하게 작성할 수 있습니다.
개선 2의 경우 객체로 key : value 값을 지정해 준 후 네이밍 체이닝 연산자를 통해 결과 값을 반환하고 있습니다.
이렇게 작성하면 새로운 타입이 추가될 때마다 key: value 값만 지정해 주면 됩니다.
또한 이 객체를 외부 파일로 빼놓은 후 모듈화 해서 따로 관리하기도 용이합니다. :)
[ Object Destructuring ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /** * Object Destructuring */ // 변경 전 function Person1(name, age, location) { this.name = name; this.age = age; this.location = location; } const zenghyun1 = new Person1("zenghyun", 27, "korea"); // 개선 1 function Person2({ name, age, location }) { this.name = name; this.age = age ?? 27; // ?? 를 사용하여 null이나 undefined일 경우 27 반환 this.location = location ?? 'korea'; // ?? 를 사용하여 null이나 undefined일 경우 korea를 반환 } const zenghyun2 = new Person2({ age: 27, name: "zenghyun", location: "korea", }); // 이렇게 바꾸면 매개변수의 순서를 지키지 않아도 되고, age와 location의 경우 값을 넣지 않아도 된다. // 개선 2 function Person3( name, { age, location }) { // name은 필수 this.name = name; this.age = age; this.location = location; } const zenghyunOptions = ({ age: 27, location: "korea", }); const zenghyun3 = new Person3('zenghyun', zenghyunOptions); | cs |
변경 전의 함수 구성은 흔히들 가장 쉽게 작성하는 로직입니다만, 매개변수를 순서대로 입력을 해주어야 한다는 단점과, 기본 값을 지정해주지 않았기 때문에 값을 입력하지 않으면 오류가 발생할 수 있습니다.
이럴 때 아래의 개선과 같은 방식을 유지보수에 있어 보다 깔끔하게 이용할 수 있습니다.
매개변수의 순서를 지키지 않아도 되고, 특정 인자의 값을 넣지 않아도 상관없게 되죠
다른 예시를 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * Object Destructuring */ // 변경 전 const orders = ["First", "Second", "Third"]; const [first, , third] = orders; console.log(first); // First console.log(third); // Third // 변경 후 (이렇게도 가능하다.) const { 0: st, 2: rd } = orders; console.log(st); // First console.log(rd); // Third | cs |
배열 안에서 원하는 값을 따로 지정하고 싶을 때 아래와 같은 방법을 사용하고는 합니다.
const orders = ["First", "Second", "Third"];
const [first, , third] = orders;
이 방법도 문제는 없지만 객체 분해할당을 이용해 보는 건 어떨까요?
객체 분해할당을 이용하면 기존의 방식에서 내가 필요하지 않은 값을 ,를 통해 공백으로 나열해야 했던 작업을 하지 않아도 됩니다.
[ Object.freeze ]
객체 안의 값을 유지하기 위해서 사용하는 Object.freeze의 경우 shallow copy만 가능하기 때문에 deep copy와 같은 경우 개발자가 원하는 대로 동작하지 않는 결과를 나타내고는 합니다.
이럴 때는 deep copy에 대한 대안책을 만들어서 사용할 수 있고, 혹은 TypeScript의 readonly를 통해 간단하게 해결할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | /** * Object.freeze * * 1. 대중적인 유틸 라이브러리 사용 (lodash) * 2. 직접 유틸 함수 생성 * 3. TypeScript => readonly */ const STATUS = Object.freeze({ // shallow copy PENDING: 'PENDING', SUCCESS: 'SUCCESS', FAIL: 'FAIL', OPTIONS: { GREEN : 'GREEN', RED: 'RED', }, }); STATUS.PENDING = 'pending'; STATUS.PLUS = 'PLUS'; console.log( STATUS ); console.log( Object.isFrozen(STATUS) ); // shallow copy vs deep copy // Object.freeze는 shallow copy를 한다. STATUS.OPTIONS.GREEN = 'green'; STATUS.OPTIONS.YELLOW = 'YELLOW'; console.log( Object.isFrozen(STATUS.OPTIONS) ); console.log( STATUS.OPTIONS); // 수정됨 // deep freeze 직접 구현 function deepFreeze(targetObj) { /** * 1. 객체를 순회 * 2. 값이 객체인지 확인 * 3. 객체이면 재귀 * 4. 그렇지 않으면 Object.freeze */ // 예시 Object.keys(kargetObj).forEach(key => { // if(/* 객체가 맞다면 */) { // deepFreeze(targetObj[key]) // } }) return Object.freeze(targetObj); } | cs |
Object.freeze에 대해 보다 더 자세하게 알고 싶으면 아래의 게시물을 참고해 주세요!
https://despiteallthat.tistory.com/59
[ Prototype 조작 지양하기 ]
예전에는 prototype을 조작하여 내가 원하는 메서드를 직접 만들고는 했지만 요즘 같이 JS가 많이 발전된 환경에서는 위험한 결과를 초래할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /** * Prototype 조작 지양하기 * * 1. 이미 JS는 많이 발전했다. * 1-1. 직접 만들어서 모듈화 * 1-2. 직접 만들어서 모듈화 => 배포 => NPM * 2. JS 빌트인 객체를 건들지말자 */ // 클래스가 생기기 이전 function Car(name, brand) { this.name = name; this.brand = brand; } Car.prototype.sayName = function () { return this.brand + "-" + this.name; }; // 클래스가 생긴 이후 class Car{ constructor(name, brand) { this.name = name; this.brand = brand; } sayName() { return this.brand + "-" + this.name; } }; const casper = new Car("캐스퍼", "현대"); console.log(casper); | cs |
[ hasOwnProperty ]
자바스크립트는 프로퍼티 명칭으로서 hasOwnProperty를 보호하지 않습니다. 그러므로 이 명칭을 사용하는 프로퍼티를 가지는 개체가 존재하려면, 올바른 결과들을 얻기 위해서는 외부 hasOwnProperty를 사용해야 합니다.
hasOwnProperty를 사용하려고 호출했을 때 다른 hasOwnProperty가 호출될 수 있어 위험합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /** * hasOwnProperty */ const person = { name : 'zenghyun', }; console.log( person.hasOwnProperty('name') ); // true console.log( person.hasOwnProperty('age') ); // false const foo = { hasOwnProperty: function() { return 'hasOwnProperty'; }, bar: 'string', }; // hasOwnProperty은 보호받지 못한다. console.log( foo.hasOwnProperty('bar') ); //' hasOwnProperty' console.log( Object.prototype.hasOwnProperty.call(foo, 'hasOwnProperty')); // true | cs |
[ 직접 접근 지양하기 ]
객체를 직접 건드는 영역을 분리하지 않고 호출하는 것과, 객체를 직접 건드는 영역을 분리한 후에 호출하는 방법에는 차이가 있습니다.
객체를 직접 건드는 영역을 분리하여 호출했을 경우 모델에 접근하는 권한을 확실히 설정할 수 있고, 어디서나 모델에 접근할 수 있던 방법을 지양하고 함수에 위임하여 추상화할 수 있습니다.
이런 방법을 이용하면 모델이 어디서 어떻게 변화하는지도 추적 가능하고 모델을 보다 안전하게 사용할 수 있습니다.
아래의 예시를 살펴보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * 직접 접근 지양하기 * flux => action => reducer => state 변화 * 예측 가능한 코드를 작성해서 동작이 예측 가능한 앱을 만들자 */ const model = { isLogin: false, isValidToken: false, }; // model 직접 접근 불가 function login() { model.isLogin = true; model.isValidToken = true; // model 직접 접근 불가 function logout() { model.isLogin = false; model.isValidToken = false; } someElement.addEventListener('click', login); | cs |
위의 코드는 login 메서드와 logout 메서드를 통해 모델에 직접 접근하고 있습니다. 이를 구조화해서 살펴보면,
login & logout
1. model에 직접 접근한다.
2. model 객체의 isLogin과 isValidToken의 boolean 값을 변경한다.
겉보기에는 문제가 없어 보일 수도 있지만, 하나의 함수는 최소한의 기능을 수행한다는 개념에서 살펴보면 지금 함수는 두 개의 기능을 하고 있습니다.
model에 직접 접근하는 함수를 따로 분리하고, login과 logout에서는 그 함수를 호출하는 방식으로 바꾸면 어떨까요?
또한 이 방법을 이용하여 아래와 같이 setLogin과 setValidToken 메서드를 따로 만들어 놓으면 이 메서드를 실행할 때마다 serverAPI의 log를 출력하는 작업도 할 수 있습니다. ( 할 수 있다의 가능성을 염두에 두기보다는 메서드의 이름에 맞는 보다 직관적인 역할을 수행할 수 있죠 )
serverAPI의 log를 만약 기존의 방식에서 login과 logout에서 이용한다면, 혹은 다른 기능을 추가한다면
하나의 함수는 최소한의 기능을 수행한다는 개념에 더욱 거리가 멀어지게 되니까요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * 직접 접근 지양하기 * flux => action => reducer => state 변화 * 예측 가능한 코드를 작성해서 동작이 예측 가능한 앱을 만들자 */ // 이 model에 너무 쉽게 접근하고 있다. 모델에 직접 접근 지양 const model = { isLogin: false, isValidToken: false, }; // model 대신 접근 function setLogin(bool) { model.isLogin = bool; serverAPI.log(model.isLogin); } // model 대신 접근 function setValidToken(bool) { model.isValidToken = bool; serverAPI.log(model.isValidToken); } // model 직접 접근 불가 function login() { setLogin(true); setValidToken(true); } // model 직접 접근 불가 function logout() { setLogin(false); setValidToken(false); } someElement.addEventListener('click', login); | cs |
[ Optional (선택적) Chaning (?.) ]
앞선 cleancode 게시물에서 언급했던 Optional Chaning 연산자와 Nullish Coalescing 연산자는 아래의 게시물에서 보다 자세히 확인할 수 있습니다.
https://despiteallthat.tistory.com/54
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /** * Optional Chaining (?.) */ const obj = { name: 'value' }; console.log( obj?.name ); const response = { data : { userList: [ { name: 'Lee', info: { tel: '010', email: 'zenghyun@naver.com' } } ] } }; console.log( response.data.userList[0].info.email ); // 만약에서 서버에서 데이터를 가져올 때 특정 부분이 유실된 상태라면? // 특정 부분 유실에 대한 if문을 이용하면 아래와 작성할 수 있다. 이는 꽤나 번거롭고, 복잡하다. function getUserEmailByIndex(userIndex) { if(response.data) { if(response.data.userList) { if(response.data.userList[userIndex]) { // some code } } } // 옵셔널 체이닝을 이용하면 오류에 보다 깔끔하게 대응한다. function getUserEmailByIndex(userIndex) { if(response?.data?.userList?.[userIndex]?.info?.email) { return response.data.userList[userIndex].info.email; } return '알 수 없는 에러가 발생했습니다.'; } console.log( getUserEmailByIndex(0) ); // zenghyun@naver.com console.log( getUserEmailByIndex(1) ); // 알 수 없는 에러가 발생했습니다. | cs |
'JavaScript > Clean code' 카테고리의 다른 글
[CleanCode] 추상화하기 (0) | 2023.06.05 |
---|---|
[CleanCode] 함수 다루기 (1) | 2023.06.04 |
[CleanCode] 배열 다루기 (0) | 2023.06.01 |
[CleanCode] 분기다루기 (0) | 2023.05.30 |
[CleanCode] 경계 다루기 (0) | 2023.05.25 |