2021-09

source: categories/daily-issue/issue4.md

9월 1일

github 폴더가 클릭되지 않는 이슈

하위 폴더(숨김 파일 및 폴더) 내의 .git 폴더를 삭제하여 문제를 해결했습니다.
루트 폴더에는 .git이 하나만 있어야 합니다.

리액트, 뷰 컴포넌트간 통신방법… 제대로 공부해야될거 같다..

대충 이론상 이해했어도 인터렉션 구현할 때 서로 컴포넌트 끼리 값을 주고받아야될 때…
이론은 알겠는데 코드상으로 어떻게 정리 해야되는건지 모르겠네…. 휴 어렵네..

동일한 레벨의 컴포넌트 간 통신..
뷰 공부할 때 예시는 봤는데, 구현해보기도하고..
근데 그 예시를 지키려면 따로 나누고 싶은 컴포넌트들도 하나로 합쳐야되고..
흐음.. 그렇게 만들어야하는건가.
아니면 다른 방법이 있는건가.

여튼 더 파야될거같다.

스토리북 addon - 화면에 보이는 것들 조정

9월 3일

리액트 - 이미지 레이지로드

상황

  • 이미지 로드되는 시점이 서로 다 달라서 처음 로드된 화면이 엉성하게 보이는 이슈
  • 빠르게 로드된 이미지는 애니메이션 시작점부터 제대로 보여서 자연스러우나 애니메이션이 실행되고나서 진행된 도중에 로드된 이미지는 부자연스럽게 보임
  • 또한 이미지 로드되는 시점이 서로 달라 처음 로딩시 영역이 뚝뚝 끊겨서 늘어나는 것처럼 보이는 현상도 발생

해결방법

  1. 아래 oxMain 클래스를 가진 요소에 opacity: 0 속성을 주었습니다.
  2. lazy-load를 적용해야하는 img 태그에 lazy-load 클래스명과 lazy-src 속성을 적용했습니다.
  3. 그리고 Promise 객체의 allSettled라는 static 메소드를 활용하여 lazy-load 클래스를 가지고있는 img 태그들의 이미지 로드가 완전히 끝나는 시점을 인식하도록 했습니다.
  4. 이미지 로드가 완전히 끝나면 oxMain 요소에 is_loaded 클래스를 추가하여 opacity: 1 속성을 주었습니다.

Promise.allSettled 메소드와 Promise.all 메소드의 차이점

  • Promise.allSettled 메소드는 순회 가능한 객체를 통해 나열된 Promise 각각이 resolve를 하던 reject를하던 상관없이 각 Promise의 결과를 배열 형태로 반환합니다.
  • Promise.all 메소드는 순회 가능한 객체를 통해 나열된 Promise들 중에서 어느것 하나라도 reject하는 경우 이를 사용해 자기 자신도 reject합니다.

이러한 차이 때문에 요즘은 Promise.all 보단 Promise.allSettled 메소드를 많이 사용한다고합니다.


import React, {useEffect, useRef} from 'react'
import './MainPage.css'
import imgTitle from '../../resources/picture/oxQuizTitle3.png';
import imgH1 from '../../resources/picture/h1_fix.png';
import imgH2 from '../../resources/picture/h2_fix.png';
import btnStart from '../../resources/picture/btnStart3.png';
import imgCloudMain from '../../resources/picture/cloudMain.png'
import imgHill from '../../resources/picture/hill3.png'
import {Link} from 'react-router-dom';
import lottie from 'lottie-web';


