[프로입문기] 레벨테스트 – 타임머신 에디터 만들기

summer 주임의 신입시절 레벨테스트

지난 인터니들에 이어서
한 달 반 가량의 인턴기간을 마친 우리들… 각자의 업무에 따라 다른 길을 걷게 되는데….
프로의 세계에 막 발을 들인 우리들은 과연 Bsidesoft에서 어떤 일을 하게 될까요?

미리 스포하자면 이런 일을 했습니다.

참쉽죠?

레벨테스트

정직원이 되고 회사 확장 이사로 팔다리에 배긴 알이 다 빠지기도 전에 실장님이 물으셨습니다.

‘너 프로그래밍에 대해 아는게 뭐니?’

그 순간 전 많은 HTML과 CSS, JavaScript 등을 비롯한 많은 단어들이 떠올랐습니다. 그러나 동시에 ‘내가 이걸 안다고 말할 수 있나..?’라는 생각에 우물쭈물하고 있으니 실장님이 이어 말씀하셨습니다.

‘네가 어느 정도 아는지 테스트를 해봐야겠다. 오늘 안에 타임머신 에디터를 만들어라’

하시며 예시를 보여주셨습니다. 별안간 떨어진 테스트에 밀려오는 초조함과 다급함에 마음이 급해졌습니다. 지난 인턴기간동안 그리고 취업을 준비하는 동안 공부했던 모든 지식을 총동원하기 시작했습니다.

삽질의 연속과 실패

타임머신…?

우선 타임머신 에디터가 무엇이고 어떻게 만들어야 하는지 정했습니다.
제가 만들어야 할 타임머신 에디터란 ‘사용자가 저장한 내용을 저장한 순서에 따라 버전별로 기억하여 불러올 수 있는’, 비교적 간단한 프로그램이었습니다. 코드부터 치고 싶은 욕구를 억누르며, 인턴기간에 배운 ‘기초튼튼 코드튼튼 다함께 프로그래밍’의 내용대로 우선 테마와 줄거리, 시나리오를 작성하기로 했습니다. 이때까지만해도 제가 만들어야 할 프로그램을 컴퓨터 문서화해야 한다는 생각을 하지 못했기 때문에 공책에 연필로 내용을 끄적이기 시작했습니다.

손으로 적은 테마와 줄거리/시나리오… 삽질의 흔적들

그러나 이런 손글씨로는 주임님에게 질문을 하기가 어려웠고, 어차피 실장님에게 검사를 맡으려면 컴퓨터 문서화하는 작업이 필요할 것이라는 조언에 따라 부랴부랴 이 내용을 ppt에 옮기고 Flowchart를 그리기 시작했습니다.

우선 테마의 요구사항 다음의 세 가지로 구성했습니다.
1. 텍스트 창에 값을 입력하면 해당 값이 저장된다.(저장기능)
2. 이전 버튼을 클릭하면 저장했던 이전 값을 불러온다.(이전기능)
3. 다음 버튼을 클릭하면 저장했던 다음 값을 불러온다.(다음기능)

이 저장기능, 이전기능, 다음기능에 화면을 구성하는 기능까지 더해 총 4가지 기능을 각각 Flow chart로 만들었습니다.

Flow chart 1 – 화면 구성

시작단계에서 사용될 변수들을 초기화하고 이를 화면으로 구성하는 flow 입니다. 현재 화면의 순서를 세어 줄 count, 모든 버전을 갖고 있어야 할 contentList, 함수표현식으로 정의한 함수 등이 모두 포함됩니다.

Flow chart 2 – 이전 버전 보기

이전 버전을 보기 위해 이전 버튼을 클릭하는 flow 입니다. 이전 버전을 불러온 뒤 현재 화면의 순서에서 1을 빼고, 화면을 구성합니다. 만약 이전 버전이 존재하지 않는다면 경고창을 띄웁니다.

Flow chart 3 – 다음 버전 보기

다음 버전을 보기 위해 다음 버튼을 클릭하는 flow 입니다. 다음 버전을 불러온 뒤 현재 화면의 순서에서 1을 더하고, 화면을 구성합니다. 만약 다음 버전이 존재하지 않는다면 경고창을 띄웁니다.

Flow chart 4 – 현재 버전 저장

