본문 바로가기
공부/JavaScript

느슨한 연결

by 무심한고라니 2020. 12. 27.

최근 제가 작성한 JS 코드가 유지보수하기 힘들고 파악하기 쉽지 않다는 느낌을 받았고, 그 원인에 대한 생각을 하다가 이 글을 작성하게 되었습니다. 수정할 부분이 있다면 댓글로 피드백 부탁드립니다. 참고자료의 출처는 하단에 기재하였습니다. 글의 대략적 목차는 아래와 같습니다.

 

1. 전역 선언 방지

2. One-Global 접근법

3. UI 레이어 느슨하게 연결하기

 

_____

 

전역 선언 방지

 

전역 객체를 사용하면 어디에서나 사용 가능한 변수나 함수를 만들 수 있다[1]. 전역 객체는 언어 자체나 호스트 환경에 기본 내장되어 있는 경우가 많다[2].

 

alert("Hello");
// 위와 동일하게 동작합니다.
window.alert("Hello");

var gVar = 5;
alert(window.gVar); // 5 (var로 선언한 변수는 전역 객체 window의 프로퍼티가 됩니다)

 

위처럼 var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다[3]. 이 전역 객체 window는 웹페이지 종료 시점까지 유효하므로 var 키워드[4]로 선언된 전역 변수 또한 웹페이지 종료 시점까지 유효하다. 즉 전역 선언이란 전역 객체를 통한 선언이라고 할 수 있다. 하지만 특별한 경우가 아니라면 전역 선언은 하지 말아야 한다. 다음 예를 살펴보자.

 

function sayColor() {
    alert(color);	// 나쁜 예: color는 어디서 왔을까요?
}

 

위 함수는 전역 변수 color를 사용하는데, 만약 이 변수가 다른 파일에 있거나 여러 곳에 정의되어 있다면 디버깅이 어려워진다. 뿐만 아니라 전역 환경은 네이티브 자바스크립트 객체[6]를 정의하는 곳이어서 우리가 멋대로 이름을 추가했다가는 추후 브라우저에서 새로 추가하는 이름과 겹칠 수도 있다[5]. 이처럼 전역을 사용하는 함수는 변화에 취약해 환경이 바뀌면 함수 실행에 문제가 생길 수 있기에 아래와 같이 수정하는 것이 좋다.

 

function sayColor(color) {
    alert(color);
}

 

이처럼 변수 color를 인자로 받게 하면 전역 변수에 더는 의존하지 않아 전역 환경이 바뀌어도 영향을 받지 않게 된다[7]. 이로써 주변 환경에서 함수를 독립시켜 한쪽을 변경해도 다른 쪽에 영향이 가지 않게 할 수 있다.

 

 

One-Global 접근법

 

하지만 전역 변수 없이 코딩이 가능할까? 보통 팀으로 개발하면 여러 개의 자바스크립트 파일에 코드를 작성하는데 여러 파일로 나누어진 코드 간에 통신하려면 모든 코드가 바라봐야 할 전역 변수가 필요하기 마련이다. 이에 대한 해결책으로 전역 변수를 최소한으로 사용하기 위한 One-Global 접근법이 있다.  이는 이미 우리에게 잘 알려진 자바스크립트 라이브러리에서 모두 사용하는 방법이다.

 

  • YUI는 YUI라는 전역 객체를 사용한다.
  • jQuery는 두 개의 전역 객체를 사용하는데 $와 jQuery다. jQuery라는 전역 객체는 타 라이브러리에서 $를 먼저 사용했을 때에만 추가된다.
  • Closure 라이브러리는 goog 전역 객체를 사용한다.

 

즉, One-Global 접근법에서는 네이티브 API가 사용하지 않을만한 이름으로 전역 객체를 만들고 그 안에 필요한 로직을 모두 추가한다.

 

***

추가적으로 남긴다. 위에서 언급했듯 결국 내 코드가 유지보수가 힘들어진 것은 (화면 상에서의) 전역 변수와 이를 이용하는 함수 때문이었다. 이 함수에서 변화하는 부분(전역 변수)을 인자로 추출함으로써 부수 효과를 줄이는 방향으로 리팩토링했지만, 전역 변수를 완전히 제거할 수 없을까 하는 생각[11]을 해보았다. 예를 들어 현재 개발하는 화면에서 사용자가 그리드의 한 행을 선택하면 선택 행을 담는 변수가 다음과 같이 해당 화면 내의 전역 변수로 선언되어 있다.

 