function MainPage() {

    const container1 = useRef(null)
    const container2 = useRef(null)
    const container3 = useRef(null)

    const imgLoad = () => {
        const imgArr = Array.prototype.slice.call(document.querySelectorAll('.lazy-load'));
        Promise.allSettled(imgArr.map((item, index) => {
            return new Promise(resolve => {
                item.addEventListener('load', () => {
                    resolve();
                })
                item.src = item.getAttribute('lazy-src');
            })
        }))
            .then(_ => {
                document.querySelector('.oxMain').classList.add('is_loaded');
            })
            .catch(err => console.log(err));
    }


    useEffect(() => {
        lottie.loadAnimation({
            container: container1.current,
            renderer: 'svg',
            loop: true,
            autoplay: true,
            animationData: require('../../resources/svg/lottie_gift.json'),
        })
        lottie.loadAnimation({
            container: container2.current,
            renderer: 'svg',
            loop: true,
            autoplay: true,
            animationData: require('../../resources/svg/lottie_play1.json'),
        })
        lottie.loadAnimation({
            container: container3.current,
            renderer: 'svg',
            loop: true,
            autoplay: true,
            animationData: require('../../resources/svg/lottie_play2.json'),
        })
        imgLoad();
    }, [])

    return (
        <div className="oxMain">
            <div className="LottieGift" ref={container1}/>
            <div className="wrapImgTitle">
                <img lazy-src={imgTitle} className="lazy-load imgTitle" alt=""/>
            </div>
            <div className="wrapImgH1">
                <img lazy-src={imgH1} className="lazy-load imgH1" alt=""/>
            </div>
            <div className="wrapImgH2">
                <img lazy-src={imgH2} className="lazy-load imgH2" alt=""/>
            </div>
            <div className="wrapButton">
                <Link to="/quiz">
                    <img lazy-src={btnStart} className="lazy-load btnStart" alt=""/>
                </Link>
            </div>
            <div className="wrapOtherImgs">
                <img lazy-src={imgCloudMain} className="lazy-load imgCloudMain" alt=""/>
                <img lazy-src={imgHill} className="lazy-load imgHill" alt=""/>
            </div>
            <div className="LottiePlay1" ref={container2}/>
            <div className="LottiePlay2" ref={container3}/>
        </div>

    )
}

export default MainPage

9월 6일

리액트 - 이미지 레이지로드2 상위 컴포넌트에서 레이지로드 함수 관리, 하위 컴포넌트로 레이지로드 함수 전달해서 코드 간단하게하기

React Router로 렌더링하는 컴포넌트에 prop 전달하기

<Route exact path='/education' component={Education}/>

포폴 개편 작업을 하다가 react-router를 사용하여 위와 같이 각 path 마다 컴포넌트를 뿌려줄 때, props는 어떻게 전달해주어야 하는지 찾아보았다.

<Route exact path='/education' component={Education} education={data}/>

예를들어, Education 컴포넌트에서 education이라는 prop이 필요하다고 가정해보자.

Education 컴포넌트로 어떻게 전달해야할지 몰라 저렇게 일단 넣어보면 원하는대로 동작하지 않는다.
React Router는 컴포넌트로 education이라는 prop을 전달해주지 않고 그냥 무시하기 때문이다.

<Route
path='/education'
component={() => <Education education={data} />}/>

위의 방법이 먹히지 않았으니, 두번째 방법으로 적용해본다면 원하는대로 작동은 한다!
하지만 공식 docs에 따르면 performance측면에서 좋은 방법은 아니라고 나와있다.

이유는 component propinline-function형태로 값을 주면, 렌더링을 할 때마다 새로운 컴포넌트를 생성한다.
따라서, 원래라면 컴포넌트가 이미 마운트가 한번 되고 그후로는 업데이트되어야 하는데,
이 컴포넌트를 언마운트 시킨 후, 매번 불필요하게 ‘재 생성'하여 마운트 하기 때문에 퍼포먼스 측면에서 좋지 않다고 한다.

그렇다면 best solution은 무엇일까?

<Route
path='/education'
render={() => <Education education={data} />}/>

Route 컴포넌트의 component prop을 사용하지 말고 render prop을 사용하는 것이다.
render prop은 함수형 컴포넌트를 수용하고, 위의 방법을 사용했을 때 처럼 불필요하게 다시 마운트 되지 않는다.

로띠는 네트워크 리퀘스트가 아니다. 빌드하면 소스에 다 포함되어 나간다.