현재 입력창에 입력된 값을 저장하기 위해 저장버튼을 클릭하는 flow입니다. 값을 저장하고 현재 화면에 방금 저장한 가장 최신의 버전을 불러옵니다. 화면의 순서도 전체 버전에서 가장 마지막 순서의 숫자를 표시합니다.

이렇게 프로그램을 한국어로 작성하는 과정을 거친 후 에디터를 켜고 JavaScript 코드를 한줄한줄 적어나갔습니다.

우선 외부 js코드와의 전역 오염을 막기 위해 습관처럼 즉시실행함수로 전체 코드를 감싸고, Data를 처리하기 위해 변수를 지정했습니다. 현재 버전의 위치를 카운트해 담아줄 변수를 count로 모든 버전의 콘텐츠를 담을 배열을 contentList로 정의했습니다.

(function(){
	var count = 0;
	var contentList = [];
})();

이벤트에 따라 화면을 구성하고 초기화 역할도 수행하는 render() 라는 함수를 만들었습니다. 사람이 인식하는 순서인 1,2,3 순과 JavaScript가 인식하는 순서인 0, 1, 2의 차이를 없애기 위해 render() 내부에서 사용할 contentIndex를 따로 만들어 count – 1 을 할당해 주었습니다. 그 다음 이 contentIndex를 기반으로 contentList에서 해당하는 값을 가져와 content에 할당했습니다. 이를 기반으로 HTML을 바꿔주는 기능이었습니다.

// 화면을 출력하는 render() 함수 정의
var render = function(){
	var contentIndex = count - 1;
	var content = contentList[contentIndex];
		document.getElementById('editor').innerHTML = 
	`<textarea id="input" autofocus>${content}</textarea>
	<div>
		<div id="version-check">
			<span id="current-version">${count}</span>
			<span>/</span>
			<span id="whole-version">${contentList.length}</span>
		</div>
	</div>`;
};

다음으로 저장 버튼을 누르면 실행되는 save() 함수를 정의했습니다. content라는 변수에 입력창에 입력한 내용을 할당했고, 빈 값을 받았을 경우 저장을 받는 분기문을 만들었습니다. 아직 저장된 버전이 없는 상황과 이미 저장된 버전이 있으며 현재 보고 있는 버전이 마지막 버전인 상황, 그리고 이 두 상황이 아닌 나머지 모든 상황을 정의하고 이에 따라 count를 다르게 처리하게 하였습니다.(이후 코드리뷰에서 이 분기문은 전혀 필요없는 것으로 밝혀졌습니다…!) 끝으로 contentList의 마지막 위치에 content를 할당하는 식으로 콘텐츠 버전을 저장하게 하고, render()를 호출하여 화면을 재구성하도록 했습니다.

// 입력창의 내용을 저장하는 save() 함수 정의
var save = function(){
	var content = document.getElementById('input').value;

	if (content === ''){return alert("빈 값은 저장할 수 없습니다.");}

	if (count === 0) {
	// 저장된 버전이 전혀 없는 상황
		count += 1;
	} else if (count === contentList.length) {
	// 현재 버전이 마지막 버전인 상황
		count += 1;
	} else {
	// 저장된 버전이 있고, 현재 버전이 마지막 버전이 아닌 상황 
		count = contentList.length+1;
	}

	contentList[contentList.length] = content;
		console.log(`[저장] 저장한 content는 ${content}, 현재 count는 ${count}, contentList는`);
		console.log(contentList);
	render();
};

이전 버전과 다음 버전을 화면에 보여주기 위한 기능인 previous()와 next()을 만들었습니다. 각각 현재 버전이 첫 버전이거나 마지막 버전일 경우 해당 alert창을 띄우도록 했습니다. 이 분기문을 통과하면 count에 1을 더하거나 빼고 render()를 호출했습니다.

// 이전 버전으로 돌아가는 previous() 함수 정의
var previous = function(){
	if (count <= 1) {return alert('이전 버전이 없습니다.')}
	count -= 1;
		console.log(`[이전] 현재 count는 ${count}`);
	render();
};

// 다음 버전으로 돌아가는 next() 함수 정의
var next = function(){
	if (count > contentList.length-1) {return alert('다음 버전이 없습니다.')}
	count += 1;
		console.log(`[다음] 현재 count는 ${count}`);
	render();
};

끝으로 프로그램의 동시에 render()를 호출하고 각 기능들은 사용자가 각 기능의 버튼을 누를 때까지 대기하도록 하였습니다.

