JavaScript/TypeScript 코드를 정적 분석하는 방법

JH Ahn
네이버 플레이스 개발 블로그
25 min readNov 20, 2020

1. 개요

안녕하세요, 플레이스서비스개발팀에서 인턴 프로젝트로 “코드의 브라우저 호환성 검증” 을 위해 JavaScript/TypeScript를 정적 분석하며 얻었던 경험 및 지식을 공유하고자 합니다.

문제 정의

  • 현재 다양한 브라우저가 존재하는데, 각 브라우저마다 JavaScript 엔진이 다르기 때문에 지원하는 기능 및 스펙에 차이가 존재
  • 따라서 최신 스펙의 JavaScript 코드를 작성할 때, 해당 코드가(예. Array.prototype.flat) 각 브라우저에서 호환이 되는지 MDN, caniuse 사이트 등 수동으로 확인해야 하는 불편함을 유발
  • 개발에 사용한 최신 JavaScript 스펙이 서비스의 타겟 브라우저가 지원하지 않는다면 polyfill 파일을 별도로 생성해줘야 함

Note: 일반적으로 브라우저와의 코드 호환성 문제는 Babel 및 Babel-polyfill(현재는 Babel core-js v3에 포함), plugin-transform-runtime 등으로 해결이 가능하지만, 기타 이유로 인해 Babel을 사용하지 않는 프로젝트 또한 존재하기 때문에 해당 프로젝트를 진행하게 됐습니다.

그림. Array.prototype.flat() 의 브라우저 호환성 (MDN)

해결책

  • Github pull request를 올렸을 때 사용자 선택에 따라 PR에 해당하는 커밋의 코드만, 혹은 전체 프로젝트 JavaScript/TypeScript 코드의 호환성을 자동으로 검증하여 Github의 Check, Review, Comment 등을 통해 호환성 결과를 출력
  • 호환되지 않는 스펙에 대해서 polyfill 파일을 자동으로 생성하는 자동화 프로세스를 개발

2. 개발 프로세스

개발은 두 부분으로 나누어 진행하였습니다.

  1. Github과 전반적으로 통신하는 부분으로써, 저장소 내 특정 Commit의 코드 혹은 전체 프로젝트 코드를 받아오기, Pull Request에 Comment/Review/Check 형태로 호환성 검사 결과를 출력하기, 자동 폴리필 파일 생성하기 등 작업이 포함됩니다
  2. ESLint 커트텀 플러그인 개발을 통해서 코드를 정적 분석하여 코드의 브라우저 호환성을 검증하는 부분입니다

다음은 Pull Request가 올라가는 시점부터 어떠한 방식으로 코드의 호환성 검증이 이루어지는지 나타내는 그림입니다.

그림. 프로세스의 전체 Flow

전체 프로세스의 흐름은 다음과 같습니다:

1. 사용자가 Github 저장소로 코드를 푸시 후
2. Pull Request를 생성하면
3. 등록해둔 Github Apps가 지정한 WebHook URL로 WebHook payload를 보내게 되고, Pull Request(PR) 이벤트를 listen하고 있던 Probot 어플리케이션(아래에서 설명)에서 PR에 해당하는 commit의 코드를 받습니다.
4. 받은 commit의 코드를 ESLint API를 사용하여 코드의 브라우저와의 호환성 검증 후 (혹은 사용자 설정에 따라 프로젝트 전체 검증 가능)
5. 다시 Github Apps를 통해 check, review, comment 등으로 해당 PR에 호환성 검증 결과를 출력합니다.

Github Apps란 Github 내에서 독자적인 권한 및 정체성을 가지고 저장소에서 일어나는 다양한 workflow를 API를 통해 자동화 및 간편화 할 수 있게 도와주는 프로그램입니다.

그림. Github Market place에 있는 다양한 Apps

Github Apps은 Probot을 사용하여 개발했습니다. Probot은 Github Apps를 보다 편리하게 개발할 수 있게 해주는 Node.js 기반 Bot 프레임워크 및 어플리케이션입니다. Github REST API를 간편하게 사용할 수 있게 하며, 또 Github의 다양한 WebHook 이벤트를 listen하여 추가 작업을 할 수 있게 도와줍니다. Github 저장소 내에서 이루어지는 대부분의 행동(PR 생성, Commit 내용 조회, 코멘트, Review 생성 등)은 Github REST API를 통해서도 처리할 수 있어 전체적인 프로세스를 Probot으로 개발하기에 적합하였습니다. Github Apps 및 Probot 사용법은 아래 공식 페이지를 참고하시길 바랍니다.