그렇기 때문에 로띠 라이브러리 적용이 느리게돼서 아래 영역이 밀린다 싶으면, 그 영역에 height 값을줘서 안밀리게하던가 해야된다.

9월 13일 교육 이슈

웹팩 깃발

  1. ejs 변수명 따로 설정안해도 includelocals 변수명으로 넘긴값 가져올 수 있음
  2. validator에서 outline 체크 후 밸리 돌리면
    1. heading
    2. structure

    위 구조 동일하게 나와야된다.

  3. " 이런거, 아스키코드 여러종류있으니까 개발에서 인식할 수 있는걸로 입력! 주의!
  4. 깃발 path 에러 (window OS 고려 안한거 수정)

    window에서 돌리면

    위와 같이 나왔다.

     apiOptions: {
         cssImageRef: `../${spriteImagePath(dir).replace(/^(dist|build)\//,'')}.png`,
             generateSpriteName: (path) => {
             const fileName = path.substring(value.lastIndexOf('/') + 1).replace('.png', '')
             return `${dirName(dir)}-${fileName}`
         }
     },
    

    위와 같이 되어있던거.. (mac에선 괜찮았을지 모르나 window에서 에러)
    아래와 같이 수정하면 window에서도 잘 작동한다!

    수정

9월 13일 오후 마크업 과제

9월 13일 - 이런 디자인은 제발 그만!

9월 15일 - 스크린 리더기 테스트

  • iOS -> 설정 -> 손쉬운사용 -> VoiceOver
  • Android -> 설정 -> 접근성 -> 시각 -> TalkBack

  • 동작방법: 좌우 스와이프 = 순차이동, 더블탭 = 선택

9월 27일

vue, react를 공부하면서 느낀점 (백, 프론트할거없이 총체적으로 공부해야되는 이유)

react, vue 프레임워크를 공부하며 느낀점

  1. 백~프론트 전반적인 이해가 없으면 제대로된 컴포넌트 설계 자체가 불가능합니다.

    정말 안타까운 현실이지만, xx에서하던 스토리북 사용 → 컴포넌트 정리는 에이전시 특성상 빠듯한 기한 안에 완성해야된다는 현실적인 문제를 안고있습니다.

    때문에 컴포넌트 설계에 대해 진지하게 생각해볼 시간도, 지식도 없는 상황이었다는 것을 새삼 느끼게 되었습니다.

    지금 생각해보면 XXX가 컴포넌트 작업은 리액트로하고 실제 FE에선 뷰로 했던게 그나마 다행이었던 것 같습니다. 같은 프레임워크를 사용하고 마크업에서 구현한 컴포넌트를 그대로 가져다쓰는 구조였다면 아마 난리가 났을겁니다. (컴플레인 아마 엄청 많았을겁니다..)

    그런데 이는 xx 고유의 문제라는 생각은 안들었습니다. 진짜 에이전시의 한계였던 것 같습니다.

    특정 기한 내에 프로젝트를 완수해야되는 에이전시 특성상 10~15년전 코드로 지금까지 작업할 수밖에 없는 구조적인 이유, 자기개발은 등한시하고 시간에 쫓겨 마크업만 찍어내는 그런 구조적 문제를 봤을 때는 어쩔 수 없는 현상이라고 생각이듭니다.

  2. REST API든 GraphQL이든 데이터와 마크업을 연동하는 작업이 주인 프론트개발자로썬 백~프론트 전반적인 이해는 필수입니다.

    백단에서 데이터를 어떻게 설계했는지, 그 설계도 그리고 API를 넘겨받은 다음에 프론트단에서 REST API 또는 GraphQL로 연결하는 작업을 합니다.

    백단에 대한 지식 없이는 불가능한 작업입니다.

  3. xx에 숨겨져있던 숨은 고수

    이번에 컴포넌트간 통신에 대해 공부하면서 느낀 것이 있습니다.

    상위 컴포넌트에서 하위 컴포넌트로 통신할 땐 props를 활용하고, 하위 컴포넌트에서 상위 컴포넌트로 통신할 땐 event emit을 활용해 통신한다.

    이는 리액트, 뷰를 할것 없이 컴포넌트 설계 할때 기본적인 설계 방법입니다.

    이를 위해 컴포넌트를 설계할 때, 컨테이너 컴포넌트(데이터조작), 프레젠테이셔널 컴포넌트(화면에 그리기만하는)로 나누는 것이 일반적이죠.

    이렇게 하는 이유는 리액트 용어로는 state, 뷰 용어로는 data의 흐름을 단순화시켜 state, data의 흐름을 파악하기 쉽게 하고, 예상치 못한 에러를 줄이기 위해서입니다.


    그런데 이와 같은 구조를 전 xx에서 본적이 있습니다.

    프로젝트 코드였는데, 10~15년전 방식으로 구현한 것이지만, xx처럼 아직 jQuery를 쓰는 회사에서 어떤 선구자가 jQuery를 위와 같은 방식으로 사용했던 것 같습니다.

    특정 이벤트에 이벤트명을 지어 해당 이벤트를 emit 시키고 상위 컴포넌트에서 그 이벤트를 받아 특정 데이터를 조작하는 로직을 처리하는 구조를 본적이 있었는데, 그 당시엔 제가 위와 같은 지식이 없었기 때문에 왜 이렇게 번거롭게 처리하지? 라는 생각을 했었습니다.

    그런데 state, data의 흐름을 단순화시켜 에러를 최소화하기위해 그런식으로 작성했다는 것을 최근 알게되었습니다.

    과거 xx에 정말 숨겨진 고수, 선구자가 있었던 것 같습니다.