render();
document.getElementById('save').addEventListener('click', save);
document.getElementById('previous').addEventListener('click', previous);
document.getElementById('next').addEventListener('click', next);

그리고 모든 기능에는 한눈에 알아볼 수 있도록 주석을 달았고 줄바꿈으로 구분해 놓았습니다.

버그와 에러의 연속…하…

작성한 시나리오를 프로그래밍 언어로 바꾸면 된다는 단순한 생각과 달리 프로그램은 제 의도대로 움직여주지 않았습니다. 주임님에게 끊임없이 코드 질문을 해야 했고, <인사이드 자바스크립트> 등 회사의 책들을 펼쳤다 접었다를 끊임없이 반복해야 했습니다.(글을 쓰는 이 순간에도 제 코드를 보며 자괴감을 느끼고 있습니다…ㅎㅎ )

그러는 사이 시간은 다 지나가고 결국 주어진 기한 안에 제출할 수 있는 수준으로 완성하지 못했습니다.

이때 실장님이 회사는 무엇보다 일정이 중요하고, 이 일정을 수시로 보고하고 준수할 것을 강조하셨습니다. 즉, 시간이 부족했다라는 식의 변명은 프로의 세계에서 통하지 않는다는 뜻입니다. 시간을 적절히 통제할 줄 알아야 하고 프로그래밍에 대한 열정이 있어야 프로의 세계에서 살아남을 수 있다는 사실을 다시 한번 기억에 새겼습니다.

배포와 코드리뷰

‘넌 지금까지 한거 url 배포하고, 다들 코드보고 한마디 해’

실장님의 한마디에 배포와 코드리뷰는 일사천리로 이뤄졌습니다. 신랄한 코드리뷰가 쏟아졌고 정신없이 받아 적기 바빴습니다. 이 중 특히 기억에 남는 것과 스스로 깨닫게 된 것을 적어봤습니다.

1.명확하게 세분화한 요구사항
제가 테마에 적은 요구사항은 단 3줄이었습니다. 해야 할 일이 너무 범위가 크고 뭉뚱그려져 있다고보니 어디까지 진행되었는지, 앞으로 무엇을 해야 하는지, 얼마나 시간을 투입해야 하는지, 지금 코드가 완성은 된 것인지 알 수 없었습니다. 그러다보니 당연히 완성을 예측할 수 없고, 무엇을 어디까지 완성했는지도 보고할 수 없었습니다. 따라서 테마를 발전시킨 줄거리와 시나리오의 단계에서는 이 3줄을 구분가능한 단위로 명확하게 세분화해야 해야 합니다. 게다가 그 요구사항조차도 모든 상황과 의미를 고려하지 않은 채 작성되었기 때문에 실제 코드에서도 의미없는 코드가 작성되거나 분기문이 잘못 작성된 결과가 나왔습니다.
즉, 모든 상황과 의미를 고려하여 명확하게 세분화한 요구사항이 있어야 시간과 진행상황을 통제할 수 있고, 코드를 작성할 때에도 무의미한 코드 작성와 오류를 피할 수 있습니다. 여전히 프로그램을 한국어로 작성하는 문서화 작업은 중요합니다.

2.관심사의 분리
많이 들어본 말이었지만, 실제 코드로 구현해 본 적은 거의 없었던 것 같습니다. 저 나름대로도 화면 구성과 각 기능을 분리하는 것으로 관심사를 분리했다고 생각했지만, 사실 그 속을 보면 온갖 요소가 뒤섞여 있습니다. 관심사를 분리할 때에는 단순히 내세운 기능의 이름만 분리하는게 아니라 실제 코드에서도 요소들이 분리가 되어야 함을 말합니다. 모델과 뷰, 뷰와 리소스 등 각 요소 간의 접점을 최소화하고 관심사가 아닌 영역은 분리해야합니다.
이런 분리는 왜 하는 것일까요? 바로 유지보수와 끊임없이 변화하는 고객의 요구에 대응하기 위함입니다. 만약 저 타임머신 에디터에 title이 추가된다면? 작성시간이 추가된다면? 제 코드였다면 새로운 요구사항을 반영하는 것에도 손대야 할 것이 많지만, 코드를 한번 쭉 읽어보면서 파악하는데만에도 한나절이 걸릴 것입니다. 프로의 세계에선 결국 이런 변화에 대응할 수 있도록 관심사를 분리시키는 것이지요. 코드리뷰 과정에서 실장님이 짠 코드와 제 코드를 비교할 수 있었습니다. 실장님이 짠 코드에서는 모든 관심사가 분리되어 있어 리소스나 뷰, 모델에 요소가 갑자기 추가된다고 하더라도 전혀 어렵지 않게 추가할 수 있었습니다. 가령 타임머신 에디터에 제목이 추가되면 어떨까요? 아마 제 코드는 render() 함수와 save() 함수의 내용을 다 뜯어 고쳐야 함은 물론 이렇게 추가한 코드가 기존 코드와 잘 호환되어 정상 작동하는지도 알 수 없을 것입니다. 이것을 잘 하는 방법은 결국 지식과 경험이라는 실장님의 말씀을 들었습니다.

