본문 바로가기

[JS] JSON.stringify 재귀를 이용하여 직접 구현하기

[JS] JSON.stringify 재귀를 이용하여 직접 구현하기

 

 

*  재귀를 이용해 메서드 JSON.stringify를 함수의 형태로 직접 구현하기

 

  • JSON.stringify는 객체를 JSON으로 변환하는 메서드이다.
  • 이 메서드를 재귀를 사용하여 함수의 형태로 직접 구현해본다.
    • 자바스크립트 객체와 JSON은 대표적인 트리 구조로 되어 있다.
    • 즉, 전형적인 재귀 탐색이 가능한 구조(객체의 값으로 객체를 포함하는 구조)이기 때문에 재귀 사용을 적극 권장한다. 

 


 

JSON.stringify

 

아래의 JSON.stringify와 같은 결과를 구현하는 함수를 만들어본다.

 

JSON.stringify 규칙
[Browser에 존재하는 JSON.stringfy 함수 === 구현할 stringifyJSON]

JSON.stringfy 함수는 input 값을 JSON 형식으로 변환한다.

1. 단, undefined와 function은 JSON으로 생략되거나 null로 변환된다.
2. stringifyJSON은 아래와 같이 작동한다.

 - Boolean이 input으로 주어졌을 경우  :  stringifyJSON(true);   // 'true'
 - String이 input으로 주어졌을 경우  : stringifyJSON('foo');   // '"foo"'
 - Array가 input으로 주어졌을 경우 : stringifyJSON([1, 'false', false]);   // '[1,"false",false]'
 - Object가 input으로 주어졌을 경우 : stringifyJSON({ x: 5 });   // '{"x":5}'
 - undefined, function이 주어졌을 경우 :
    stringifyJSON(undefined)   // undefined
    stringifyJSON(function(){})   // undefined
    stringifyJSON({ x: undefined, y: function(){} })   // '{}'

 


 

그 전에 알아야하는 것들

 

1. 백틱 용례 (중요!!)

 

 

  • 백틱 속에서 ${} 안의 []나 ''나 "", [''] 등의 기호는 사라진다.
  • [' "" '] -> [''] 속의 ""는 살아있다.
  • 객체의 경우 백틱을 사용해 바로 문자열로 변환하면 '[object Object]'가 나온다.

 

2. 객체 속 undefined와 function 결과 

 

value 값이 undefined인 경우와 property와 value가 function인 경우 생략된다.

 

typeof 용례

 

3.  if 조건에 없으면 함수 결과는 undefined

 

  • 함수에 undefined, function이 주어졌을 경우를 이렇게 처리해주면 된다.

 

 


 

구현결과

 

1. 처음에 구현한 stringiftJSON

 

function stringifyJSON(obj) {

  if(obj === null){                        // 🟡 null (typeof null === 'object')
    return 'null';                         // 'null'
  }
  
  if (typeof obj === "number"){            // 🟡 9 (숫자)
    return String(obj);                    // '9'
  }
  
  if(typeof obj === "boolean"){            // 🟡 true (Boolean)
    return String(obj);                    // 'true'
  }
  
  if(typeof obj === "string"){             // 🟡 'apple' (문자열)
    return `"${obj}"`                      // '"apple"' 
  }
  
  if(Array.isArray(obj)){                  // 🟡 [7, 'hi'] (배열)
    let newArr = [];
    obj.forEach(function (ele){
      newArr.push(stringifyJSON(ele))      // ['7', '"hi"']
    })
    return `[${newArr}]`                   // [7, "hi"]
  }
  
  if(typeof obj === "object"){             // 🟡 { age: 10, name: 'S' } (객체)
    let result = "";
    for(let key in obj) {
      if(obj[key] === undefined || typeof obj[key] === "function"){
         result = String(result);          // String("") === '' // 빈문자열(띄어쓰기X)
      } else {
        result += `${stringifyJSON(key)}:${stringifyJSON(obj[key])},`; 
        // 또는 stringifyJSON(key) + ':' + stringifyJSON(obj[key]) + ',';
      }  
    }
    // (1) 첫 번째 result =  `${'"age"'}:${'10'},`
    // (1)    즉, result = '"age":10,'
    // (1) 두 번째 result = '"age":10,' + `${'"name"'}:${'"s"'},`
    // (2)    즉, result = '"age":10,"name":"s",'                // 📌  
    
    result = result.substr(0, result.length -1); // 마지막에 있는 콤마 삭제 (또는 result.slice(0, -1))
    return `{${result}}`
    // 최종 result = '{"age":10,"name":"s"}'
  }
  
  };

 

2. switch를 사용한 리팩토링

 

그 전에 알아야 하는 join 용법
const elements = [3, 1, 2];

console.log(elements.join());  // Expected output: "3,1,2"
console.log(elements.join(''));  // Expected output: "312"
console.log(elements.join('-'));  // Expected output: "3-1-2"

// const elements = ["3, 1, 2"];

console.log(elements.join());   // Expected output: "3, 1, 2"

// const elements = ["3, '1', 2"];