REST API와 GraphQL 관련해선 이번주에 정리해서 올리도록 하겠습니다.

REST API vs GraphQL (feat. 인터렉션을 프론트엔드 개발자가 담당하는 이유)

이번엔 REST API와 GraphQL 관련 내용입니다.

최근 react 기반으로 REST API와 GraphQL을 활용해 정말 간단한 CRUD를 구현해봤습니다. + 무한스크롤링 인터렉션도 구현해봤습니다.

우선 API라는게 무슨뜻일까요?

API란 간단히 말해서 한 프로그램에서 다른 프로그램으로 데이터를 주고받기 위한 방법을 정의한 것이라고 보시면됩니다.

마크업에서 자주 사용하는 swiper 플러그인도 API가 있습니다.

이 API는 swiper 플러그인과 데이터를 통신하는 방법입니다.

그 정리되어있는 방법을 보고 정의되어있는 옵션값, 메소드들을 활용하여 swiper 플러그인을 조작합니다.

즉, 데이터 통신 방법이 API입니다.

즉, 백앤드 개발자들이 API를 작성해 프론트로 넘겼다 라는 것은 백앤드 개발자들이 설계해놓은 데이터베이스와 어떤 방식으로 통신하면되는지 그 방법에 대해 정리해 프론트개발자에게 넘겼음을 뜻합니다.

프론트 개발자들은 해당 API를 보고 코드를 작성하구요.

여기서 REST API라는 것이 나옵니다.

REST API는 별거 없습니다. API를 작성하는 규범? 규칙 같은 것입니다.

백단 데이터와 통신하는 API를 작성할 때 이런식으로 작성해! 라는 것입니다.

하지만 이런 REST API의 규칙은 너무나도 많습니다.

개발자들이 일일이 그 규칙에 맞게 코드를 작성하는 것은 쉽지않은 일이죠.

때문에 이를 라이브러리로 만들어놓은 것들이 많습니다.

그 중 대표적인것이 바로 express 입니다.

express 프레임워크를 사용하면 자잘한 것들에 대해 처리할 필요가 없어지죠.

관련 동영상: 정재남 리액트 풀스택 강의

REST API

import {v4} from "uuid";
import {readDB, writeDB} from "../dbController.js";