var selectedRow = "";

 

그리고 관련된 모든 함수, 이를 테면 사용자가 그리드의 한 부분을 클릭하거나 사용자가 해당 행의 상세 내용을 수정하는 등에서 이 전역 변수를 매개변수로 받도록 해놓았다. 이를 전역 변수가 아니라 다음과 같이 함수로 추출하면 어떨까?[12]

 

// 코드가 동작할지 확신은 안 서지만...
let selectedRow = function getSelectedRow(row) {
    let selectedRow;
    if (typeof row !== undefined) {
        selectedRow = row;
    }

    return function() {
        return selectedRow;
    };
}();

 

 

UI 레이어 느슨하게 연결하기

 

웹 개발에서 사용자 인터페이스(UI)는 총 세 개의 레이어로 나뉘고 서로 유기적으로 작동한다.

 

  • HTML: 페이지에서 데이터와 의미를 정의
  • CSS: 페이지의 스타일을 꾸미는데 사용
  • JavaScript: 페이지에 동작을 부여하는 데 사용

 

규모가 큰 웹 애플리케이션, 즉 여러 개발자가 개발 및 유지보수를 해야 하는 상황이라면 컴포넌트 간 느슨한 연결(loose coupling)이 필수다. 내 경우 CSS를 다루는 경우는 많지 않았기 때문에 HTML과 자바스크립트를 분리해야 하는 경우[8]가 많았다.

 

<p onclick="changeText(this)" id="p1">이 문자열을 클릭해 보세요!</p>

<script>
function changeText(element) {
    element.innerHTML = "문자열의 내용이 바뀌었습니다!";
}
</script>

 

위 코드는 HTML에 자바스크립트 코드가 강하게 결합되어 있어 여러 문제를 야기시킨다. 첫째, 파일이 로딩되지 않은 상태에서 사용자가 p 태그를 클릭하면 자바스크립트 에러가 발생한다. 즉, changeText(element) 함수가 외부 자바스크립트 파일에 정의되어 있다면 자바스크립트 파일은 HTML 파일보다 나중에 로딩되게 되므로 발생할 수 있는 일이다. 둘째, 만약 함수명 혹은 호출 함수를 바꾸고자 한다면 HTML 및 자바스크립트 파일을 둘 다 변경해야 한다. 따라서 HTML의 on 속성에 이벤트 핸들러를 추가하면 안 된다.

 

<p id="p1">이 문자열을 클릭해 보세요!</p>

<script>
function changeText(element) {
    element.innerHTML = "문자열의 내용이 바뀌었습니다!";
}

var p = document.getElementById("p1");
p.addEventListener("click", changeText, false);	// [9]
</script>

 

대신 이처럼 changeText(element) 함수가 이벤트 핸들러[10]를 추가하는 코드와 같은 파일에 정의되어 있다면 함수명 변경 시 또는 p 태그 클릭 시 호출 함수 변경 시에도 하나의 파일만 수정하면 된다. 한편 자바스크립트 라이브러리를 이용할 때는 라이브러리에서 제공하는 메소드를 사용하면 된다.

 

// YUI
Y.one("#p1").on("click", changeText);

// jQuery
$("#p1").on("click", changeText);

 

 

_____

1. 자바스크립트가 처음 실행될 때부터 다양한 전역 변수와 전역 함수가 선언되는데, 이 선언된 전역은 필요할 때 마음대로 사용할 수 있다.

2. 브라우저 환경에서는 전역 객체를 window, Node.js 환경에서는 global이라 부르는데 각 호스트 환경마다 부르는 이름은 다르다.

3. 모든 변수는 생명주기를 갖고 있는데, 전역 코드들은 실행하는 특별한 진입점(호출)이 없고 코드가 로드되자마자 곧바로 해석되고 실행된다.

4. var 대신 let을 사용하면 전역 객체를 통해 변수에 접근할 수 없다.

let gLet = 5;
alert(window.gLet); // undefined (let으로 선언한 변수는 전역 객체의 프로퍼티가 되지 않습니다.)

5. color는 별다른 식별자도 없는 평범한 명사이기 때문에 새로 추가될 네이티브 API와 충돌할 가능성이 있고 다른 개발자가 똑같은 이름의 변수를 선언한 가능성이 높은 이름이다.

