JS에서 값을 전달하는 방법

이상현·2022년 7월 20일
0

idea

목록 보기
1/1

A와 B라고 불특정의 변수를 가정해보자.
이 변수는 리터럴 객체일수도 있고, 어떤 클래스의 인스턴스일수도 있으며, 함수일 수도 있다. 정한 것이 아무 것도 없다고 할 때, 언어의 syntax만이 남을 테고 그러면 scope만은 언제나 존재한다고 할 수 있다.

서로 다른 유효 범위 - scope -는 값을 공유하기가 기본적으로 어렵다. A와 B가 값을 공유하려면 scope가 공유되어야 한다. 그러면 값을 공유한다 = scope 공유가 될텐데, 의심많고 경계심 많은 이 성격에 스스로 생각해보는 중임에도 의구심이 들었다.

어떤 방법들이 있을 지 생각해보았는데...

callback

function A(parameter){
  console.log(parameter);
}
function B(callback){
  const v = null;
  callback(v);
}

B(A);	// be expected : null

어느 한쪽의 scope에 다른 한쪽의 scope가 들어가는 것 - 중첩 - 이 가장 간단한 방법일 것이다. 들어가는 쪽이 받아주는 쪽의 scope를 공유할 수 있다. web api의 대부분이 이 방식을 쓴다.

return & parameter

function A(v){
  const v = null;
  return v;
}
function B(parameter){
  console.log(parameter);
}

B(A());	// expected log : null

callback과 같은 아이디어이지만 scope는 공유하지 않고, 값을 주입해 건네주는 방식을 생각해 볼 수 있다.

callback과 이 예제의 경우에서 A가 객체라면, B는 A의 property를 열람할 권한을 얻고 동시에 A의 정보를 바꿀 수 있는 권한도 얻는다.

closure

function A(){
  let v = null;
  return {
  	read : function(){
      console.log(v);
    },
    overwrite : function(parameter){
      v = parameter;
    }
  }
}

const a = A();
a.overwrite("string");
a.read();	// expected log : "string"

예제는 global scope로 작성했지만, B라는 scope에서 A의 scope를 참조한다고 해도 방법은 같다. 이 경우 A가 제공하는 property로만 접근할 수 있고, A가 scope 내의 모든 값에 접근하도록 해주어도 "const" 키워드로 선언된 경우에는 overwrite를 사용해 값을 바꿀 수 없다. 객체지향과 유사하게 private과 public의 성격으로 정보를 분리해 전달할 수 있다.

쓰다보니 새삼 아이러니한 게 느껴졌다. inner scope는 outer scope를 참조할 수 있는 데, outer scope는 inner scope를 참조할 수 없다.

global variable

let v = null;
function A(){ v = "string"; console.log(v);}
function B(){ v = [0,5,2]; console.log(v);}

최상위 outer context이자 outer scope에 메모리를 열어놓고 해당 메모리를 A, B가 사용한다. inner scope에서 변경한 outer scope의 값은 다른 모든 inner scope에도 영향을 준다.

restucture

function A(){
  let v = null;
  return {
  	get : function(){
      return v;
    },
    set : function(parameter){
      v = parameter;
    }
  }
}
function B(read_property){
	console.log(this[read_property]);
}
const C = {
  a : A,
  b : B
};
C.a.set("sign");
C.v = C.a.get();
C.b(v);

앞선 몇가지 방법을 교합한 방법으로, 또 다른 변수 - 메모리 - 에 A와 B를 각각 할당해 하나의 객체 아래 묶고, this 키워드로 접근할 수 있도록 구조화시켰다. 단지 구조만 바꾸었을 뿐인데, 자기 scope 외부의 값을 쓴다는 인상을 주지 않아 마치 본래 하나였던 것처럼 보인다. 그런데 코드 구성에도 보이듯이 약자로만 구성했음에도 리딩이 조잡하다. 그리고 this 키워드의 대상이 공통 scope인 C를 가르켜야 하는데 하위 구성은 a에 종속되어 C로 넘겨주기 위한 모종...의 문제를 매번 해결해줘야 한다.

storage

const storage = (function(initial_data){
  let store = new Object(initial_data);
  return {
  	update : function(data){ if(store === data) return false; store = {...store, ...data}; },
    read : function(){ return store;},
    reset : function(){store = new Object(initial_data);},
    picker : function(name){ return store[name];},
    dispatch : function(data){ return this.update(data);}
  }
})(1);

// ....blahblah

값은 제 3의 scope에 들어가 있고, A와 B는 그걸 가져와 사용한다. 아마도 공유라는 말을 문자 그대로 사용한, 가장 직관적인 방식일 것이다.

prototype