3. 배경지식

코드의 호환성을 검증하기 위해서는 먼저 코드를 정적 분석할 수 있는 환경 및 툴이 필요했습니다. 코드 정적 분석을 위해 필요한 배경지식은 다음과 같습니다.

AST (Abstract Syntax Tree)

코드의 정적 분석은 일반적으로 parser, AST(Abstract Syntax Tree)을 요구합니다. JavaScript의 경우 개발할 때 빼놓을 수 없는 대표적인 코드 분석툴인 ESLint가 있는데, ESLint 마찬가지로 자체 JavaScript parser (Espree)를 통해 AST를 생성하여 코드를 분석합니다. 여기서 AST란 Abstract Syntax Tree의 약자로, 소스 코드의 구조를 트리 형태로 변환한 것입니다.

AST는 여러 노드로 구성이 되어 있는데, 노드의 타입, 소스 코드에서의 코드 위치, 하위 자식 노드들에 대한 레퍼런스 등 여러 정보가 포함되어 있습니다.

아래는 간단한 두 줄 코드(왼쪽)의 AST(오른쪽) 입니다:

그림. 프로그램의 Abstract Syntax Treee

하나의 프로그램의 body 안에는 여러 노드로 구성이 되어 있습니다. 해당 예제에서는 프로그램 순서상 변수 선언(VariableDeclaration) 타입을 가진 노드가 최상위 노드로 위치해있고, 또 이 노드는 child로 변수 선언자(VariableDeclarator), descendant로 식별자(Identifier), 배열 표현식(ArrayExpression) 등 하위 노드를 쭉 가지고 있는걸 볼 수 있습니다. 하나의 노드는 각 코드 조각에 대한 정보를 가지고 있습니다. 이러한 노드 타입과 다양한 노드의 정보를 통하여 코드를 분석하게 되는데, 더 자세한 내용은 밑에서 다루도록 하겠습니다.

그림. 다양한 노드의 유형

ESLint

ESLint는 개발자들이 널리 사용하는 문법 검사 도구로써, 추상화가 잘 되어 있어 내부 흐름을 알 필요 없이 원하는 규칙을 쉽게 적용할 수 있습니다. 이런 ESLint 또한 내부적으로는 정적으로 코드를 분석하여 문법 및 규칙을 검증합니다. ESLint는 몇 가지 특징을 가지고 있습니다.

  • ‘Espree’ 라는 JavaScript Parser를 사용. 현재 다양한 JavaScript parser가 존재하는데, 각 parser는 JavaScript를 파싱하여 AST를 생성한다는 점에서 동일하지만 트리 구조나 포함되는 정보가 각자 사용처 필요에 따라 조금씩 다르게 설계되어 있음
    * 다양한 parser와 각 parser가 생성하는 AST 정보는 여기에서 확인하실 수 있습니다.
  • ESLint는 parser가 생성한 AST를 순회(estraverse)하며 각 노드마다 해당 노드에 대한 정보 및 이벤트를 콜백으로 반환하고, 이를 통해 특정 코드 패턴 분석을 가능하게 함
  • 이전 세대의 JS Lint툴인 JSHint, JSLint에 비해 ESLint의 큰 장점은, ESLint에 사용되는 모든 rule은 하나의 플러그인이기 때문에 직접 플러그인을 만들어서 사용하거나 third-party 플러그인을 사용하여 쉽게 커스터마이징이 가능함

ESLint의 Rule

지금까지 ESLint의 third-party 플러그인(airbnb 등)을 사용하여 개인 혹은 팀에게 맞는 스타일과 규칙을 간단하게 설정하여 사용만 해왔으나, 이번 기회에 내부적으로 각 rule이 어떻게 작동하는지 알아보게 되었습니다.

그림. eslintrc의 rules

ESLint의 rule은 특정 에러를 검출하고자 하는 목표를 가지고 미리 작성된 로직에 따라 AST를 순회하며 각 노드를 검증하고, 통과하지 못하면 에러를 출력합니다. 각 rule은 누군가가 만든 플러그인이며, 하나의 플러그인에 다수의 rule을 정의할 수 있습니다. 따라서 원하는 rule을 골라서 하나씩 추가할 수도 있고, eslintrc 내의 extends 옵션을 사용하여 플러그인에서 미리 정의한 여러 rule의 집합을 한 번에 적용할 수도 있습니다 (예. 보편적으로 사용하는 airbnb, eslint:recommended).