const getMsgs = () => readDB('messages') 
const setMsgs = data => writeDB('messages', data) 
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: ({query: {cursor = ''}}, res) => {
            const msgs = getMsgs();
            const fromIndex = msgs.findIndex(msg => msg.id === cursor) + 1
            res.send(msgs.slice(fromIndex, fromIndex + 15))
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        method: 'get',
        route: '/messages/:id',
        handler: ({params: {id}, res}) => { 
            try {
                const msgs = getMsgs();
                const msg = msgs.find(m => m.id === id) 
                if (!msg) throw Error('not found')
                res.send(msg)
            } catch (err) {
                res.status(404).send({ error: err })
            }
        }
    },
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        handler: ({body}, res) => {
            try {
                if (!body.userId) throw Error('no userId');
                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), 
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg) 
                setMsgs(msgs);
                res.send(newMsg) 
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', 
        handler: ({body, params: {id}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id) 
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                const newMsg = {
                    ...msgs[targetIndex], 
                    text: body.text, 
                }
                msgs.splice(targetIndex, 1, newMsg)
                setMsgs(msgs)
                res.send(newMsg) 
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
    { // DELETE MESSAGE
        method: 'delete',
        route: '/messages/:id',
        handler: ({params: {id}, query: {userId}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== userId) throw '사용자가 다릅니다.'

                msgs.splice(targetIndex, 1)
                setMsgs(msgs);
                res.send(id) 
            } catch (err) {
                res.status(500).send({error: err}) 
            }
        }
    },
]

export default messagesRoute

REST API는 어떻게보면 쉽습니다.

express 에 정의되어있는 get, post , put, delete 메소드로 요청을 받아 어떤식으로 처리해야되는지만을 정의해주면됩니다.

단순하면서도 초심자도 작성하기 쉬운 영역이라고 느꼈습니다.

하지만 여기에 로그인 기능 + 여러 보안 이슈가 있는 데이터를 다루는 API라면 그때부터 어려워지는 것 같습니다.

여튼 REST API 관련 지켜야할 준수사항들은 웬만하면 express 가 알아서해주기에 단순한 어플리케이션은 빠르게 구현할 수 있습니다.

GraphQL

REST API와 GraphQL의 가장 큰 차이점은

  • REST API 서버는 route를 사용해 사용자가 요청한 route에 따라서 그에 대응하는 response를 내려주는 형태
  • GraphQL 서버는 오직 /graphql이라는 path 하나로 그 안에서 어떤 요청인지를 나누는 형태

즉, 위의 코드를 보시면 REST API는 get, put, post, delete 별로 다 나눠서 API를 작성했었죠?

GraphQL은 그렇지 않습니다.

import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
// import cors from 'cors' // cors 라이브러리 불러온거 삭제해줍니다.
import resolvers from './resolvers/index.js'
import schema from './schema/index.js'
import {readDB} from "./dbController.js";

// ApolloServer 서버를 생성합니다.
const server = new ApolloServer({
    // typeDefs 라는 것이 필요하고, schema(스키마)라는게 필요합니다.
    typeDefs: schema,
    // 그리고 resolvers이라는 정보가 필요하게됩니다.
    resolvers,
    // 그리고 resolvers가 참조할 데이터.. 즉, DB 역할을하는 models 부분이 정의가 되어있어야 합니다.
    context: {
        db: {
            // 아래 정의에 대해선 이따 하도록 하겠습니다.
            messages: readDB('messages'),
            users: readDB('users'),
        }
    }
});

const app = express();
await server.start(); 
server.applyMiddleware({
    app,
    path: '/graphql',
    cors: {
        origin: [
            'http://localhost:3000',
            'https://studio.apollographql.com'
        ], // 클라이언트 서버
        credentials: true,
    }
})

// 서버 경로는 8000번
await app.listen({port: 8000})
console.log('server listening on 8000...')

위와 같이 new ApolloServer() 를 통해 테이블 타입을 정의 typeDefs 해주고 (스키마 정의가 따로 필요합니다.)

그 아래 resolvers 프로퍼티를 정의해줍니다.

// resolvers 내용
import {v4} from "uuid";
import {writeDB} from "../dbController.js";

const setMsgs = data => writeDB('messages', data)

const messageResolver = {
    Query: {
        messages: (parent, {cursor = ''}, {db}) => {
            const fromIndex = db.messages.findIndex(msg => msg.id === cursor) + 1;
            return db.messages?.slice(fromIndex, fromIndex + 15) || [];
        },
        message: (parent, {id = ''}, {db}) => {
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        createMessage: (parent, {text, userId}, {db}) => {
            const newMsg = {
                id: v4(),
                text, 
                userId, 
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) 
            setMsgs(db.messages)
            return newMsg
        },
        updateMessage: (parent, {id, text, userId}, {db}) => {
            const targetIndex = db.messages.findIndex(msg => msg.id === id)
            if (targetIndex < 0) throw Error('메시지가 없습니다.') 
            if (db.messages[targetIndex].userId !== userId) throw Error('사용자가 다릅니다.') 

            const newMsg = {
                ...db.messages[targetIndex],
                text,
            }
            db.messages.splice(targetIndex, 1, newMsg)
            setMsgs(db.messages)
            return newMsg; 
        },
        deleteMessage: (parent, {id, userId}, {db}) => {
            const targetIndex = db.messages.findIndex(msg => msg.id === id)
            if (targetIndex < 0) throw '메시지가 없습니다.'
            if (db.messages[targetIndex].userId !== userId) throw '사용자가 다릅니다.'
            db.messages.splice(targetIndex, 1)
            setMsgs(db.messages)
            return id
        },
    },
    Message: {
        user: (msg, args, {db}) => db.users[msg.userId]
    },
}

export default messageResolver

그리고 new ApolloServer() 를 통해 생성한 인스턴스를 가지고 applyMiddleware 메소드를 실행하여 들어오는 요청에 대해 미들웨어를 적용해줍니다.

즉, graphql을 통해 들어오는 요청이 이 미들웨어를 통해 /graphql 요청으로가고 resolvers 를 통해 어떤 요청인지 분류가됩니다.


그럼 GraphQL을 사용하는 이유는 무엇일까요?

사실 아직까지 큰 차이점은 모르겠습니다.

다만 정재남님의 강의를 통해 느꼈던 것들에 대해 말씀드리겠습니다.

  1. 개발 용이성? 코드는 REST API보다 훨씬 어렵습니다. 데이터 요청 query를 직접 작성해야돼서 자바스크립트에 익숙한 저희에게 문법적인 혼란을 줍니다. 하지만 익숙해지기만 한다면 개발할 때 더 편할거같다는 생각이 들었습니다. 저희가 REST API를 통해 개발할 때는 postman이란 것을 사용해야됩니다.

    왜냐. get 요청을 제외한 post, put, delete 요청은 url로 보낼 수 있는 요청이 아니기 때문이죠.

    postman 과 같은 특별 프로그램을 통해서 테스트를 해야됩니다.

    graphql은 graphql 자체 라이브러리에서 이러한 테스트를 할 수 있는 방법을 제공합니다.

  2. GraphQL을 사용하면 데이터의 원하는 프로퍼티 값만 뽑아올 수 있습니다.

     const data = [
     	{
     		a: "123",
     		b: "456",
     	},
     	{
     		a: "345",
     		b: "986",
     	},
     	// ...
     ]
    

    어떤 데이터 형태가 위와 같다면 REST API로 요청을 보내어 데이터를 뽑아오면 a , b 프로퍼티 모두 뽑아옵니다.

    특정 프로퍼티만 뽑아오고싶어! 가 안됩니다.

    하지만 GraphQL은 특정 프로퍼티에 대한 값만 뽑아올 수 있습니다.

    위와 같은 데이터형태라면 a 프로퍼티만 뽑아온다던지, b 프로퍼티만 뽑아온다던지가 가능합니다.

    → 큰 차이는 아니겠지만 이를 통해 메모리를 좀 더 효율적으로 사용해 속도, 반응성을 조금이나마 더 빠르게 할 수 있는 것 같습니다.


인터렉션

왜 프론트앤드 개발자가 인터렉션을 담당할까.

xx에서 일하면서 좀 많이 궁금했던 부분이었습니다.

인터렉션은 마크업 개발자도 충분히 다룰 수 있는데 왜 프론트앤드가 전적으로 담당하는지가 항상 궁금했었습니다.

  • 마크업 개발자의 역량부족?

    마크업 개발자의 역량 부족이 원인일수도있습니다.

    하지만, 그보다 더 큰 이유는 데이터를 직접 다루냐 안다루느냐의 차이가 더 큰 것 같습니다.

    데이터 연동이 하나도 없는, 정적인 곳에서의 인터렉션이라면 마크업개발자가 인터렉션을 구현해도 큰 문제는 없을 것입니다.

    하지만 보통 페이지들은 데이터 연동이 다 되어있고, 그 되어있는 영역에 인터렉션을 적용합니다.

    그리고 그 인터렉션을 적용하는 방법, 방법론을 react, vue 프레임워크에선 대부분 제안하고 있습니다.

    즉, 그런 방법을 고려하지않은 마크업 개발자가 작성한 인터렉션보단, 각 프레임워크에서 제안하는 방법론을 통해 개발한 인터렉션이 훨씬 더 효율적일수밖에 없습니다.

    무한 스크롤링 예시를 들겠습니다.

    리액트 기반의 무한스크롤링 구현을 위해서 자바스크립트 빌트인 객체인 IntersectionObserver 를 사용합니다.

    특정 요소가 화면에 보이는지 안보이는지를 실시간으로 체크해주는 객체입니다.

    이런 빌트인 객체와 리액트에서는 useEffect 라는 라이프사이클 훅을 사용해 이를 구현합니다.

    그리고 useEffect 훅에 reat-query 라이브러리를 추가해 useInfiniteQuery 기능을 사용하기도합니다.

    즉, 미리 구현되어있는 라이브러리, 메소드를 활용해 인터렉션을 더 견고하게 만들 수 있다는 것입니다.


    하지만 한편으론 그렇다고 마크업 개발자 포지션이 나쁜 것만은 아닌 거 같다는 생각이 듭니다.

    왜냐하면 하드 코딩을 경험해볼 수 있는 유일한 포지션이기 때문입니다.

    바로 프론트앤드 개발자로 시작하면 react 또는 vue 에서 제공하는 라이브러리, 프레임워크들만 사용하게됩니다.

    즉, 미리 만들어져있는 것들을 통해 어플리케이션 개발을하다보니 그 아래에 깔려있는, 이렇게 구현한 이유 또는 이론에 대해서 접하기가 쉽지 않습니다.

    vue도 vue를 개발하는 코어팀의 핵심 목표가 코딩을 아무것도 모르는 사람이 vue를 활용해 어플리케이션을 쉽고 빠르게 개발할 수 있도록 하는 것인 만큼, (react도 마찬가지일거라고 생각합니다.)

    정말 활용하기 쉽게 만들어져있다는 장점이 있지만, 반대로 그 핵심 이론에 대해 접하기가 쉽지 않습니다.

    하지만 마크업 개발자들은 상대적으로 이런 프레임워크를 사용해 개발하지 않다보니 어떤 인터렉션을 구현할 때 순수 자바스크립트로 구현하는 경우가 많습니다.

    그리고 그 과정을 통해 왜 이런식으로 코드를 작성하고 개발해야되는지를 깨닫게되죠.

    제 경험상 그런거 같습니다.

    마크업 개발자를 통해 그런것들을 느끼고 현재 리액트와 뷰 프레임워크에 대해 공부하고 사용하다보니, 왜 리액트나 뷰가 이런식으로 짜여져있는지가 훨씬 더 잘 이해가 되는거 같습니다.