학습 & 성장 (Learning & Growth)/개발 공부 (Development Study)

[YDKJSY]🛡️ 최소 노출: JS 스코프와 클로저 이해하기

vanillinav 2024. 8. 1. 19:58
728x90
반응형

지금까지 우리는 스코프와 변수가 어떻게 작동하는지에 대해 설명하는 데 집중했습니다. 이제 이러한 기초가 확고히 자리잡았으니, 프로그램 전반에 걸쳐 적용되는 결정과 패턴에 대해 더 깊이 생각해볼 시간입니다. 본 글에서는 채권의 원리와 더 복잡한 예제를 통해 이를 설명하겠습니다.

🔍 최소 노출

함수가 자체 스코프를 정의한다는 점은 이해가 됩니다. 그렇다면 왜 블록도 스코프를 생성해야 할까요?
소프트웨어 공학에서는 "최소 권한 원칙"(POLP)이라는 기본 규율을 설명합니다. 현재 논의와 관련된 이 원칙의 변형은 "최소 노출"(POLE)입니다.

📏 POLP와 POLE

POLP는 소프트웨어 아키텍처에 대한 방어적 자세를 표현합니다. 시스템의 구성 요소는 최소한의 권한, 접근, 노출로 작동하도록 설계되어야 합니다. 이렇게 하면 하나의 부분이 손상되거나 실패할 경우 전체 시스템에 미치는 영향이 최소화됩니다.
POLP가 시스템 수준의 구성 요소 설계에 초점을 맞춘다면, POLE은 더 낮은 수준에서 작동합니다. 스코프가 서로 어떻게 상호 작용하는지에 초점을 맞추는 것이죠.

❓ 왜 변수를 전역 스코프에 두면 안 되나요?

프로그램의 모든 변수를 전역 스코프에 배치하지 말아야 하는 이유는 다음과 같습니다:

네이밍 충돌: 프로그램의 다른 부분에서 공통적이고 유용한 변수/함수 이름을 사용할 때, 하나의 공유된 스코프(예: 전역 스코프)에서 이를 선언하면 네이밍 충돌이 발생합니다. 예를 들어, 모든 채권의 금리를 단일 전역 rate 변수로 사용한다고 가정해보세요. 한 함수의 채권 금리가 다른 함수의 금리와 겹치는 경우, rate 변수의 값이 예상치 못한 방식으로 변경될 수 있습니다.

var rate;

function bond1() {
    for (rate = 0; rate < 5; rate++) {
        console.log('bond1 rate:', rate);
    }
}

function bond2() {
    for (rate = 0; rate < 5; rate++) {
        console.log('bond2 rate:', rate);
    }
}

bond1();
bond2();

예상치 못한 동작: 프라이빗한 프로그램의 변수/함수를 노출시키면, 다른 개발자가 이를 예상치 못한 방식으로 사용할 수 있습니다. 이는 예상되는 동작을 위반하고 버그를 유발할 수 있습니다. 예를 들어, 채권의 만기가 모두 일정하다고 가정했는데, 다른 코드가 만기 값을 변경하면 문제가 생길 수 있습니다.

var maturities = [1, 2, 3];

function addMaturity(maturity) {
    maturities.push(maturity);
}

function addInvalidMaturity(invalidMaturity) {
    maturities.push(invalidMaturity);
}

addMaturity(4);
addInvalidMaturity('invalid');

console.log(maturities);  // [1, 2, 3, 4, 'invalid']

의도치 않은 의존성: 변수를 불필요하게 노출하면, 다른 개발자가 해당 변수에 의존하게 됩니다. 나중에 데이터 구조를 변경하면, 다른 부분에서 이를 조정해야 하는 책임이 생깁니다.

var maturities = [1, 2, 3];

function getMaturities() {
    return maturities;
}

console.log(getMaturities());  // [1, 2, 3]

var maturities = new Set([1, 2, 3]);

console.log(getMaturities());  // [1, 2, 3] -> Set 객체가 출력될 가능성