리뷰를 반영해 고치기

  1. 명확하게 세분화한 요구사항
    우선 기존의 3줄짜리 요구사항을 더 세분화하였습니다.

    1. 시작과 동시에 초기화를 실시하고 화면을 구성한다.
    2. [이전기능] (분기) 만약 표시된 현재 버전이 1이라면 이전 버튼을 눌렀을 때 경고메세지를 보여준다.(”이전 버전이 없습니다.”)
    3. [이전기능] (분기) 그렇지 않다면, 이전 버전을 보여준다.
    4. [다음기능] (분기) 만약 표시된 현재 버전이 전체 버전 중 마지막 버전이라면, 다음 버튼을 눌렀을 때 경고메세지를 보여준다.(“다음 버전이 없습니다.”)
    5. [다음기능] (분기) 그렇지 않다면, 다음 버전을 보여준다.
    6. [저장기능] (분기) 만약 입력창에 입력된 값이 없다면, 저장 버튼을 눌렀을 때 경고메세지를 보여준다.
    7. [저장기능] (분기) 그렇지 않다면, 입력된 값을 저장을 하고 가장 마지막(최신) 버전으로 이동시킨 후 보여준다.
    8. [저장] 현재 버전의 위치와 관계없이 저장을 하면 가장 마지막(최선) 버전으로 이동시킨 후 보여준다.
  2. 관심사의 분리
    관심사가 뒤섞여있고, 전역변수가 남발하는 현재의 코드를 코드리뷰를 반영하여 고쳐야 했습니다. 리뷰를 듣고 그저 고개만 끄덕인다면 실력이 전혀 늘지 않을테니까요. 기존의 정체가 불분명하던 변수와 함수를 새롭게 정의하고, 기존의 줄거리와 시나리오를 뜯어 고쳤습니다. 이를테면 현재 버전의 위치를 수를 세는 방식으로 나타내고자한 변수 count는 해당 변수의 본질에 맞지 않는 이름이라는 지적을 받아, 이를 currPosition으로 고쳐 변수명만으로도 그 의미와 본질을 알 수 있게 하였습니다.

변수 정의

DOM 처리 함수

기능함수

줄거리는 다음과 같습니다.
시나리오를 바탕으로 한 플로우차트는 다음과 같습니다.

Flow chart 1 – 시작

변수의 초기화를 실시하고 특정 값을 할당합니다.

Flow chart 2 – 이전 버튼 클릭

이전 버튼을 클릭하여 이전 버전을 불러오고 화면을 구성합니다.

Flow chart 3 – 다음 버튼 클릭

다음 버튼을 클릭하여 다음 버전을 불러오고 화면을 구성합니다.

Flow chart 4 – 저장 버튼 클릭

저장 버튼을 클릭하여 입력창에 저장된 값을 저장하고, 저장된 현재 버전을 최신 버전으로 합니다.

Flow chart 5 – 에러 처리

에러처리와 같은 DOM 관련 요소를 관심사 분리를 위해 따로 빼내었습니다.

줄거리는 한국어 프로그램으로 코드와의 결합을 최대한 줄이려 노력하였습니다.
시나리오는 코드 작성을 염두에 두되, 리소스와 뷰의 분리, 모델과 뷰의 분리를 최대한 구현하고자 했습니다.

이를 바탕으로 고쳐진 코드를 설명드리려 합니다.