ESLint의 플러그인과 rule을 만들기

ESLint의 플러그인과 rule을 만들어 간단하게 코드를 정적 분석하는 예제를 보도록 하겠습니다.

ESLint 플러그인의 프로젝트 구조는 yeoman을 통해 생성이 가능합니다(직접 생성해도 되지만 편의를 위해). 같은 디렉토리에서 yo eslint:plugin명령어를 통해 플러그인 전체 프로젝트 구조를 생성하고, yo eslint:rule를 통해 하나의 rule을 작성할 수 있도록 보일러 플레이트를 생성합니다.

각 플러그인은 하나의 NPM module이며, package.json에서 모듈 이름은 반드시 eslint-plugin-<플러그인 이름>의 naming 컨벤션을 따라야 합니다(@<scope>/eslint-plugin-<플러그인 이름> 혹은 @<scope>/eslint-plugin도 가능). 또 플러그인의 entry 파일 index.js에서는 반드시 rule의 이름을 포함하고 있는 rules 객체를 아래와 같이 export 해야 합니다(위 언급한 yeoman을 사용하면 위 작업을 자동으로 설정해 줍니다).

예를 들어 no-deprecate-function 이라는 이름을 가진 rule을 만들고자 한다면, 플러그인 entry 파일 내 rules 객체 안에 다음과 같이 정의합니다.deprecateFunction은 해당 rule을 검증하는 로직(추후 설명)을 담고 있어야 합니다.

module.exports = {
rules: {
‘no-deprecate-function’: deprecateFunction,
},
};

플러그인 이름이 eslint-plugin-deprecate이고 위 rule을 사용하고자 한다면 eslintrc 파일에서 아래와 같이 설정합니다.

“plugins”:[“deprecate”],
“rules”: {
“deprecate/no-deprecate-function”: “error”,
}

예제로 deprecated된 함수(createApartment, createHouse)를 사용했을 때 ESLint 에러를 내는 간단한 rule을 만들어 보겠습니다 (yeoman을 이용하면 아래와 같은 template 파일이 자동 생성됩니다).

// no-deprecate-function.jsconst DEPRECATED_FUNCTIONS = [‘createApartment’, ‘createHouse’];module.exports = {
// rule에 대한 정보
meta: {
docs: {
description: ‘do not allow use of deprecated functions’,
},
},
create: function (context) {
return {
// node의 타입이 CallExpression일 때 아래의 로직을 수행
CallExpression(node) {
// 2. 함수의 이름이 deprecated 된 함수 이름에 포함이 된다면
if (DEPRECATED_FUNCTIONS.includes(node.callee.name)) {
// 3. report 메서드를 통해서 ESLint 에러 출력
context.report({
node,
message: `${node.callee.name}()는 deprecated된 함수입니다.`,
});
}
},
};
},
};
그림. Custom ESLint Rule을 적용한 결과

create 함수는 ESLint가 AST를 순회 중 각 노드를 방문하며 부르는 여러 method가 담긴 객체를 반환합니다. 위 예제 코드를 보면 create 함수가 반환하는 method의 이름이 CallExpression(호출식)인데, 해당 부분은 AST를 순회하다 CallExpression 타입인 노드, 즉 일반 함수를 만나면 정의한 해당 이벤트가 전달되어 실행되는 로직입니다.

  • 노드 타입의 정보 등 espree parser(estree 기반 AST)의 스펙은 https://github.com/estree/estree 에서 확인 가능합니다 (es5.md, es2015.md 파일 등).

Method의 parameter로는 node가 들어오는데, 아래와 같이 CallExpression 타입에 해당하는 node의 정보가 담겨있습니다.

그림. node에 포함되는 정보

해당 노드의 node 매개변수를 통해서 함수의 이름(node.callee.name)을 확인하고, callee Identifier의 이름이 deprecated 목록에 있을 시 context.report()를 통해 에러를 발생시킵니다. context에는 report 메서드와 같이 rule 정의하는 데 도움이 되는 유용한 추가 기능들이 포함되어 있습니다. 따라서 rule을 정의할 때는 필요한 node 타입이 무엇인지, 해당 node로 무엇을 할 것인지 잘 파악하는 것이 중요합니다.