6. 네이티브 객체는 특정 환경(브라우저 혹은 Node.js)에 종속되지 않은 ECMAScript 명세의 내장 객체를 말한다.

7. 함수를 정의할 때는 가능한 한 지역 변수를 사용해야 한다. 함수 안에서 정의할 수 있는 건 모두 지역 변수로 선언하고 함수 외부에서 선언된 데이터는 인자로 받아야 한다.

8. 대표적 예로 이벤트를 들 수 있다. 클라이언트 측 자바스크립트를 비동기식 이벤트 중심(event-driven)의 프로그래밍 모델이라고 한다.

9. 이벤트 대상이 되는 객체에 직접 프로퍼티로 등록하지 않고, 객체나 요소의 메소드에 이벤트 리스너를 전달하는 방법이다.

/*
 * 1. 이벤트 명 : 이벤트 리스너를 등록할 이벤트 타입을 문자열로 전달한다.
 * 2. 실행할 이벤트 리스너 : 지정된 이벤트가 발생했을 때 실행할 이벤트 리스너를 전달한다.
 * 3. 이벤트 전파 방식 : false면 버블링 방식으로, true면 캡처링 방식으로 이벤트를 전파한다.
 */
대상객체.addEventListener(이벤트명, 실행할이벤트리스너, 이벤트전파방식);

10. 이벤트 리스너라고도 하며, 이벤트가 발생했을 때 그 처리를 담당하는 함수를 가리킨다.

11. 전역변수가 나쁜지에 대한 고민은 차치하고.. 무조건 나쁜지 모르겠다.

12. getSelectedRow() 함수를 호출하면 호출할 때마다 새로운 렉시컬 환경 객체[14]가 만들어진다. 그리고 이 렉시컬 환경 객체엔 getSelectedRow()를 실행하는데 필요한 변수들이 저장된다. 따라서 선택 행을 사용하는 이벤트 함수들[13]이 렉시컬 환경을 공유하도록 하기 위해 이처럼 함수 선언과 동시에 호출해주었다. 관련하여 아래 예[15]를 참고해보자.

function makeCounter() {
    let count = 0;

    return function() {
        return count++;
    };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert(counter()); // 0
alert(counter()); // 1

alert(counter2()); // 0
alert(counter2()); // 1

13. 선택 행을 사용하는 이벤트 함수들이다. 전역 변수를 함수로 변경함에 따라 아래와 같이 변경될 것이다.

// 변경 전
var selectedRow;

// 그리드 클릭 이벤트
grd_oncellclick = function(row, col) { 
    selectedRow = row;  // 선택한 행 저장
} 

// 상세 수정 이벤트 
txt_onchange = function(e) { 
    // 저장되어 있는 전역 변수(selectedRow)를 통해 상세 수정을 그리드 반영 
}

// 변경 후 
let selectedRow = function getSelectedRow(row) {
    let selectedRow;
    if (typeof row !== undefined) {
        selectedRow = row;
    }

    return function() {
        return selectedRow;
    };
}();

// 그리드 클릭 이벤트 
grd_oncellclick = function(row, col) { 
    selectedRow(row);
} 

// 상세 수정 이벤트 
txt_onchange = function(e) {
    let sRow = selectedRow();
    // 변수(sRow)를 통해 상세 수정을 그리드 반영 
}

14. 하단의 참고자료에도 기재하였지만 다음 글의 단계 3, 즉 내부와 외부 렉시컬 환경을 보면 렉시컬 환경 객체에 대해 이해할 수 있다.

15. 이 예를 결과를 이해하기 위해선 렉시컬 환경과 클로저[16]에 대한 이해가 필요하다.

  • 변수값 갱신은 변수가 저장된 렉시컬 환경(counter, counter2)에서 이루어진다.
  • 클로저는 외부 변수를 기억하고 있고 이에 접근할 수 있다.

16. 더 정확히 말하면 자바스크립트에서는 모든 함수가 자연스럽게 클로저가 된다. 그리고 그 이유는 렉시컬 환경 프로퍼티와 렉시컬 환경의 동작으로 설명 가능하다.

 

_____

참고자료

 

'공부 > JavaScript' 카테고리의 다른 글

var과 let의 차이점  (0) 2021.01.02
비동기 처리  (0) 2020.12.27
Function  (0) 2020.12.26
Logical Operators  (0) 2020.12.25
번역] 7가지 유용한 자바스크립트 내장 함수  (0) 2020.12.20

댓글