const custom_array = [0,1,2,3,4,5].map(num => num +* 2).filter(num => 0 === num % 2).sort();

근래에 가장 많이 쓰는 stream 구조의 코드로, 어떤 데이터를 return 하되 해당 데이터의 prototype method를 이용해 값을 연속적으로 공유할 수 있다. 이걸 이용하면 pipe가 없어도 pipe처럼 쓸 수 있다.

const data = [0,1,2,3,4,5];
const callback_a = (value)=>{
  ...
  return value;
};
const callback_b = (value)=>{
  ...
  return value;
};
                             
const stream = data.map(callback_a).map(callback_b);

promise를 사용하는 방법과 유사하다. 다만 체이닝의 목적이 contructor의 메모리를 사용하는 것뿐, 방법에는 제한이 없어서 map을 쓰든 forEach를 쓰든 filter를 쓰든 상관이 없다. 코드가 지시하는 바가 무엇인지 알 수 없다. pipe라는 property를 map을 복사해 넣어줄 순 있지만, 눈 가리고 아웅하는 게 아닌가 싶다. customize가 필요하다면 promise의 사용법을 참고할 수 있을텐데, 역시 아웅 류를 벗어나지 않을 것이다. 그리고 native 객체를 수단으로 할 때 이를 저런 목적성에서 확장하는 것이 잘하는 것인지 의문도 든다.

iterable generator

function *stream_generator(initial_value){
	let v = yield initial_value;
	while(true){
		v = yield v;
	}
}
function stream_runner(list, initial_value){
	const stream = stream_generator(initial_value);
	let data = stream.next();
	for(i=0; i <= list.length; i++){
		data = stream.next(list[i](data, initial_value));
		console.log(i, data);
	}
  	return data;
}
function A(data, initial_data){
	console.log(`A input :`, data);
	return data.value.map(v => v*= 2);
}
function B(data, initial_data){
	console.log(`B input :`, data);
	return data.value.filter(v => 9 < v);
}
stream_runner(
	[A, B], 		// list
	[0,1,2,3,4,5]	// initial_value
);
// expected log
// A input : {value: [0,1,2,3,4,5], done: false}
// 0 {value: [0,2,4,6,8,10], done: false}
// B input : {value: [0,2,4,6,8,10], done: false}
// 1 {value: [10], done: false}

generator를 통해 목적하는 바 - stream - 가 알려져 아무 property나 반복하는 것보다 나아보인다. 원한다면 초기값을 사용할 수도 있다. 제어권을 runner가 가지고 있긴 하지만, 무한히 반복되는 iterable인 것이 걸리거나 {done : true}의 값을 활용하려 한다면 list의 길이를 generator에 넘겨줄 수도 있겠다.

...Epilogue

처음에 상정했던 명제는 결국 거짓이었다. 제 3의 공간을 이용해 던지고 받을 수 있었다. 사람이 의도적으로 만들어낸 공간이든 JS 스펙이 만들어준 공간이든 값을 주고 받는 건 가능하다. 값의 전달에 어느 한쪽 또는 양자 모두의 scope 공유는 필수 조건이 아니고, scope의 제한을 뛰어넘거나 우회할 방법들이 있었다.

그리고 무언가 보완하고 싶은 부분에 따라 구조는 가변적이다. 단, 모든 방법에서 공통적으로 보이는 현상은 매개변수를 이용하려든다는 점이고, 그래서 값을 가지고 있는 scope로서 함수가 일반 객체보다 유리한 듯 보였다는 게 이 테스트의 return으로 보인다.

또 하나 위 방식들을 보면 현재 React에서 사용하는 코드들이 이 값의 전달 방법을 고민한 결과물이 아닐까... 하는 생각도 든다.

scope는 top-down 참조가 안되기 때문에 단방향 플로우와 중첩, 매개변수라는 조합은 순응의 결과처럼 보인다. 그러나 순응의 댓가로 드릴링 이슈와 서로 다른 계층 또는 위치의 scope 간의 값의 이동과 공유가 어려우지며 끌어올리기라는 수단이 필요했고, 그럼에도 근본적인 문제의 해결을 위해선 root scope를 필요로 하여 값을 관리할 scope - context api, store... -가 나왔을테다.

이후에는 값이 지나다니는 길목과 경로등의 처리가 필요했을테고, 또 길에서 정보의 정리, 분산과 재사용을 목적으로 하는 아이디어가 나왔을테고, 그러다보니 generator까지 가지 않았을까 싶다. 실행의 대기, 최종 실행, 보장된 반복 수행등 generator의 옵션들은 웹 프로그래머가 필요로 하는 여러면을 갖추었다. 쓰기 위한 학습과 패턴화가 어렵다는 것만 빼면.

profile
이런 건 왜...

0개의 댓글