우선 에러 메시지같은 리소스를 따로 분리해내고, 향후 다국어지원을 대비하여 lang을 할당했습니다. Msg라는 객체를 정의하고 에러메세지에 해당하는 내용을 프로퍼티와 프로퍼티값으로 구조화하여 표현했습니다. 기존의 코드에선 리소스와 기능이 분리되어 있지 않아 향후 수정사항이 생기면 일일히 코드를 뜯어봐야 했습니다. 예를 들어 기존의 코드에서 일본어 에러메세지를 추가하거나 특정 메세지의 내용을 수정하려면 전체 코드를 일일히 찾아가며 추가하고 수정해주어야 했지만, 개선된 코드에선 Msg의 err의 값만 수정하면 됩니다.

var lang = 'ko';
var Msg = {
    err:{
        ko:{
            previous: '이전 버전이 없습니다.',
            next: '다음 버전이 없습니다.',
            save: '빈 값은 저장할 수 없습니다.'
        },
        en:{
            previous: 'No previous version.',
            next: 'No next version.',
            save: 'Can\'t save blank.'
        }
    }
};

다음으로 데이터 변수를 새롭게 정의했습니다. 현재 위치를 기억하기 위한 프로퍼티와 프로퍼티값(currPosition), 전체 버전을 저장하기 위한 프로퍼티와 프로퍼티값(contentList)을 객체구조로 바꾸어 모델의 역할을 수행하게 하였습니다.

var Data = {
    currPosition: 0,
    contentList: []
};

기능 함수는 따로 Func이라는 객체로 집어넣고, 모델을 건드린다는 하나의 관심사로 집중하였습니다. 기존에 관심사의 분리없이 DOM을 가져와 유효한 값인지를 판단하고, 경고창을 띄워주며 DOM을 건드리기 위한 창구는 render() 함수 하나로 한정하였습니다.

var Func = {
   save: function(content){
        Data.contentList.push(content);
        Data.currPosition = Data.contentList.length;
        render(Data.currPosition, Data.contentList);
    },
    previous: function(){
        Data.currPosition -= 1;
        render(Data.currPosition, Data.contentList);
    },
    next: function(){
        Data.currPosition += 1;
        render(Data.currPosition, Data.contentList);
    }
};

DOM을 건드리고 경고창을 띄워주는 기능을 다른 함수에서 분리해냈습니다.
에러메세지를 띄워주는 err()를 새롭게 정의하고, errCondition을 인자로 받아 lang과 errCondition을 통해 에러메세지를 출력하게 하였습니다.
현재위치(posit)과 저장된 전체 콘텐츠 배열(contList)을 인자로 받아 HTML을 구성하는 render() 함수를 정의하였습니다. 기존의 render() 함수는 모델의 값들을 조작하고 사용하였는데, 이를 인자로 받는 구조로 바꾸어 본래의 모델(데이터)를 건드리지 않으면서 DOM 구조에만 집중하도록 하였습니다. 특히 기존에 html 전체를 작성하는 방식에서 벗어나 쿼리를 선택하고 해당 값만을 살짝살짝 바꿔주는 방식으로 변경하였습니다. 초기화를 위해 인자로 들어온 값이 없는 경우를 대비한 예외처리를 위한 코드도 작성하였습니다. 끝으로 render() 함수는 즉시실행함수의 인자로 넣어 실행시킴으로써 기능 코드와 분리하고자 하였습니다.

(function(render){
...
var err = function(errCondition) {
    var errMsg = Msg.err[lang][errCondition];
    return alert(errMsg);
}
...
})(function(posit, conList){
        var contentList = conList ? conList : [];
        if(posit){
            var position = posit;
            var content = contentList[posit - 1];
        }else{
            var position = 0;
            var content = '';
        }
        document.querySelector('.content').value = content;
        document.querySelector('.curr-version').innerHTML = position;
        document.querySelector('.whole-version').innerHTML = contentList.length;
    });

실제 사용되는 host 코드입니다. 에러메시지를 기능함수가 호출하던 기존의 방식을 수정하여 DOM과 직접 관계맺는 코드가 DOM의 유효성을 판단하여 err() 함수를 호출하도록 하였습니다.

render();
document.querySelector('.save').addEventListener('click', function(){
    document.querySelector('.content').value === '' ? err('save') : Func.save(document.querySelector('.content').value);
});
document.querySelector('.previous').addEventListener('click', function(){
    Data.currPosition <= 1 ? err('previous') : Func.previous();
});
document.querySelector('.next').addEventListener('click', function(){
    Data.currPosition === Data.contentList.length ? err('next') : Func.next();
});