console.log(elements.join());   // Expected output:  "3, '1', 2"

console.log(['"age":10'].join(',')) // Expected output: '"age":10'
console.log([['"age":10', '"age":13'].join(',')].join(',')) 
// Expected output: '"age":10,"age":13'

 

리팩토링한 코드
function stringifyJSON(obj) {
  if (obj === null) {
    return 'null';
  }

// 🟣 바뀐 부분 (if문 대신 switch문 사용) 
  switch (typeof obj) {   
    case 'number':          // typeof obj === "number" 과 동일 
    case 'boolean':
      return String(obj);   // 또는 `${obj}`
    case 'string':
      return `"${obj}"`;
  }

// 🟣 바뀐 부분 (map과 join 사용)                              // 🟡 [7, 'hi'] (배열)
  if (Array.isArray(obj)) {                                
    return `[${obj.map(stringifyJSON).join(",")}]`;        // map 쓰면 ['7', '"hi"'] --> join 쓰면 '7,"hi"' --> 결과 '[7,"hi"]' 
  }                                                          

// 🟣 바뀐 부분 (continue, join 사용)                          // 🟡 {age: 10} (객체)
  if (typeof obj === 'object') {
    let result = [];

    for (let key in obj) {
      if (obj[key] === undefined || typeof obj[key] === 'function') {
        continue;                     // 이 경우, 무시한다.
      }
      result.push(`${stringifyJSON(key)}:${stringifyJSON(obj[key])}`);  
    }   // (1-1) result = [`${'"age"'}:${'10'}`]
        // (1-2) result = ['"age":10']    // 만약 여러 개라면 ['"age":10', '"age":13']

    return `{${result.join(",")}}`;
        // `{${'"age":10'}}`;             // 만약 여러 개라면 `{${'"age":10,"age":13'}}`; 
        // '{"age":10}';                  // 만약 여러 개라면 '{"age":10,"age":13}';
  }
};

 

  • if문 대신 switch문 사용: 데이터 타입에 따라 다른 동작을 수행하는 경우 switch문을 사용하는 것이 보다 간결하고 가독성이 좋다.
  • 객체 속성 값이 undefined나 함수일 때 result에 추가하지 않고 넘어감: 이러한 값을 JSON으로 직렬화할 수 없으므로 추가할 필요가 없다. (리팩토링 전에는 빈문자열 활용)
  • result를 문자열 대신 배열로 초기화: 문자열과 문자열 연결 대신 Array.push와 Array.join을 사용하여 마지막에 ,를 빼지 않아도 된다.

 

백틱이 헷갈리는 경우

 

 

위의 경우를 참고하여 아래와 같은 코드를 작성해줘도 된다.
// (1) 배열 

if (Array.isArray(obj)) {
	let str = '[';
    for (let i=0 ; i < obj.length ; i+=1) {
    	if ( i === obj.length -1) { str += stringifyJSON(obj[i]); }
		else { str += stringifyJSON(obj[i]) + ','; }}
	str += ']'
    return str;
}

// (2) 객체 : 이런 식으로 변수를 만들어 두면 가독성이 좋아진다.

let keyJSON = stringifyJSON(key);
vlet valueJSON = stringifyJSON(obj[key]);

res.push(keyJSON + ‘:’ + valueJSON);

return ‘{‘ + res + ‘}’;

 


 

결과 확인 

 

잘 된다.

포스팅 초기의 JSON.stringify 결과와 똑같은 결과가 나왔다.

 


 

테스트 코드 

 

생략

더보기
const stringifiableObjects = [
  9,
  null,
  true,
  false,
  "Hello world",
  [],
  [8],
  ["hi"],
  [8, "hi"],
  [1, 0, -1, -0.3, 0.3, 1343.32, 3345, 0.00011999],
  [8, [[], 3, 4]],
  [[[["foo"]]]],
  {},
  { a: "apple" },
  { foo: true, bar: false, baz: null },
  { "boolean, true": true, "boolean, false": false, null: null },
  // basic nesting
  { a: { b: "c" } },
  { a: ["b", "c"] },
  [{ a: "b" }, { c: "d" }],
  { a: [], c: {}, b: true },
];

// JSON은 function과 undefined를 stringify해서는 안 된다.
const unstringifiableValue = {
  functions: function () {},
  undefined: undefined,
};

describe("stringifyJSON는 JSON.stringify 함수를 실행했을 때와 같은 결과를 리턴해야 합니다.", function () {
  stringifiableObjects.forEach(function (test) {
    const stringified = JSON.stringify(test)
    it(`객체는 문자열 "${stringified}"로 변환되어야 합니다`, function (done) {
      const expected = stringified;
      const result = stringifyJSON(test);
      expect(result).to.equal(expected);
      done();
    });
  });

  it("함수와 undefined는 stringify되지 않습니다.", function (done) {
    const expected = JSON.stringify(unstringifiableValue);
    const result = stringifyJSON(unstringifiableValue);
    expect(result).to.equal(expected);
    done();
  });
});

 


 

백틱에 대해서 정말 제대로 알게 된 날!

하! 하!

 

728x90
⬆︎