POLE을 적용하면 가능한 최소한의 노출을 기본으로 하고, 가능한 모든 것을 프라이빗하게 유지합니다. 변수를 가능한 작고 깊이 중첩된 스코프에 선언하고, 모든 것을 전역 스코프에 배치하지 않습니다. 이렇게 소프트웨어를 설계하면, 위의 세 가지 위험을 최소화할 수 있습니다.

📘 예제: 스코프 노출 최소화

채권의 금리와 만기일을 관리하는 함수의 예제를 살펴보겠습니다.

function swapRatesAndMaturities(rate1, rate2, maturity1, maturity2) {
    if (rate1 > rate2) {
        let tmpRate = rate1;
        rate1 = rate2;
        rate2 = tmpRate;

        let tmpMaturity = maturity1;
        maturity1 = maturity2;
        maturity2 = tmpMaturity;
    }

    return {
        rateDifference: rate2 - rate1,
        maturityDifference: maturity2 - maturity1
    };
}

console.log(swapRatesAndMaturities(5, 3, 10, 15));  // { rateDifference: 2, maturityDifference: 5 }
console.log(swapRatesAndMaturities(2, 7, 20, 5));   // { rateDifference: 5, maturityDifference: 15 }

위 예제에서는 tmpRatetmpMaturity 변수를 if 블록 내부에 선언하여 스코프를 최소화합니다. 이러한 변수들은 함수 수준에 있거나 전역 변수일 필요가 없습니다. 따라서 if 블록 내부에 let을 사용하여 블록 스코핑합니다.

🔐 함수 스코프에서 숨기기

변수와 함수 선언을 가능한 낮은(가장 깊이 중첩된) 스코프에 숨기는 것이 왜 중요한지 명확해졌습니다. 그렇다면 어떻게 그렇게 할 수 있을까요?

📚 함수 표현식 사용

함수 스코프를 사용하여 변수를 숨길 수 있습니다. 이를 위해 함수 표현식을 사용할 수 있습니다.

var calculateYield = (function hideTheCache() {
    var cache = {};

    function calculateYield(bond) {
        if (!bond || bond.principal < 0) return 0;
        if (!(bond.principal in cache)) {
            cache[bond.principal] = bond.principal * (1 + bond.rate / 100) ** bond.years;
        }
        return cache[bond.principal];
    }

    return calculateYield;
})();

const bond1 = { principal: 1000, rate: 5, years: 10 };
const bond2 = { principal: 2000, rate: 4, years: 5 };

console.log(calculateYield(bond1));  // 1628.894626777442
console.log(calculateYield(bond2));  // 2430.4240800000005

위 예제에서 cache 변수를 함수 스코프 내에 숨기기 위해 함수 표현식을 사용했습니다. cachecalculateYield 함수 외부에서는 접근할 수 없습니다. 이를 통해 cache 변수가 외부에 노출되지 않도록 합니다.

🔄 함수 표현식을 즉시 호출하기

즉시 호출 함수 표현식(IIFE)은 변수를/함수를 숨기기 위해 스코프를 생성할 때 유용합니다.

(function() {
    // 내부 숨겨진 스코프
})();

IIFE는 표현식이기 때문에 JS 프로그램에서 표현식이 허용되는 어떤 장소에서도 사용할 수 있습니다. 이를 통해 변수를 숨기기 위한 임시 스코프를 생성할 수 있습니다.

🧩 스코프와 블록 사용

블록 스코핑은 변수/함수 선언의 노출을 제한하는 데 유용합니다.

{
    let thisIsNowAScope = true;

    for (let rate = 0; rate < 5; rate++) {
        if (rate % 2 == 0) {
            console.log(rate);  // 0, 2, 4
        }
    }
}

위 예제에서는 블록 스코핑을 통해 변수의 노출을 최소화합니다. 블록 스코핑을 통해 변수의 범위를 필요한 곳으로만 제한할 수 있습니다.

🔍 예제: 명시적 블록 스코프

채권의 만기일을 관리하는 예제를 살펴보겠습니다.