위 코드들을 즉시실행함수로 감싼 전체 코드는 다음과 같습니다.

(function(render){
    var lang = 'ko';
    var Msg = {
        err:{
            ko:{
                previous: '이전 버전이 없습니다.',
                next: '다음 버전이 없습니다.',
                save: '빈 값은 저장할 수 없습니다.'
            },
            en:{
                previous: 'No previous version.',
                next: 'No next version.',
                save: 'Can\'t save blank.'
            }
        }
    };
    var Data = {
        currPosition: 0,
        contentList: []
    };
    var Func = {
        save: function(content){
            Data.contentList.push(content);
            Data.currPosition = Data.contentList.length;
            render(Data.currPosition, Data.contentList);
        },
        previous: function(){
            Data.currPosition -= 1;
            render(Data.currPosition, Data.contentList);
        },
        next: function(){
            Data.currPosition += 1;
            render(Data.currPosition, Data.contentList);
        }
    };
    var err = function(errCondition){
        var errMsg = Msg.err[lang][errCondition];
        return alert(errMsg);
    };
    render();
    document.querySelector('.save').addEventListener('click', function(){
        document.querySelector('.content').value === '' ? err('save') : Func.save(document.querySelector('.content').value);
    });
    document.querySelector('.previous').addEventListener('click', function(){
        Data.currPosition <= 1 ? err('previous') : Func.previous();
    });
    document.querySelector('.next').addEventListener('click', function(){
        Data.currPosition === Data.contentList.length ? err('next') : Func.next();
    });
})(function(posit, conList){
        var contentList = conList ? conList : [];
        if(posit){
            var position = posit;
            var content = contentList[posit - 1];
        }else{
            var position = 0;
            var content = '';
        }
        document.querySelector('.content').value = content;
        document.querySelector('.curr-version').innerHTML = position;
        document.querySelector('.whole-version').innerHTML = contentList.length;
    });

이 밖에 쓸데없는 주석과 공백을 제거하고 삼항연산자를 활용해 보았습니다.
마지막으로 html 구조를 변경된 js에 맞게 바꾸고, css를 손보아 최초 구현하고자 했던 화면을 만들었습니다.

Jeje의 타임머신 에디터 보러가기

회고를 마무리하며

타임머신 에디터를 만들고 코드 리뷰를 받고 회고글을 쓰면서 평소 공부를 게을리하고 ‘이 정도까진 알지 않나? 이 정도면 되지 않나?’ 라는 안일한 생각으로 프로그래밍을 해오지 않았는지 되짚어보았습니다. 당장 이번 과제의 리뷰를 나열해도 이 페이지가 끝이 없을 지경이라 모든 것을 담지 못해 아쉬움이 남습니다. 또 리뷰를 받고 코드를 재작성하면서도 리뷰 받은 내용을 구체적으로 코드로 재현하는 것이 쉽지 않았습니다. 특히 실장님의 코드와 그 의미를 다 구현하지 못해 여전히 부족함이 많다는 것을 느꼈습니다. 특히 왜 그래야 하는지와 어떻게 구현하는지를 어렴풋이 기억하면서도 관련한 정확한 지식을 몰라 삽질을 했던 부분은 더욱 그랬습니다.

개선한 코드에서 조차도 DOM 부분이 직접 모델을 참조하는 등 관심사의 분리가 제대로 되어 있지 않다는 것을 느꼈습니다. 여전히 문제점으로 지적된 부분이 그대로 남아있거나 처리되지 못한 부분이 많습니다. 이 부분은 차차 다음 과제(+회고)를 하고 경험을 쌓아가면서 구현해나가려 합니다. 이번 과제는 프로의 세계에 들어온만큼 프로로 살아남기 위해 열심히 공부해야겠다는 생각을 하게 된 소중한 시간이었습니다.

우선적인 테마와 시나리오 구현은 마쳤지만, 이 타임머신 에디터는 계속해서 개선해 나갈 것이고 이 개선코드 이후에도 꾸준히 고치쳐나가며 올려보려고 합니다. 기대해주세요~^_^! 다음 과제와 회고글로 뵙겠습니다.

%d 블로거가 이것을 좋아합니다: