모던 자바스크립트의 엘레강스한 패턴: RORO(Elegant patterns in modern JavaScript: RORO)

나는 자바스크립트 언어가 개발 된 지 얼마되지 않았을 때, 처음 몇 줄의 코드를 써봤다. 그 때, 언젠가 자바스크립트에서 엘레강스한 패턴에 대한 글을 쓰게 될 거라고 말했다면, 당신은 웃어 넘겼을 것이다. 자바스크립트를 간신히 "진짜(real) 프로그래밍" 자격이 있는 이상한 언어로 나는 생각했다.
20년 동안 많은 변화가있었다. 나는 이제서야 더글러스 크락포드 (Douglas Crockford)가 JavaScript: The Good Parts: “An outstanding, dynamic programming language … with enormous, expressive power.”을 썼을 때 보았던 자바스크립트로 보이게 됐다.
그래서 더 이상 걱정할 것 없이, 내가 최근에 사용하는 작지만 멋진 패턴을 소개하겠다. 당신도 나만큼 이 패턴을 좋아 했으면 좋겠다.
주의 사항: 여기에 소개하는 모든 것은 내가 만든 것이 아니다. 나는 다른 사람들의 코드에서 이러한 것들을 발견했고, 그것을 나의 것으로 만들었다.

객체를 받아서, 객체를 반환(RORO): Receive an object, return an object (RORO)

내가 작성하는 대부분의 함수는 이제 object 타입의 단일 파라미터를 받아서 대부분 object를 리턴한다.
ES2015에서 소개 된 destructuring 기능 덕분에 이것이 강력한 패턴이라는 것을 알게되었다. 나는 여기에 웃긴 이름을 지어주었다. 그 이름은 바로 "RORO"다.¯ \ _ (ツ) _ / ¯
참고: Destructuring은 내가 모던 자바스크립트에서 가장 좋아하는 기능 중 하나이다. 우리는 이 글 전반에 걸쳐 이것을 꽤 활용할 것이다. 그러니 이 기능에 익숙하지 않다면, 여기에 여러분이 속도를 낼 수 있도록 해 주는 간단한 영상이 있다.
당신이 이 패턴을 좋아할 이유는 다음과 같다:
  • 네임드 파라미터(Named parameters)
  • 더 명확한 디폴트 파라미터(Cleaner default parameters)
  • 더 풍부한 리턴 값(Richer return values)
  • 쉬운 함수 컴포지션(Easy function composition)
각각을 살펴 보자.

네임드 파라미터(Named Parameters)

주어진 역할(Role)에 있는 사용자(Users) 목록을 리턴하는 함수가 있다고 가정하고, 각 사용자의 연락처 정보와 비활성 사용자를 포함하는 또 다른 옵션을 제공해야한다고 가정 해 보자. 일반적으로 다음과 같이 작성할 수 있다:
function findUsersByRole ( role, withContactInfo, includeInactive ) {...}
이 함수 호출은 다음과 같이 한다:
findUsersByRole( 'admin', true, true )
마지막 두 파라미터가 얼마나 모호한지 보자. "true, true"가 무엇일까?
우리 앱에서는 연락처(Contact Info)는 거의 필요 없지만 비활성 사용자(Inactive Users)는 대부분의 경우 필요하다면 어떻게 될까? 필요가 없는 경우에도 우리는 그 중간(두번째) 파라미터를 항상 넣어주어야 한다 (나중에 이것에 대해서 다루겠다).
간단히 말해서, 이 전통적인 접근법은 우리에게 모호하고 시끄러운(noisy) 코드를 만든다. 이 코드는 이해하기 어렵고 작성하기가 더 까다롭다.
그 대신 하나의 객체를 받으면 어떻게되는지 보자:
function findUsersByRole ({ role, withContactInfo, includeInactive }) {...}
함수 파라미터 주위에 중괄호를 두었다는 점을 제외하고는 거의 동일해 보인다. 이는 세 개의 별개 파라미터를 받는 대신 우리 함수가 role, withContactInfo, 과 includeInactive라는 속성(property)을 가진 단일 객체를 기대한다는 것을 나타낸다.
이는 ES2015에서 소개 된 자바스크립트의 Destructuring 기능으로 인해 작동한다.
이제 다음과 같이 함수를 호출 할 수 있다:
findUsersByRole({ role: 'admin', withContactInfo: true, includeInactive: true })
이것은 모호하지 않고 읽고 이해하기가 훨씬 쉽다. 게다가 파라미터를 생략하거나 재정렬하는 것은 더 이상 문제가 아니다. 이제는 객체의 명명 된 속성이기 때문이다.
예를 들어 다음과 같이 할 수 있다:
findUsersByRole({ withContactInfo: true, role: 'admin', includeInactive: true })
그리고 이렇게 사용 해도 된다:
findUsersByRole({ role: 'admin', includeInactive: true })
또한 이전 코드를 손상시키지 않고 새 파라미터를 추가 할 수 있다.
한 가지 중요한 사항은 모든 파라미터를 선택적(optional)으로 지정하려는 경우, 즉 아래의 호출이 유효하다면...
findUsersByRole()
... 파라미터 객체의 기본값을 설정해야한다. 예를 들면 다음과 같다:
function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) {...}
파라미터 객체에 대한 destructuring 사용의 추가 이점은 불변성을 장려한다는 것이다. 우리가 함수로가는 도중에 object를 destructuring 할 때 우리는 객체의 속성을 새로운 변수에 할당한다. 이러한 변수의 값을 변경해도 원래 객체는 변경되지 않는다.
다음을 확인해보자:
const options = { role: 'Admin', includeInactive: true }
findUsersByRole(options)
function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { role = role.toLowerCase() console.log(role) // 'admin' ... }
console.log(options.role) // 'Admin'
role 값을 변경하더라도 options.role의 값은 변경되지 않은 채로 남게 된다.
수정: 파라미터 객체의 속성 중 하나가 복잡한 타입 (예 : array 또는 object) 인 경우 destructuring은 얕은(shallow) 복사를 하기 때문에 원본이 변경 될 수 있다.
(Yuri Homyakov 가 이 부분을 지적했다)
지금까지 좋은가?

더 명확한 디폴트 파라미터(Cleaner default parameters)

ES2015 자바스크립트 함수를 사용하면 디폴트 파라미터(default parametters)를 정의 할 수있다. 실제로 우리는 위의 findUsersByRole 함수의 파라미터 객체에 ={}을 추가 해서 디폴트 파라미터를 사용했다.
전통적인 디폴트 파라미터를 사용하면 findUsersByRole 함수가 다음과 같다.
function findUsersByRole ( role, withContactInfo = true, includeInactive ) {...}
includeInactivetrue로 설정하려면 withContactInfo을 명시적으로 undefined로 전달해야 디폴트 값을 유지할 수 있다:
findUsersByRole( 'Admin', undefined, true )
얼마나 끔찍한 일인가?
다음과 같이 파라미터 객체를 사용하는 것과 비교해보자:
function findUsersByRole ({ role, withContactInfo = true, includeInactive } = {}) {...}
이제 이렇게 쓸 수 있다:
findUsersByRole({ role: ‘Admin’, includeInactive: true })
… 그리고 우리의 withContactInfo 의 디폴트 값은 보존된다.

보너스: 필수 파라미터

당신은 얼마나 자주 이런 코드를 작성 하는가?
function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { if (role == null) { throw Error(...) } ... }
참고: 위의 == (double equals)를 사용하여 null과 undefined를 한번에 테스트한다.
위의 처럼 사용하는 대신, 디폴트 파라미터를 사용하여 필수 파라미터의 유효성을 검사 할 수 있다면 어떨까?
먼저, Error를 던지는(throw) requiredParam() 함수를 정의해야 한다.
이런식으로:
function requiredParam (param) { const requiredParamError = new Error( `Required parameter, "${param}" is missing.` )
// preserve original stack trace if (typeof Error.captureStackTrace === ‘function’) { Error.captureStackTrace( requiredParamError, requiredParam ) }
throw requiredParamError }
무슨 말을 하려는지 안다. requiredParam은 RORO가 아니다. 그래서 내가 많은 나의 함수라고 했지, 전부라고 하지 않은 것이다.
이제 requiredParam 호출을 role의 디폴트 값으로 설정할 수 있다. 예를 들면 다음과 같다:
function findUsersByRole ({ role = requiredParam('role'), withContactInfo, includeInactive } = {}) {...}
위의 코드에서 누군가가 role 에 값을 주지 않고 findUsersByRole을 호출하면 Required parameter, “role” is missing.라는 Error 메세지를 보여주게 된다.
이 테크닉을 일반적인 파라미터 변수와 함께 사용할 수도 있다. 반드시 객체를 사용 해야 할 필요는 없다. 그러나 이 트릭은 그냥 넘어가기엔 너무 유용하기 때문에 여기에서 다뤘다.

더 풍부한 리턴 값(Richer Return Values)

자바스크립트 함수는 단일(single) 값만 리턴 할 수 있다. 그 값이 object라면 더 많은 정보를 포함 할 수 있다.
User를 데이터베이스에 저장하는 함수를 생각해보자. 이 함수가 객체를 리턴하면 호출자에게 많은 정보를 제공 할 수 있다.
예를 들어, 저장 기능에서 데이터를 저장하는 일반적인 패턴은 "upsert"또는 "merge"하는 것이다. 즉, 데이터베이스 테이블에 행을 삽입하거나 (존재하지 않는 경우) 행을 업데이트한다 (존재하는 경우).
이러한 경우, Save 함수가 수행하는 작업이 INSERT 인지 UPDATE인지를 알면 편하다. 또한 데이터베이스에 저장된 내용을 정확히 나타내는 것이 좋을 것이며 작업 상태를 아는 것이 좋다. 성공 했는지, 더 큰 트랜잭션의 일부로 펜딩(pending) 중인지, 시간이 초과 되었는지?
객체를 리턴 할 때 모든 정보를 한 번에 쉽게 전달할 수 있다.
이런 식으로:
async saveUser({ upsert = true, transaction, ...userInfo }) { // save to the DB return { operation, // e.g 'INSERT' status, // e.g. 'Success' saved: userInfo } }
기술적으로, 위의 함수는 객체를 resolve하는 Promise 를 리턴 하지만, 위의 함수가 무엇을 말하는지 알 것이다.

쉬운 함수 컴포지션(Easy function composition)

"함수 합성(function composition)은 두 가지 이상의 함수를 결합하여 새로운 함수를 만드는 과정이다. 함수를 함께 합성하는 것은 우리의 데이터가 흐르도록 일련의 파이프를 결합하는 것과 같다. "- Eric Elliott
다음과 같은 pipe 함수를 사용하여 함수를 합성 할 수 있다:
function pipe(...fns) { return param => fns.reduce( (result, fn) => fn(result), param ) }
위의 함수는 함수 리스트를 받아서 주어진 파라미터에서 시작한 다음 리스트의 각 함수 결과를 리스트의 다음 함수로 전달하는, 왼쪽에서 오른쪽으로 리스트를 적용 할 수 있는 함수를 리턴한다.
혼란스럽겠지만 걱정하지 말자. 아래에 예를 보면 좀 더 이해가 될 것이다.
이 접근법의 한 가지 제약 사항은 목록의 각 함수가 하나의 파라미터만 받아야 한다는 것이다. 다행히도, 우리가 RORO 할 때 문제가되지 않는다!
function saveUser(userInfo) { return pipe( validate, normalize, persist )(userInfo) }
validate, normalizepersist 함수에서 rest parameter를 사용하여 각 함수가 필요로하는 값만 destructuring해서 사용 할 수 있고, 모든 것을 호출자(caller)에게 다시 전달할 수 있다.
여기 간단한 코드를 살펴보자:
function validate( id, firstName, lastName, email = requiredParam(), username = requiredParam(), pass = requiredParam(), address, ...rest ) { // do some validation return { id, firstName, lastName, email, username, pass, address, ...rest } }
function normalize( email, username, ...rest ) { // do some normalizing return { email, username, ...rest } }
async function persist({ upsert = true, ...info }) { // save userInfo to the DB return { operation, status, saved: info } }

RO 할 것인가 Ro 하지 않을 것인가, 그것이 문제로다.

처음에 대부분의 내가 작성한 함수는 객체를 받아서 많은 경우 객체를 리턴한다고 말했다.
다른 패턴과 마찬가지로, RORO는 우리가 사용하는 도구 중 하나로 보아야 한다. 우리는 파라미터의 목록을 보다 명확하고 융통성있게 만들고 리턴 값을 보다 표현식으로 만들어 가치를 더하기 위해 사용한다.
단일 파라미터만 받아도 되는 함수를 작성하는 경우에 object를 받는 것은 지나친 방법이다. 마찬가지로 간단한 값을 리턴해도 호출자에게 명확하고 직관적인 응답을 전달할 수 있는 함수를 작성하는 경우에는 object를 반환 할 필요 없다.
RORO를 거의 사용 하지 않는 예는 assertion 함수를 작성할 때이다. 주어진 파라미터가 양의 정수인지 아닌지를 검사하는 isPositiveInteger 함수가 있다고 가정하면, 이런 함수에는 RORO의 이점을 전혀 얻지 못할 것이다.

이 글이 유용 했다면 박수(원본글에서)를 쳐서 이 글이 널리 퍼지도록 해주세요. 그리고 이와 같은 내용을 더 읽고 싶다면 아래의 제 Dev Mastery 뉴스 레터에 가입하세요.
https://upscri.be/83d379?as_embed=true&referrer=https%3A%2F%2Fmedium.freecodecamp.org%2Fmedia%2Facc7ce8e79ec6264b987f0a6f9798c10%3FpostId%3Dbe01e7669cbd


이 글은 Bill Sourour 의 글을 번역한 글입니다. 원문은 아래에서 확인 할 수 있습니다. 혹시 잘못된 번역이 있다면 알려주시면 감사하겠습니다.
https://medium.freecodecamp.org/elegant-patterns-in-modern-javascript-roro-be01e7669cbd