if (marketConditionChanged) {
    {
        let newMaturity = calculateNewMaturity();
        notifyInvestors(newMaturity);
    }

    adjustPortfolio();
}

위 예제에서는 명시적 블록 스코프를 사용하여 newMaturity 변수의 노출을 최소화합니다. newMaturityif 블록 내부에서만 필요하므로, 해당 블록 내에 블록 스코핑합니다.

📝 varlet 사용

varlet의 차이를 이해하고 적절히 사용하는 것이 중요합니다.

🆚 varlet의 예

function swapRatesAndMaturities(rate1, rate2, maturity1, maturity2) {
    if (rate1 > rate2) {
        var tmpRate = rate1;

  // `tmpRate`는 함수 스코프에 속합니다
        rate1 = rate2;
        rate2 = tmpRate;

        var tmpMaturity = maturity1;  // `tmpMaturity`는 함수 스코프에 속합니다
        maturity1 = maturity2;
        maturity2 = tmpMaturity;
    }

    return {
        rateDifference: rate2 - rate1,
        maturityDifference: maturity2 - maturity1
    };
}

위 예제에서는 var를 사용하여 함수 스코프에 변수를 선언했습니다. 이는 tmpRatetmpMaturity 변수가 함수 전체에서 접근 가능하게 합니다.

🌀 for 루프에서 let 사용

for (let rate = 0; rate < 5; rate++) {
    // 무언가를 합니다
}

위 예제에서는 for 루프의 반복자 변수를 let으로 선언하여 블록 스코핑을 사용했습니다. 이는 rate 변수가 루프 내부에서만 접근 가능하게 합니다.

⚠️ 주의할 점

🔍 catch 절의 예외

catch 절은 추가적인 블록 스코핑 선언 기능을 사용합니다.

try {
    calculateYield(null);
} catch (err) {
    console.log(err);
    let errorOccurred = true;
    var outerVariable = true;
}

console.log(outerVariable);  // true

위 예제에서는 catch 절의 err 변수가 블록 스코핑되어 블록 내부에서만 접근 가능합니다.

🛑 블록 내 함수 선언 (FiB)

블록 내부에 함수 선언을 배치하는 것은 예측 가능하지 않은 결과를 초래할 수 있습니다.

🤔 FiB 예제

if (false) {
    function ask() {
        console.log("이게 실행되나요?");
    }
}
ask();

위 예제는 다양한 JS 환경에서 다르게 동작할 수 있습니다. 이는 블록 내 함수 선언이 예측 불가능한 동작을 초래할 수 있음을 보여줍니다.

⚛️ React에서의 예제

이제 위의 개념을 React에서 어떻게 사용할 수 있는지 살펴보겠습니다.

🧩 컴포넌트 내의 상태 관리

React에서 상태를 관리할 때도 스코프 노출을 최소화하는 것이 중요합니다. 컴포넌트 내의 상태와 함수는 해당 컴포넌트 내에서만 접근할 수 있도록 설계되어야 합니다.

import React, { useState } from 'react';

function Bond() {
    const [rate, setRate] = useState(0);

    function increaseRate() {
        setRate(prevRate => prevRate + 1);
    }

    function decreaseRate() {
        setRate(prevRate => prevRate - 1);
    }

    return (
        <div>
            <p>Rate: {rate}</p>
            <button onClick={increaseRate} aria-label="Increase rate">Increase</button>
            <button onClick={decreaseRate} aria-label="Decrease rate">Decrease</button>
        </div>
    );
}

export default Bond;

위 예제에서는 Bond 컴포넌트 내에서 상태 rate와 이를 변경하는 함수 increaseRate, decreaseRate를 선언했습니다. 이러한 상태와 함수는 Bond 컴포넌트 외부에서 접근할 수 없습니다. 이를 통해 스코프 노출을 최소화하고, 컴포넌트의 상태를 안전하게 보호할 수 있습니다. 

🛠️ 명시적 블록 스코프 사용

명시적 블록 스코프를 사용하여 변수의 노출을 제한할 수 있습니다. React 컴포넌트 내에서도 동일한 원칙을 적용할 수 있습니다.

import React, { useState } from 'react';

function BondDetails() {
    const [maturity, setMaturity] = useState('');

    function handleChange(event) {
        {
            let newMaturity = event.target.value;
            setMaturity(newMaturity);
        }
    }

    return (
        <div>
            <label htmlFor="maturityInput">Enter maturity:</label>
            <input id="maturityInput" type="text" value={maturity} onChange={handleChange} aria-label="Maturity input" />
            <p>{maturity}</p>
        </div>
    );
}

export default BondDetails;

위 예제에서는 handleChange 함수 내에서 newMaturity 변수를 명시적 블록 스코프로 감싸서 선언했습니다. newMaturity는 해당 블록 내에서만 유효하며, 함수 외부에서는 접근할 수 없습니다. 

🔒 함수 표현식과 클로저 사용

React에서는 클로저를 사용하여 상태와 함수를 관리할 때 스코프를 효과적으로 제어할 수 있습니다.

import React, { useState } from 'react';

function BondCalculator() {
    const [num, setNum] = useState(0);
    const [result, setResult] = useState(0);

    const calculateBond = (function() {
        const cache = {};

        function calcBond(n) {
            if (n < 2) return n;
            if (cache[n]) return cache[n];
            return (cache[n] = calcBond(n - 1) + calcBond(n - 2));
        }

        return calcBond;
    })();

    function handleChange(event) {
        const newNum = parseInt(event.target.value, 10);
        setNum(newNum);
        setResult(calculateBond(newNum));
    }

    return (
        <div>
            <label htmlFor="numberInput">Enter number:</label>
            <input id="numberInput" type="number" value={num} onChange={handleChange} aria-label="Number input" />
            <p>Bond Result: {result}</p>
        </div>
    );
}

export default BondCalculator;

위 예제에서는 calculateBond 함수를 클로저로 정의하여 내부 cache 변수를 숨깁니다. cachecalcBond 함수 외부에서 접근할 수 없으며, calculateBond 함수는 컴포넌트 내에서만 사용됩니다. 이를 통해 스코프 노출을 최소화하고, 함수의 동작을 안전하게 유지할 수 있습니다. 

🌐 브라우저와 Node.js 환경에서의 작동 방식

위의 예제들은 브라우저와 Node.js 환경 모두에서 잘 작동할 수 있습니다. 여기서 두 환경에서의 차이점과 유사점을 설명하겠습니다.

🌍 브라우저에서의 작동 방식

브라우저 환경에서는 JavaScript 코드가 HTML 문서와 함께 실행됩니다. React 컴포넌트는 일반적으로 브라우저에서 렌더링되는 사용자 인터페이스를 구성합니다. HTML 파일에서 JavaScript 파일을 포함하고, ReactDOM을 사용하여 React 컴포넌트를 HTML 요소에 마운트합니다.
예제:

HTML 파일 (index.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React App</title>
</head>
<body>
    <div id="root"></div>
    <script src="index.js"></script>
</body>
</html>

JavaScript 파일 (index.js):

import React from 'react';
import ReactDOM from 'react-dom';
import Bond from './Bond';

ReactDOM.render(<Bond />, document.getElementById('root'));

브라우저는 HTML 파일을 로드하고, JavaScript 파일을 실행하며, ReactDOM.render를 사용하여 Bond 컴포넌트를 root 요소에 마운트합니다.

📦 Node.js에서의 작동 방식

Node.js 환경에서는 JavaScript 코드가 서버에서 실행됩니다. Node.js는 브라우저 환경과 달리 HTML 문서를 렌더링하지 않지만, 서버 측 로직을 처리하고 클라이언트와의 통신을 관리할 수 있습니다.

React 컴포넌트를 서버 측에서 렌더링하려면 react-dom/server 패키지를 사용할 수 있습니다. 서버 측 렌더링(SSR)을 통해 React 컴포넌트를 HTML 문자열로 렌더링하고, 이를 클라이언트에 전송하여 초기 로딩 속도를 향상시킬 수 있습니다.

예제:

서버 파일 (server.js):

const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const Bond = require('./Bond').default;

const app = express();

app.get('/', (req, res) => {
    const html = ReactDOMServer.renderToString(<Bond />);
    res.send(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>React App</title>
        </head

>
        <body>
            <div id="root">${html}</div>
            <script src="client.js"></script>
        </body>
        </html>
    `);
});

app.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
});

클라이언트 파일 (client.js):

import React from 'react';
import ReactDOM from 'react-dom';
import Bond from './Bond';

ReactDOM.hydrate(<Bond />, document.getElementById('root'));

Node.js 서버는 React 컴포넌트를 HTML 문자열로 렌더링하고, 클라이언트에 전송하여 hydrate를 통해 클라이언트 측에서 React 컴포넌트를 활성화합니다.

💧 Hydrate란?

Hydrate는 서버 측에서 렌더링된 HTML을 클라이언트 측에서 React 컴포넌트로 다시 활성화하는 과정입니다. 서버 측 렌더링(SSR)된 HTML은 초기 로딩 시간을 단축시키고 SEO(검색 엔진 최적화) 성능을 개선하는 데 도움이 됩니다. 클라이언트 측에서 React는 기존의 HTML을 탐색하고, 필요한 이벤트 핸들러를 연결하여 클라이언트 측의 인터랙티브한 애플리케이션으로 변환합니다.

💧 Hydrate의 예제

서버 측 렌더링:

  • Node.js 서버에서 ReactDOMServer.renderToString을 사용하여 React 컴포넌트를 HTML 문자열로 렌더링합니다.
  • 렌더링된 HTML 문자열을 클라이언트로 전송합니다.
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const Bond = require('./Bond').default;

const app = express();

app.get('/', (req, res) => {
    const html = ReactDOMServer.renderToString(<Bond />);
    res.send(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>React App</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="client.js"></script>
        </body>
        </html>
    `);
});

app.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
});

클라이언트 측 Hydrate:

  • 클라이언트에서 ReactDOM.hydrate를 사용하여 서버 측에서 렌더링된 HTML을 탐색하고 이벤트 핸들러를 연결합니다.
import React from 'react';
import ReactDOM from 'react-dom';
import Bond from './Bond';

ReactDOM.hydrate(<Bond />, document.getElementById('root'));

Hydrate 과정을 통해 클라이언트는 서버 측에서 이미 렌더링된 HTML을 재사용하여 초기 로딩 시간을 단축시키고, 서버와 클라이언트 간의 일관성을 유지할 수 있습니다.

🔚 결론

렉시컬 스코핑 규칙은 프로그램의 변수를 적절히 구성하는 데 매우 중요합니다. POLE 원칙을 따르면 불필요한 스코프 노출을 최소화할 수 있습니다. 블록 스코핑과 함수 표현식을 사용하여 변수의 노출을 최소화하는 습관을 기르면 프로그램의 안정성과 유지보수성이 크게 향상됩니다.
이 원칙은 React 컴포넌트에서도 유용하게 적용될 수 있습니다. React에서는 상태와 함수를 적절히 스코프 내에 숨겨 안전하고 효율적으로 상태를 관리할 수 있습니다. 브라우저와 Node.js 환경에서 각각의 예제가 어떻게 작동하는지 이해하면, 다양한 환경에서 안전하고 효율적으로 코드를 작성할 수 있습니다.
Hydrate 과정은 서버 측에서 렌더링된 HTML을 클라이언트 측에서 다시 활성화하여 초기 로딩 시간을 단축시키고 SEO 성능을 개선하는 데 중요한 역할을 합니다. 이러한 개념을 통해 여러분은 더 안전하고, 효율적이며, 유지보수가 쉬운 코드를 작성할 수 있을 것입니다.
 

✔️ 참조

 

728x90
반응형