4. ESLint 커스텀 Rule을 통해 브라우저의 호환성 검증

이번 프로젝트의 JavaScript의 코드 호환성은 JavaScript의 function, constructor 및 method에 대해 검증하는 것을 목표로 하였는데, 이때 필요한 node 타입은 총 3가지 정도로 압축할 수 있습니다.

1. CallExpression — fetch()와 같은 일반 함수가 불렸을 때
2. MemberExpression — [Array 변수].includes()와 같이 instance의 멤버로 접근했을 때
3. NewExpression — new Promise()와 같이 new 연산자가 사용됐을 때

호환성 검증은 `mdn-browser-compat-data` 모듈에서 제공해주는 MDN 데이터를 사용하여 검증하고자 하는 method/constructor를 데이터에서 탐색 후 서비스의 타겟 브라우저 최소 지원 버전과 해당 method/constructor의 최소 지원 버전을 비교하는 방식으로 진행하였습니다.

아래와 같이 MDN 이 제공하는 데이터에는 JavaScript의 모든 함수 및 메소드에 대해 각 브라우저별에서 사용 가능한 최소 버전이 명시 되어 있습니다. 따라서 호환성을 검증하는 rule의 로직은 서비스의 타겟 브라우저 최소 지원 버전이 MDN 데이터에 명시 되어 있는 버전보다 더 낮을 경우 에러를 발생합니다.

그림. Array.prototype.concat의 브라우저 호환성 정보

앞서 rule을 작성하는 방법과 같이 rule 파일의 create 함수 안에 검증할 노드 메서드를 반환하여 주고,


create: function (context) {
..., //검증을 위해 필요한 로직 작성
return {
CallExpression: lintCallExpression.bind( // 작성한 검증 로직 전달
null,
context,
handleFailingRule,
),
NewExpression: lintNewExpression.bind(
null,
context,
handleFailingRule,
),
MemberExpression: lintMemberExpression.bind(
null,
context,
handleFailingRule,
),
}
}

각 노드 타입을 만나면 전달될 검증 로직을 작성해 줍니다(예. CallExpression pseudo 코드).

export function lintCallExpression(
context,
handleFailingRule,
node
) {
if (!node.callee) return;
const calleeName = node.callee.name;
// 자체 재가공한 mdn 데이터(rules) 내에서 검증 조건을 위반하는 함수가 존재하는지 확인
const failingRule = rules.find((rule) => rule.object === calleeName);
if (failingRule){
// report error 로직 작성
handleFailingRule(context, failingRule, node);
...
}
...
}

Note: ESLint가 기본으로 제공하는 JavaScript parser에서는 type에 대한 정보를 제공하지 않고 있기 때문에, TypeScript를 사용하는 프로젝트는 typescript와 @typescript-eslint/parser 모듈을 설치한 후 eslintrc parserOption에 `project` 옵션을 사용하면 앞서 언급하였던 rule 파일의 contextparameter를 통해서 variable 및 instance의 type 정보를 받을 수 있습니다.

//eslintrc 파일“parser”: “@typescript-eslint/parser”,
“parserOptions”: {
“ecmaFeatures”: {
“jsx”: true
},
“ecmaVersion”: 12,
“sourceType”: “module”,
“project”: “./tsconfig.json” // 중요! tsconfig.json가 있는 경로 설정
},

예를 들어 ‘Hello word’.startsWith(‘H’)는 instance에 접근하는 메서드이기 때문에 MemberExpression 노드 타입이고, 다음과 같이 startsWith 메서드가 동작하는 instance의 타입을 추론할 수 있습니다.

function lintMemberExpression(
context,
handleFailingRule,
node
) {
// getTypeChecker()를 통해 type을 추론할 수 있는 다양한 기능을 가져온다
const checker = context.parserServices.program.getTypeChecker();
if (!checker) return;
const tsProperty = context.parserServices.esTreeNodeToTSNodeMap.get(
node.property
);
const propertySymbol = checker.getSymbolAtLocation(tsProperty);
if (!propertySymbol) return;
const propertyType = propertySymbol.parent.escapedName.toString(); console.log(propertyType); // 'Hello Word'의 Type은 String
}

5. 호환성 검증 결과

  1. 사용자가 명시한 각 브라우저 버전에 대해 PR Commit에 해당하는 코드의 호환성을 검증 한 후 Pull Request에 결과 출력 예시
그림. PR에 해당하는 Commit의 diff 코드 호환성 검사

2. 사용자가 명시한 각 브라우저 버전에 대해 프로젝트 전체의 호환성을 검증한 후, 호환되지 않는 스펙을 Pull Request에 출력 예시(사용된 횟수 및 폴리필이 존재하는지의 여부 포함)

그림. 프로젝트 전체 코드에 대해 호환성 검사 결과

3. 호환되지 않는 스펙에 대하여 자동으로 생성된 polyfill 파일 예시
* polyfill source는 https://www.polyfill.io 의 API를 이용

그림. 자동 생성된 polyfill 파일 예시

4. check 생성 후 호환성 검사 실패시 check failure (linter-pr)

그림. Github Check 연동

6. 번외 편 — ESLint를 사용하지 않고 코드 분석하기

ESLint가 현재 정적 분석을 위한 보편적인 툴로써 자리 잡아있고, 어느정도 검증된 툴이라고 생각되어 처음부터 ESLint를 통해 정적 분석을 진행하고자 했습니다. 하지만 VSCode 등 개발 IDE에서 실시간으로 코드 linting 에러를 보고자 하는 것이 아닌 이상, 굳이 ESLint와 ESLint가 제공하는 parser를 사용할 필요가 없었습니다. 오히려 앞서 언급했던 @typescript-eslint/parser는 내부적으로 TypeScript의 자체 parser를 이용하여 TypeScript AST에서 ESLint와 호환되는 AST로 변환하는 추가적인 작업이 필요했기 때문에 성능 오버헤드가 존재하였고, 따라서 호환성 검사 시간이 더 오래걸린다는 단점이 있었습니다. 예를 들어 PR에 해당하는 commit의 코드만 검사하는 것은 일반적으로 수 초 이내에 빠르게 완료할 수 있었지만, 큰 프로젝트(2000개 이상의 파일) 전체 검사 시 완료까지 3분 이상이 걸리기도 하였습니다.

같은 프로그램에 대한 아래 두 parser의 AST가 상당히 다른 것을 확인할 수 있습니다:

@typescript-eslint/parser의 AST

TypeScript 자체 AST

처음 목표했던 프로젝트의 스펙 구현은 ESLint를 통해 모두 완료하였지만, 남은 시간 동안 ESLint를 사용하는 것이 아닌 TypeScript 자체 compiler API를 통해 호환성 검증 성능을 최적화 하고자 했습니다. 앞서 언급했던 ESLint 커스텀 플러그인에 대한 내용은 접어두고, TypeScript의 compiler API를 어떻게 사용했는지 간단하게 살펴보겠습니다.

  • TypeScript compiler API에 대한 자세한 내용은 공식 문서를 참고해주세요

TypeScript compiler API

TypeScript가 제공하는 API를 통해 AST 생성과 Type 추론하는 방법을 간단하게 알아보겠습니다.

TypeScript에서 제공하는 createProgram 메서드를 통해 파싱하고자 하는 코드의 파일 경로가 담겨있는 배열을 인자로 넘겨주어 프로그램에 대한 SourceFile을 생성합니다. 생성된 각 SourceFile에는 해당 소스파일에 대한 text, TypeScript의 AST와 기타 정보가 포함되어 있습니다. SourceFile내에 AST를 재귀 함수로 순회하며 ESLint 때와 마찬가지로 각 노드에 대한 검증을 실행합니다.

import ts from 'typescript';// TypeScript 소스파일 생성
// createProgram 두 번째 인자에는 tsconfig의 compilerOptions와 같음
const program = ts.createProgram([fileName], {
target: ts.ScriptTarget.ESNext,
allowJs: true,
jsx: 2,
});
// type을 추론할 수 있게 도와주는 typeChecker
const checker = program.getTypeChecker();
// 생성된 각 소스파일을 하나씩 루프
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
// AST를 순회
ts.forEachChild(sourceFile, visit);
}
}
function visit(node: ts.Node) {
const nodeType = ts.SyntaxKind[node.kind]; // node의 타입
switch (nodeType) {
// MemberExpression의 노드 타입
case ‘PropertyAccessExpression’: {
// 해당 노드의 타입 정보를 가져온다
const tsObject = checker.getTypeAtLocation(
(node as ts.PropertyAccessExpression).expression,
);
if (tsObject === undefined) {
return;
}
// instance의 타입
const type = checker.typeToString(tsObject);
// 호환성 검증 로직
validateExpressionStatement(
type,
node.name.escapedText,
);

break;
}
… // CallExpression 등 다른 node 타입도 같은 방식으로 검증
}
// AST를 재귀 함수로 모두 순회
ts.forEachChild(node, visit);
}

호환성을 검사하는 로직은 ESLint와 마찬가지로 MDN 데이터를 이용하여 브라우저 버전을 비교하는 방식으로 진행하였습니다. ESLint의 Typescript parser에서 TypeScript 자체 parser로 성공적으로 교체한 결과, 프로젝트 전체(2천개 이상의 파일) 호환성을 검사 했을 시 검사 시간이 3분에서 1분 이내로 크게 단축되는 효과를 볼 수 있었습니다. 또한 ESLint와 무관하기 때문에 ESLint 설정을 따로 수정하지 않아도 된다는 장점도 존재했습니다.

이처럼 코드를 정적 분석하고자 할 때 프로젝트의 요구 사항을 상세히 파악 후 TypeScript compiler API, ESLint의 API, 혹은 타 parser를 적절히 선택하여 상황에 맞게 사용하여야 합니다.

7. 네이버 GLACE 에서 인턴을 하며 느낀 점

문화

개발에 최대한 집중할 수 있게 조성된 분위기

사진. Glace CIC 개발 리더 최승락님의 인터뷰

Glace CIC는 기본적으로 책임근무제로 운영되는 만큼 자율성이 보장되지만, 동시에 그에 따른 책임이 존재합니다. 자신이 맡은 일을 꾸준히 잘 한다면 근무 장소나 출퇴근 시간에는 제약이 크게 없습니다. 네이버의 문화를 처음 접해본 사람이라면 “이런 문화가 지속될 수 있을까?”라는 의문을 가질 수 있지만, 실제로 팀원들 모두 책임감을 가지고 각자 맡은 일을 하는 분위기였습니다. 각자의 진행 상황은 정기적으로 Github과 미팅을 통해 투명하게 공유하며, 책임감 있게 개발해 나가는 것이 매우 자연스러웠습니다.

혼자하면 잘 안되던 공부가 열심히 공부하는 사람들이 있는 환경에서는 집중이 잘 되는 경향이 있습니다. 네이버에서도 이러한 전사적인 개발 문화가 개인에게 책임감을 더욱 길러주고, 또 자율성이 주어지는 만큼 그것에 보답하기 위해서 모두가 자연스럽게 열심히 하는 환경이 조성되지 않았나 생각합니다.

개발 소통은 기본적으로 Github 내에서 활발하게 이루어졌고, 이외 미팅이나 커뮤니케이션은 사내 협업툴인 Works와 Zoom으로 진행됐습니다. 팀 분위기 또한 화기애애했습니다. 팀원들 모두 서로를 존중한다는 것이 느껴졌고, 또 점심시간 마다 다양한 이야기를 하며 친목을 다지는 팀원들의 모습을 지켜볼 수 있었습니다.

네이버에는 기술 공유를 위해 다양한 공유 채널이 존재합니다. 네이버 플레이스의 Medium 기술 공유 블로그, 네이버 Tech Concert, 곧 개최되는 DEVIEW 등, 네이버는 대표 IT 기업인 만큼 기술 공유를 매우 중요시 한 다는 것을 알 수 있었습니다. 이렇게 다양한 공유 채널을 통해 배울 수 있는 것은 최대한 배우고, 또 반대로 나눌 수 있는 지식 및 경험이 있다면 모두와 공유하는 문화가 매우 유익하다고 느꼈습니다. 저 또한 앞으로 개발을 하며 제가 얻은 새로운 지식을 공유하는 습관을 가질 것입니다.

마치며

네이버는 일만 하는 단순한 직장이 아닌, 일을 하면서도 실력 높은 개발자로 성장할 수 있는 환경을 가진 개발 문화센터라고 생각합니다. 그중에서도 Glace는 자율적인 문화 및 개발 환경을 제공하여 개개인의 능력을 키울 수 있는 좋은 조직이라고 느꼈습니다.

네이버 Glace 플레이스개발 팀에서 인턴을 진행하는 동안 참 좋은 경험과 인연을 통해 프로젝트를 잘 마무리 하게 되었습니다. 마지막으로 인턴을 잘 끝낼 수 있도록 도와주신 팀원들에게 감사의 인사를 드립니다. 긴 글 읽어주셔서 감사합니다!

--

--