• javascript 
    • JQuery UI 에서 제공하고 있는 widgets 중 하나인 (Util)Menu 를 구현한 예제이다.

      • 이전 Accordion Menu와 달리, JS 로직에 대한 비중보다는, HTML 설계에 따라 전체적인 구현 복잡도가 달라지는듯 하다.<p>

        • 그 만큼 최초 HTML 설계가 중요하다고 할 수 있다.
      • (기능 설계에 따라 달라지지만)실 구현 로직은 크게 어렵지 않으며, 코드 주석을 통해 어렵지않게 이해할 수 있을것이다.

        <iframe height='300' scrolling='no' src='http://codepen.io/yanione/embed/mVXdVa/?height=300&theme-id=0&default-tab=result' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'></iframe>
        

    Read more


  • javascript 
    • JQuery 함수인 slideUp / slidedDown 함수와 Accordion Menu 를 직접 구현한 예제이다.

      • 코어(재귀를 통한 Animation 처리) 로직을 직접 구현해보기위한 예제이므로, 그 외 로직(DOM 처리, 조작 등)에서는 JQuery 를 사용하였다.<p>

      • 부드러운 움직임을 위한, 다향한 Easing 기능이 추가된 예제이다.

        • 즉 Easing 을 통해 부드러운 움직임을 위한 가속도(사람은 속도에는 둔감해도 가속도에는 민감하다)를 적용할 수 있다.<p>

           속도에는 둔감하고 가속도에는 민감한 예를 들자면 차가 일정한 속도로 달리고 있을 때 우리는 이 차가 얼마나 빨리 움직이는 것인지 잘 모른다.
           일정한 속도라면(즉, 가속도가 0이라면) 차안에서 가만히 서있을 수 있다. 하지만 급정거나 급출발하면 어떤가? 아무리 살짝(?) 급정거, 급출발을 해도 우리는 곧바로 느낄 수 있다.
           우리 몸이 힘을 맞았기 때문이다. UI 애니메이션에서 easing은 이런 가속도를 매끄럽게 해주는 일을 한다.
          
        • 애니메이션-easing에-숨은-비밀<p>

        • easing 테스트

        • easing 함수 generator

    • 위 slideUp / Down 함수를 응용하여 slideRight / slidedLeft 함수를 구현한 예제이다.

      • 코어 로직 중 Animation 처리를 위한 속성인 height 속성이 width 속성으로 변경된점을 제외하고는 동일하다.

    • 마지막으로 Menu Animation 이 구현된 예제이다.

    참고 URL

    Read more


  • architecture 

    1. JSXTransformer

    • JSXTransformer 내부에서 사용되는 Esprima-FB 파서는 기존 Esprima 파서를 확장(JSX 명세) 구현한 버전이다.

      • 현재 버전은 15001.1001.0-dev-harmony-fb 이며, 분석된 내용은 13001.1001.0-dev-harmony-fb 버전을 따른다.<p>

      • JSXTransformer CDN

    • 생성된 Parse 트리 구조는 모질라의 Parser AST 인터페이스 명세를 따르고 있다.(or 구현되어있다)<p>

    • 소스상의 parser 옵션 수정하여, (파서를 통해)수집된 토큰을 반환받을 수 있다.<p>

          
        function getAstForSource(source, options) {
            if (_astCache[source] && !options.disableAstCache) {
                return _astCache[source];
            }
            var ast = esprima.parse(source, {
                // 아래와 같이 tokens 속성을 추가 한다.
                tokens: true,
                comment: true,
                loc: true,
                range: true,
                sourceType: options.sourceType
            });
            if (!options.disableAstCache) {
                _astCache[source] = ast;
            }
            return ast;
        }
      

    • Esprima 공식 홈페이지

    • The ESTree Spec<p>

    • JSX Tree Spec

    2. 소스 수집 및 Parse 트리 가 생성되는 과정

    • 아래 소스는 Parser 에 의해 파싱될 원본 소스이다.

        <script type="text/jsx">
          
            function A(x, y){
          
                this.name = x;
          
                var elem = <div></div>;
          
                return elem;
            }
          
        </script>
      
    • 소스 수집 과정

      • 기본적으로 text/jsx 타입을 가진 Script Element 내부 소스(or 코드)는 브라우저에 의해 처리되지않는다.

          <script type="text/jsx">
              console.log('이 실행 코드는 브라우저에 의해 처리되지 않는다....');
          </script>
        
      • text/jsx 타입을 가진 모든 Script Element 를 수집한다.

        
          function runScripts() {
        
              var scripts = document.getElementsByTagName('script');
        
              // Array.prototype.slice cannot be used on NodeList on IE8
              var jsxScripts = [];
              for (var i = 0; i < scripts.length; i++) {
                  if (/^text\/jsx(;|$)/.test(scripts.item(i).type)) {
                      jsxScripts.push(scripts.item(i));
                  }
              }
        
              ...
        
        
              // loadScripts 함수를 호출하여, script element 내부에 포함된 원본 소스를 가져온다.
              loadScripts(jsxScripts);
          }
        
        
      • 만약 inline code 로 작성된 경우에는 script.innerHTML 속성 값을 통해 소스를 가져오고, script.src 속성 값이 있는 경우에는 xhr 을 통해 가져온다.

        
          // script.src 속성 값이 있는 경우
          if (script.src) {
            result[i] = {
                async: async,
                error: false,
                executed: false,
                content: null,
                loaded: false,
                url: script.src,
                options: options
            };
        
            // xhr 을 통해 해당 소스를 가져온다.
            load(script.src, function(content) {
        
                result[i].loaded = true;
                // content === source code
                result[i].content = content;
                check();
            }, function() {
                result[i].error = true;
                check();
            });
        
        } else {
        
            // inline code 로 작성된 경우..
            result[i] = {
                async: async,
                error: false,
                executed: false,
                content: script.innerHTML,
                loaded: true,
                url: null,
                options: options
            };
        }
        
      • 수집된 원본 소스Parser 를 통해 파싱된 후, 새로운 Script Element 를 통해 로드 및 실행된다.

        • 즉 생성된 Script Element 에 할당된 소스는 파싱된 소스를 가리킨다.<p>
          function run(code, url, options) {
        
              // script element 를 생성한다.
              var scriptEl = document.createElement('script');
              // transformCode 함수를 통해 파서가 호출된다.
              scriptEl.text = transformCode(code, url, options);
        
              // 파서에 의해 파싱된 소스
              console.log(scriptEl.text);
        
              // header element 의 자식 element 로 추가 시킨다.
              headEl.appendChild(scriptEl);
          }
        
      • 파싱된 소스원본 소스

        // 파서를 통해 파싱된 소스
        function A(x, y){
        
         this.name = x;
        
         var elem = React.createElement("div", null);
        
         return elem;
        }
        

    • Parse 트리 생성 과정

      • transformCode 함수가 호출된 후, 여러 단계(transformReact… , transform 등)를 거쳐, esprima.parse 함수를 통해, Parse 트리(AST)를 생성하는 함수인 getAstForSource 함수를 호출하게 된다.

        function getAstForSource(source, options) {
            if (_astCache[source] && !options.disableAstCache) {
                return _astCache[source];
            }
        
            // esprima 파서를 호출한다.(최종 파스 트리를 반환받는다)
            var ast = esprima.parse(source, {
                comment: true,
                loc: true,
                range: true,
                sourceType: options.sourceType
            });
            if (!options.disableAstCache) {
                _astCache[source] = ast;
            }
        
            // 생성된 파스 트리를 반환한다.
            return ast;
        }
        
      • 먼저 esprima.parse 함수가 호출되면, (여러 단계를 거친 후…) advance 함수를 통해 원본 소스에 포함된 각 문자(Char)의 의미를 분석 후, 분리된 토큰(어휘)을 객체화 시킨다.(내부적으로는 더 많은 단계를 거치게된다)

        • advance 함수 내부
        ```javascript
        
        function advance() {
        
            var ch;
        
            if (!state.inJSXChild) {
                skipComment();
            }
        
            // 소스 분석이 종료되는 시점
            // 현재 index(코드의 위치)가 length(포함된 소스의 전체 길이) 보다 크거나 같을경우
            if (index >= length) {
                return {
                    type: Token.EOF,
                    lineNumber: lineNumber,
                    lineStart: lineStart,
                    range: [index, index]
                };
            }
        
            // state.inJSXChild 가 true 인 경우(tag 열림('<') 상태가, 닫힘('>') 상태로 변경되었을 경우)
            // 즉 div element 가 닫힘('>') 문자로 인해 닫힌경우(단 열림('<') 문자 뒤에 '/' 문자가 오는 경우는 제외된다)
            if (state.inJSXChild) {
                return advanceJSXChild();
            }
        
            // 원본 소스에 포함된 각 문자(Char)를 아스키 코드로 변환한다.
            ch = source.charCodeAt(index);
        
        
            /*
              변환된 아스키 코드를 식별 후, 각 token 객체를 생성한다.
            */
        
            // Very common: ( and ) and ;
            if (ch === 40 || ch === 41 || ch === 58) {
                return scanPunctuator();
            }
        
            // String literal starts with single quote (#39) or double quote (#34).
            if (ch === 39 || ch === 34) {
                if (state.inJSXTag) {
                    return scanJSXStringLiteral();
                }
                return scanStringLiteral();
            }
        
            if (state.inJSXTag && isJSXIdentifierStart(ch)) {
                return scanJSXIdentifier();
            }
        
            if (ch === 96) {
                return scanTemplate();
            }
        
            // 해당 문자의 의미를 분석 후, 토큰 객체를 생성한다.
            if (isIdentifierStart(ch)) {
                return scanIdentifier();
            }
        
            // Dot (.) char #46 can also start a floating-point number, hence the need
            // to check the next character.
            if (ch === 46) {
                if (isDecimalDigit(source.charCodeAt(index + 1))) {
                    return scanNumericLiteral();
                }
                return scanPunctuator();
            }
        
            if (isDecimalDigit(ch)) {
                return scanNumericLiteral();
            }
        
            // Slash (/) char #47 can also start a regex.
            if (extra.tokenize && ch === 47) {
                return advanceSlash();
            }
        
            return scanPunctuator();
        }
        ```
        
      • 예를들어, 위 isIdentifierStart 함수로 해당 아스키 코드(문자)의 의미를 분석 후, scanIdentifier 함수를 통해 토큰(어휘) 객체를 생성한다.

        • isIdentifierStart 함수 내부

            function isIdentifierStart(ch) {
                // 해당 아스키 코드의 의미를 분석한다.
                return (ch === 36) || (ch === 95) ||  // $ (dollar) and _ (underscore)
                    (ch >= 65 && ch <= 90) ||         // A..Z
                    (ch >= 97 && ch <= 122) ||        // a..z
                    (ch === 92) ||                    // \ (backslash)
                    ((ch >= 0x80) && Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch)));
            }
          
        • scanIdentifier 함수 내부

            function scanIdentifier() {
          
                var start, id, type;
          
                start = index;
          
          
                // Backslash (char #92) starts an escaped character.
          
                /*
          
                // 토큰 타입
                Token = {
                    BooleanLiteral: 1,
                    EOF: 2,
                    Identifier: 3,
                    Keyword: 4,
                    NullLiteral: 5,
                    NumericLiteral: 6,
                    Punctuator: 7,
                    StringLiteral: 8,
                    RegularExpression: 9,
                    Template: 10,
                    JSXIdentifier: 11,
                    JSXText: 12
                };
          
                */
                // 식별된 토큰이 id 변수로 할당된다.
                // 예: function(분리된 토큰 문자열)
                id = (source.charCodeAt(index) === 92) ? getEscapedIdentifier() : getIdentifier();
          
          
                // There is no keyword or literal with only one character.
                // Thus, it must be an identifier.
          
                // 해당 토큰 타입을 설정한다.(식별자, 키워드, 일부 literal(null, boolean))
                if (id.length === 1) {
                    type = Token.Identifier;
                } else if (isKeyword(id)) {
                    type = Token.Keyword;
                } else if (id === 'null') {
                    type = Token.NullLiteral;
                } else if (id === 'true' || id === 'false') {
                    type = Token.BooleanLiteral;
                } else {
                    type = Token.Identifier;
                }
          
                // 생성된 토큰 객체를 반환한다.
                return {
                    type: type, // 4(Keyword)
                    value: id, // 'function'
                    lineNumber: lineNumber, // 3
                    lineStart: lineStart, // 2
                    range: [start, index] // 6, 14
                };
            }
          

      • 생성된 토큰(어휘) 객체를 통해 Parse(AST) 트리 구성을 위한 노드가 구성된다.

        - [FunctionDeclaration 명세](https://github.com/estree/estree/blob/master/spec.md#functiondeclaration)
                  
          ```javascript
          interface FunctionDeclaration <: Function, Declaration {
              // 노드 타입
              type: "FunctionDeclaration";
              // 해당 함수 선언식에 대한 식별자 정보
              id: Identifier;
          }
          ```
        - <span style="color:#6298c1">parseFunctionDeclaration</span> 함수 내부
                  
            ```javascript
                
            function parseFunctionDeclaration() {
                   
                var id, body, token, tmp, firstRestricted, message, generator, isAsync,
                    previousStrict, previousYieldAllowed, previousAwaitAllowed,
                    marker = markerCreate(), typeParameters;
            
                isAsync = false;
                if (matchAsync()) {
                    lex();
                    isAsync = true;
                }
            
                expectKeyword('function');
            
                generator = false;
                if (match('*')) {
                    lex();
                    generator = true;
                }
            
                token = lookahead;
            
                id = parseVariableIdentifier();
            
                ...
            
                tmp = parseParams(firstRestricted);
                firstRestricted = tmp.firstRestricted;
                if (tmp.message) {
                    message = tmp.message;
                }
            
                ...
            
                body = parseFunctionSourceElements();
            
                ...
           
                // 함수 선언식에 대한 Parse 트리 노드를 생성한다.
                return markerApply(
                    marker,
                    delegate.createFunctionDeclaration(
                        id,
                        tmp.params,
                        tmp.defaults,
                        body,
                        tmp.rest,
                        generator,
                        false,
                        isAsync,
                        tmp.returnType,
                        typeParameters
                    )
                );
            }
            ```
            
            ![](/blog/assets/images/posts/20151228/jsx_5.png)
        
      • 노드가 조합되어, 최종 Parse 트리가 생성된다.

        ![](/blog/assets/images/posts/20151228/jsx_6.png)
        
      • utils.append, utils.catchup 함수(또 다른 함수 등)로 각 노드 객체 및 state 등을 전달하여, 최종 결과물인 파싱된 소스를 만들어 간다.(즉 source buffer 를 채워나간다)

        • utils.append 함수 내부

          
            /**
             * Appends a string of text to the buffer
             *
             * @param {string} str
             * @param {object} state
             */
            function append(str, state) {
          
                if (state.g.sourceMap && str) {
                    state.g.sourceMap.addMapping({
                        generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
                        original: { line: state.g.sourceLine, column: state.g.sourceColumn },
                        source: state.g.sourceMapFilename
                    });
                    var transformedLines = str.split('\n');
                    if (transformedLines.length > 1) {
                        state.g.bufferLine += transformedLines.length - 1;
                        state.g.bufferColumn = 0;
                    }
                    state.g.bufferColumn +=
                        transformedLines[transformedLines.length - 1].length;
                }
                state.g.buffer += str;
          
                console.log(state.g.buffer);
            }
          
        • utils.catchup 함수 내부

          
            /**
             * Given a state fill the resulting buffer from the original source up to
             * the end
             *
             * @param {number} end
             * @param {object} state
             * @param {?function} contentTransformer Optional callback to transform newly
             *                                       added content.
             */
            function catchup(end, state, contentTransformer) {
          
                console.log('utils.catchup 함수 호출');
                console.error(state);
                console.error('source position', state.g.position, end);
          
                if (end < state.g.position) {
                    // cannot move backwards
                    return;
                }
                var source = state.g.source.substring(state.g.position, end);
                var transformed = updateIndent(source, state);
                if (state.g.sourceMap && transformed) {
                    // record where we are
                    state.g.sourceMap.addMapping({
                        generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
                        original: { line: state.g.sourceLine, column: state.g.sourceColumn },
                        source: state.g.sourceMapFilename
                    });
          
                    // record line breaks in transformed source
                    var sourceLines = source.split('\n');
                    var transformedLines = transformed.split('\n');
                    // Add line break mappings between last known mapping and the end of the
                    // added piece. So for the code piece
                    //  (foo, bar);
                    // > var x = 2;
                    // > var b = 3;
                    //   var c =
                    // only add lines marked with ">": 2, 3.
                    for (var i = 1; i < sourceLines.length - 1; i++) {
                        state.g.sourceMap.addMapping({
                            generated: { line: state.g.bufferLine, column: 0 },
                            original: { line: state.g.sourceLine, column: 0 },
                            source: state.g.sourceMapFilename
                        });
                        state.g.sourceLine++;
                        state.g.bufferLine++;
                    }
                    // offset for the last piece
                    if (sourceLines.length > 1) {
                        state.g.sourceLine++;
                        state.g.bufferLine++;
                        state.g.sourceColumn = 0;
                        state.g.bufferColumn = 0;
                    }
                    state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;
                    state.g.bufferColumn +=
                        transformedLines[transformedLines.length - 1].length;
                }
          
                state.g.buffer +=
                    contentTransformer ? contentTransformer(transformed) : transformed;
          
                state.g.position = end;
            }
          

    • transform 함수를 통해, 최종 파싱된 소스가 포함된 객체를 반환하여, 소스 수집 과정에서 말한바와 같이 Script Element 를 통해 로드 및 실행된다.

        function transform(visitors, source, options) {
          
            options = options || {};
            
            var ast;
            try {
                // 파스 트리를 생성한다.
                ast = getAstForSource(source, options);
            } catch (e) {
                e.message = 'Parse Error: ' + e.message;
                throw e;
            }
          
            var state = utils.createState(source, ast, options);
            state.g.visitors = visitors;
          
          
            ....
              
            traverse(ast, [], state);
          
            utils.catchup(source.length, state);
              
            // state.g.buffer: 최종 파싱된 소스
            var ret = {code: state.g.buffer, extra: state.g.extra};
          
            ....
              
            // 파싱된 소스(ret.code)를 포함하는 객체를 반환한다.
            return ret;
        }  
      

    3. 마치며

    • 이 글은 파서 과정 중 포인트가 될만한 부분을 골라내어 분석한 글이며, 더 자세한 내용을 알기위해서는 더 깊이있는 소스 분석이 필요하다.

    • 요즘 트렌드인 React 사용 방법과는 크게 상관 부분이지만, JSXTransformer 와 같은, 자신만의 Transpiler(or 언어) 를 제작해보고 싶은 사람이라면, 한번쯤 이와 같은 소스를 분석해보는것도 좋을 듯 하다.

    Read more