클린코드 - 02. 의미있는 이름

의도를 분명히 밝혀라

변수(혹은 함수나 클래스)

  • 존재이유?
  • 수행기능?
  • 사용방법?

이 모든 것을 답하기 위해 주석이 필요하다면 의도를 분명히 드러내지 못했다는 뜻.

 

그릇된 정보를 피하라

그릇된 단서는 코드의 의미를 흐린다

  • 나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용해도 안된다
    • 실제 컨테이너가 List가 아닐 경우 List로 명명하면 그릇된 정보를 제공하므로 이렇게 명명하지 않는다
    • 실제 List여도 컨테이너 유형의 이름에 넣지 않는 것이 바람직하다
  • 유사한 개념은 유사한 표기법을 사용한다
    • 이름만 보고 정보를 추측하기 때문에 일관성이 떨어지는 표기법은 그릇된 정보다
    • 연관성이 없는 것에 대해 비슷한 단어를 사용했을 경우 연관이 있다고 생각하는 오류를 범하기 때문에 흡사한 이름을 사용하지 않는다.
  • I과 O는 숫자와 유사하기 때문에 그릇된 정보가 될 수 있다

 

의미있게 구분하라

단순 컴파일러나 인터프리터를 통과하는 네이밍은 문제를 일으킨다.

  • 연속적인 숫자를 덧붙이지 않는다
    • a1, a2, ....aN
    • 의도적인 이름과 정반대이며 아무런 정보를 제공하지 못하는 이름이다
    • 저자의 의도가 드러나지 않는다
  • 불용어를 추가한 이름은 아무런 정보도 제공하지 못한다
    • Info, Data는 a, an, the와 마찬가지로 의미가 불분명한 불용어이다
      • 접두어를 사용하지 말라는 소리가 아니다
      • Name, NameString에서 Name은 String일 확률이 대부분 이므로 불용어다
      • customerInfo - customer, accountData - account, TheMessage - message와 구분이 안되기 때문에 차이를 알 수 있도록 한다 

 

발음하기 쉬운 이름을 사용하라

발음하기 어려운 이름은 토론하기도 어렵다

  • 긴 단어를 합쳐서 줄임말로 쓸 경우 의사소통에 문제가 생길 수도 있다

 

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름(짧은)보다 길더라도 검색해서 찾아내기 쉬운 이름이 좋다

  • e로만 검색할 경우 무수히 많은 검색 결과가 나온다
  • 버그가 있을 경우 찾기 쉬운 이름이 짧은 이름보다 때론 좋다

 

인코딩을 피하라

  • 헝가리식 표기법
    • 에디터 등의 발전으로 변수 이름에 타입을 인코딩할 필요가 없다
    • 타입을 바꾸기가 어려워지고 읽기도 힘들다
    • 독자를 오도할 가능성도 커진다
  • 멤버 변수 접두어
    • 클래스와 함수는 접두어가 필요없을 정도로 작아야한다
  • 인터페이스 클래스와 구현 클래스

 

자신의 기억력을 자랑하지 마라

  • 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야한다면 바람직하지않다
    • 일반적인 변수 이름이 아니었기에 생기는 문제
  • 문자 하나만 사용하는 변수 이름은 문제가 있다
    • 루프에서 범위가 작고 다른 이름과 충돌하지 않을 때는 괜찮다
      • 루프에서 반복 횟수 변수는 전통적으로 한 글자를 사용하기 때문에...
    • a와 b를 사용한다고 가정했을때 c를 선택해 사용한다면 최악이다

 

클래스 이름

  • 명사나 명사구가 적합하며 동사는 사용하지 않는다.
  • Manager, Processor, Data, Info 등과 같은 단어는 피한다

 

메서드 이름

  • 동사나 동사구가 적합
  • Javabean 표준에 따라 값 앞에 get, set, is를 붙인다

 

기발한 이름은 피하라

  • 의도를 분명하고 솔직하게 표현해야한다

 

한 개념에 한 단어를 사용하라

  • 추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다
    • fetch, retrieve, get | controller, manager, driver으로 제각각 쓰면 혼란스럽다
  • 이름이 다를 경우 당연히 클래스, 타입도 다르다 생각한다

 

말장난을 하지마라

  • 한 단어를 두 가지 목적으로 사용하지 마라
    • add가 하나의 값에 더한다는 의미라면 집합에 값을 추가하는 경우에는 add보다 insert나 append라는 이름을 사용한다

 

해법 영역에서 가져온 이름을 사용하라

기술 개념에는 기술 이름이 가장 적합한 선택이다

  • 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어 등을 사용해도 괜찮다

 

문제 영역에서 가져온 이름을 사용하라

'프로그래머 용어'에 없다면 문제 영역에서 이름을 가져와도 괜찮다

 

의미있는 맥락을 추가하라

스스로 의미가 분명한 이름이 없지 않다

  • 접두어를 붙인다
    • firstName -> addrFirstName이라는 접두어를 통해 주소의 일부라는 사실을 분명히한다

 

불필요한 맥락을 없애라

애플리케이션 네임을 모든 클래스의 이름의 시작에 넣는 것은 바람직하지 않다

 


 

 

문헌

클린코드 - 로버트 C. 마틴

클린코드 - 01. 깨끗한 코드

원초적 난제

기한에 맞추려면 나쁜코드를 양산할 수 밖에 없다고 느낄 것이다.

하지만 코드를 최대한 깨끗하게 유지하는 습관이 기한 맞추는 유일한 방법일지 모른다.

 

깨끗한 코드라는 예술?

"깨끗한 코드가 무엇일까?"

"어떻게 작성할까?"

'깨끗한 코드'가 무엇인지 조차 모른다면 노력해봤자 소용없다.
하지만 이 말이 '깨끗한 코드'와 '나쁜 코드'를 구분할 줄 알아야만 '깨끗한 코드'를 작성할 수 있다는 의미는 아니다.
‘코드 감각’이 있다면 개선할 방안이 떠오를 것이다.

다만 '코드 감각'은 누군가는 타고날수도 있고 누군가는 경험을 통해 얻을 수도 있다. 

 

깨끗한 코드란?

비야네 스트롭스트룹

"우아하고 효율적인 코드를 좋아한다."

우아하고 효율적인 코드를 좋아한다. 효율적인 코드는 단순 속도만을 의미하지 않고 CPU의 자원또한 포함할 것인다.

함수가 너무 많은 일을 할 경우 의도가 뒤섞이고 목적이 흐려진다. 한가지를 제대로해야하며 논리가 간단해야 버그가 숨어들지 못한다. 또한 서로에 대한 의존성을 줄여야 유지보수가 쉬워진다.

많이 놓치는 오류처리, 메모리누수, 경쟁상태, 일관성 없는 명명법 등도 신경써야하며 오류는 전략에 의거해 처리한다. 

성능을 최적으로 유지해야만 원칙없는 최적화를 하지않게된다. 원칙없는 최적화는 코드를 망칠 확률이 있다.

 

그래디 부치

"깨끗한 코드는 단순하고 직접적이다. 
가독성을 강조하고 잘 쓰여진 문장처럼 읽혀야한다."

깨끗한 코드는 단순하고 직접적이다. 

가독성을 강조하여 잘 쓰여진 문장처럼 읽혀야하며 설계자의 의도를 숨기지 않고 명쾌한(사실에 기반하여) 추상화와 단순한 제어문을 사용한다.

*명쾌한: 힘차고 단호하고 사실적인

 

데이브 토마스

"테스트 없는 코드는 깨끗한 코드가 아니다."

작성자가 아닌 사람이 읽기는 물론 고치기도 쉬울만큼 가독성 있는 코드가 깨끗한 코드이다.

읽기 쉬운 것과 고치기 쉬운 것은 엄연히 다르다.

테스트 주도 개발(TDD)이 오늘날 가장 근본적인 원칙 중 하나가 된 것을 보아라, 테스트 없는 코드는 깨끗한 코드가 아니다.

- 의미 있는 이름을 붙이고 목적을 달성하는 방법을 하나만 제공하라.

- 의존성은 최소로하고 각 의존성을 명확히 정의한다.

- API는 명확하며 최소로 줄인다. 최소는 큰 코드보다 작은 코드에 가치를 둔다는 의미로 코드는 작을 수록 좋다.

- 언어마다 모든 정보를 코드만으로 표현할 수 없기에 코드는 문학적(인간이 읽기 좋게)으로 표현해야한다


마이클 페더스

"깨끗한 코드란 언제나 누군가 주의 깊게 짰다는 느낌을 주며
고치려봐도 손 댈 곳이 없다."


론 제프리스

"중복이 없으며 간단한 코드, 단순한 코드 규칙으로 시작하고 끝낸다."

초반부터 간단한 추상화를 고려하며 시스템 내 모든 설계 아이디어를 표현한다.

의미 있는 이름으로 명명하며 객체나 메서드가 여러 기능을 수행한다면 여러개로 나누고 클래스, 메서드, 함수 등을 최대한의 크기로 줄인다.

모든 테스트를 통과해야한다.

 

워드 커닝햄

"코드가 그 문제를 풀기위한 언어처럼 보인다면 아름다운 코드라 불러도된다."

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드다.

 

우리들 생각

"깨끗한 변수 이름, 함수, 클래스"

 

 

결론

책을 읽는다고 뛰어난 프로그래머가 된다는 보장은 없다.

그저 뛰어난 프로그래머가 생각하는 방식과 그들이 사용하는 기술, 기교, 도구(경험적 교훈, 체계, 절차, 기법 등)를 소개하는 것을 보고 난 이후는 본인들이 하기 나름이다.

 


“연습해, 연습!”

 


문헌

클린코드 - 로버트 C. 마틴

 

사이트에 Form을 만들고 이것을 통해 '사이트 주인에게 메일 발송'하는 기능을 작업해보도록하겠습니다.

 

 

EmailJS

- http://www.emailjs.com/

 

Send email from Javascript - no server code required | EmailJS

Send email directly from your client-side Javascript code – no server side code required. Add static or dynamic attachments, dynamic parameters, captcha code and more. Start with our free tier!

www.emailjs.com

- 무료는 텍스트만 가능, 파일전송 X

 

 

 

1. 가입

EmailJS 메인

 

 

 

2. Email Services 페이지

Add New service를 클릭해줍니다.

메일을 발송하고, 받을 수 있는 플랫폼을 선택하고 연동하는 작업입니다.

Email services 페이지

 

 

저는 Gmail를 선택했습니다.

Service ID는 옆에 키 모양 아이콘을 누르면 변경 가능합니다.

(이 key가 공개되면 제 메일을 이용할 수 있을 것이므로, 저는 캡쳐에 나온 키 안썼습니다.)

service 선택 팝업

 

 

Connect Account 눌러서 계정 연동해줍니다.

이때, 팝업이 뜨는데 허용 버튼을 눌러줍니다.

EmailJS 권한 팝업

 

 

안내 문구를 읽어보면 발신자가 본인이 된다는 의미입니다.

허용을 모두 마쳤다면, Create Service 버튼을 클릭합니다.

 

대시보드의 Email Services에 내가 방금 생성한 것이 확인됩니다.

Email Services 대시보드 생성예시

 

 

그리고 연동한 메일에도 관련 Test 메일이 와있습니다.

EmailJS 테스트 메일

 

 

허용 누를때 안내나왔던대로 내가 발신자가 됩니다.

이후, 대시보드에서 보낸 히스토리나 이벤트 같은 것들을 확인할 수 있습니다.

Events에서 방금 테스트메일에 대한 Events를 확인할 수 있습니다.

Events 페이지

 

 

 

3. Email Templates 페이지

Create New Template를 클릭해서 템플릿을 만들어줍니다.

Email Templates 페이지 create 버튼

 

일단, 뭐가 뭔지 모르니 그냥 오른쪽 위에 있는 Save버튼을 눌러줍니다...

 

 

 

4. Integration 페이지

본격적으로 작업을 해보겠습니다.

NPM, Browser, API 버튼을 누르면 각 설명이 적혀있습니다.

 

저는 npm을 통해 작업하겠습니다.

integration 페이지

 

자세한 내용은document을 한번 읽어보세요.

https://www.emailjs.com/docs/sdk/installation/

 

SDK Installation | EmailJS

Guide goes through the various methods used to install EmailJS SDK

www.emailjs.com

 

 

 

5. npm install 및 예제 코드 확인

npm install emailjs-com --save

 

document에 적혀있는 예제

import React from 'react';import emailjs from 'emailjs-com';
import './ContactUs.css';

export default function ContactUs() {
  function sendEmail(e) {
    e.preventDefault();

    emailjs.sendForm('YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', e.target, 'YOUR_USER_ID')
      .then((result) => {
          console.log(result.text);
      }, (error) => {
          console.log(error.text);
      });
  }

  return (
    <form className="contact-form" onSubmit={sendEmail}>
      <input type="hidden" name="contact_number" />
      <label>Name</label>
      <input type="text" name="user_name" />
      <label>Email</label>
      <input type="email" name="user_email" />
      <label>Message</label>
      <textarea name="message" />
      <input type="submit" value="Send" />
    </form>
  );
  }

 

이 부분이 주의깊게 봐야하는 부분입니다.

 emailjs.sendForm('YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', e.target, 'YOUR_USER_ID')
   .then((result) => {
   	console.log(result.text);
    
   }, (error) => {
   	console.log(error.text);
    
   });
 }

sendForm 메서드에는 3개의 id와 함께 form을 넘겨줘야합니다.

(form data가 아니라 form 엘리먼트입니다.)

 

 

3개의 ID를 발급 받을 수 위치입니다.

  • YOUR_SERVICE_ID : Email Services 페이지에서 발급
  • YOUR_TEMPLATE_ID : Email Templates 페이지에서 발급
  • YOUR_USER_ID : Integration 페이지에서 API Keys에 User ID 확인

 

 

 

6. 코드

Email.JS

import React, { useCallback } from 'react';
import useInput from '../hooks/useInput';
import emailjs from 'emailjs-com';
import styled from 'styled-components';

const Form = styled.form`   
    margin: 0 auto;
    padding: 20px;  
    width: 400px;
    background: #ccc;
    text-align: center;

    input {
        padding: 5px 10px;
        width: 100%;
        border: 1px solid #666;

        & + input,
        & + textarea {
            margin-top: 20px;
        }
    }

    textarea {
        padding: 5px 10px;
        width: 100%;
        min-height: 100px;
        outline: none;
        resize: none;
        IME-MODE: auto;
    }

    button {
        margin-top: 20px;
        padding: 5px 15px;
        background: #fff;
        border: 1px solid #666;
        outline: none;
        cursor: pointer;
    }
`;

const Email = () => {
    const [name, onChangeName] = useInput('');
    const [email, onChangeEmail] = useInput('');
    const [text, onChangeText] = useInput('');

    const onSubmit = useCallback((e) => {
        e.preventDefault();

        const inputNum = e.target.childElementCount - 1; // [D] 버튼한개 제외
        const data = new FormData(e.target);
        const entries = data.entries();
        let failNum = 0;

        for (let i = 0; i < inputNum; i++) {
            const next = entries.next();
            const key = next.value[0];
            const value = next.value[1];

            if (!value) {
                failNum++;
                alert(`${key} 비어있습니다.`);
                break;
            }
        }

        if (!failNum) {
            emailjs.sendForm(
            	'YOUR_SERVICE_ID', 
                'YOUR_TEMPLATE_ID', 
                e.target, 
                'YOUR_USER_ID'
                
            ).then((result) => {
                console.log('result.text', result.text);

            }, (error) => {
                console.log(error.text);
            });
        }
    }, []);

    return (
        <Form onSubmit={onSubmit}>
            <input
                type="text"
                name="name"
                placeholder="이름"
                value={name}
                onChange={onChangeName}
            />

            <input
                type="text"
                name="email"
                placeholder="이메일"
                value={email}
                onChange={onChangeEmail}
            />

            <textarea
                name="text"
                placeholder="메세지"
                value={text}
                onChange={onChangeText}
            />

            <button type="submit">
                발송
            </button>
        </Form>
    );
};

export default Email;

 

 

 

useInput

커스텀 훅입니다.

import { useState, useCallback } from 'react';

export default (initialValue = null) => {
    const [value, setValue] = useState(initialValue);
    const handler = useCallback(({target}) => {
        setValue(target.value);
    }, []);

    return [value, handler, setValue];
};

 

 

 

7. 템플릿 수정 (EmailJS 대시보드 Email Templates 페이지)

코드 작업이 끝났으니 초반에 만들었던 Email Templates를 수정해줍니다.

input의 name값과 맞춰서 key 값을 삽입해주면 됩니다.

 

 

수정 아이콘을 눌러서 수정페이지로 이동합니다.

Email Tempalte 수정 방법

 

 

제목, 컨텐츠 모두 수정해주었습니다.

수정을 완료하였다면 Save를 눌러주세요.

Template 수정 페이지

 

 

 

8. 메일 발송 확인

이제 폼을 작성하여 발송을 눌러봅니다.

예시 폼

 

그 후 내가 서비스로 선택한 플랫폼의 메일함을 보면 메일이 도착합니다.

 

emailJS 템플릿 테스트 메일

 

 

내용도 수정된 것이 확인됩니다.

정상적으로 동작ㅎㅎ!

emailJS 템플릿 테스트 메일 컨텐츠

 

 

 

지난 포스팅에서 만들었던 슬라이드를 수정하게되었습니다.

포스팅에서도 말했었는데,

기존 방식은 지정된 갯수의 이미지만으로 작업을 한 부분이었기에 문제가 없는데,

이미지 갯수가 동적으로 들어오게되면 스타일이 깨지게됩니다.

그래서 실제로.. 작업하다가 이미지 갯수를 늘리려하니 깨지게되고 이런게 너무 거슬려서 수정하게되었습니다.

 

다만 지난 포스팅과는 방식이 다릅니다.

지난 포스팅은 customPaging 옵션 방식으로 제작한 것이고

이번엔 AsNavFor 방식으로 제작하게되었습니다.

 

 

AsNavFor 옵션 example 확인해보기

react-slick.neostack.com/docs/example/as-nav-for

 

Neostack

The last react carousel you will ever need

react-slick.neostack.com

 

 

끊어서 코드를 확인해보겠습니다.

import 

import React, { useCallback, useEffect, useRef, useState } from 'react';
import Slick from 'react-slick';
import styled, { css } from 'styled-components';

import { LeftOutlined, RightOutlined } from '@ant-design/icons';

Slick이 필수나머지는 꾸미기 위해 import해온 부분들입니다.

 

 

Style

const Wrap = styled.div`
    overflow: hidden;

    & > div + div {
        margin-top: 20px;
    }
`;

const Inner = styled.div`
    position: relative;

    .paging_items {
        filter: grayscale(1);

        &:hover {
            filter: none;
        }
    }

    .slick-current .paging_items {
        filter: none;
    }
`;

const defaultItemStyle = css`
    width: 100%;    
    text-align: center;

    img {
        height: 100%;
        vertical-align: top;
    }
`;

const MainSlickItems = styled.div`
    ${defaultItemStyle}    
    height: 350px;

    img {
        max-width: 100%;
    }
`;

const PagingItems = styled.div`
    ${defaultItemStyle}    
    height: 80px;
    cursor: pointer;
    
    img {
        width: 100%;
    }
`;

const defaultButtonStyle = css`
    position: absolute;
    top: 50%;
    padding: 0;
    width: 30px;
    height: 30px;
    line-height: 1;
    border: none;
    border-radius: 50%;
    background: none;
    outline: none;
    transform:translateY(-50%);
    cursor: pointer;
`;

const PrevButton = styled.button`
    ${defaultButtonStyle}
    left: 0;
`;

const NextButton = styled.button`
    ${defaultButtonStyle}
    right: 0;
`;

const defaultIconStyle = css`
    font-size: 22px;
    color: #dedede;

    &:focus,
    &:hover { 
        color: #666;
    }
`;

const PrevIcon = styled(LeftOutlined)`
    ${defaultIconStyle}
`;

const NextIcon = styled(RightOutlined)`
    ${defaultIconStyle}
`;

스타일은 꾸미기 나름이고 여기서 설명할 부분은 슬릭이 부여하는 클래스들인데요. 

선택된 아이템에는 slick-current가 노출되고 있는 아이템들에는 slick-active가..

그리고 centerMode가 true로 되어있어서 선택 후 가운데에 위치하게되는 객체에 .slick-current 클래스가 붙습니다.

저는 선택된 객체에 스타일을 주기위해 해당 .slick-current 클래스를 사용했고, 

hover를 추가해주기 위해서 Slick을 감싸고 있는 엘리먼트에 스타일을 추가하는 방식으로 진행했습니다.

 

 

Component_1

return 영역을 제외하고 먼저 보겠습니다.

const Slide = ({ images }) => {

	// 1.
    const [mainSlick, setMainSlick] = useState(null);
    const [pagingSlick, setPagingSlick] = useState(null);
    const mainSlickRef = useRef(null);
    const pagingSlickRef = useRef(null);

    useEffect(() => {
        setMainSlick(mainSlickRef.current);
        setPagingSlick(pagingSlickRef.current);
    }, []);
	
    // 2.
    const mainSettings = {
        dots: false,
        arrows: false,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
    };
    
	// 2.
    const pagingSettings = {
        dots: false,
        arrows: false,
        centerMode: true,
        slidesToShow: 8,
        swipeToSlide: true,
        focusOnSelect: true,
    };
	
    // 3.
    const onClickPrev = useCallback((ref) => () => ref.current.slickPrev(), []);
    const onClickNext = useCallback((ref) => () => ref.current.slickNext(), []);

    return (
    	//...
    );
};

export default Slide;

1.

ref만 저장해서 작업을 끝내고 싶었지만.. 

state를 사용하지않으면 동작하지않아서 state를 추가했습니다. 

아마 비동기때문인 것 같은데, 좋은 방법이있다면 댓글로 짚어주시면 감사합니다.

컴포넌트 마운트 시에 state 부분에 ref.current를 넣어줍니다.

 

2.

메인이 될 슬라이드와 페이지 번호를 담당할 슬라이드의 셋팅입니다.

 

3.

클릭 함수 하나로 해결하기 위해 대상이 될 ref를 인자로 받아 처리합니다.

 

 

Component_2

return (
  <Wrap>
    <Inner>
      <Slick 
      
      	{/* 1 */}
        ref={mainSlickRef} 
        asNavFor={pagingSlick}
        {...mainSettings}
      >
      
      	{/* 2 */}
        {images.map((v, i) => {
          return (
            <MainSlickItems key={`${v.title}_${i}`}>
            	<img src={v.src} />
            </MainSlickItems>
          )
        })}
      </Slick>

	{/* 3 */}
    <>
      <PrevButton onClick={onClickPrev(mainSlickRef)}>
        <PrevIcon />
      </PrevButton>

      <NextButton onClick={onClickNext(mainSlickRef)}>
        <NextIcon />
      </NextButton>
    </>
    </Inner>

    <Inner>
      <Slick
        ref={pagingSlickRef}
        asNavFor={mainSlick}
        {...pagingSettings}
      >
        {images.map((v, i) => {
          return (
            <PagingItems 
              key={`${v.title}_${i}`}
              className="paging_items"
            >
            	<img src={v.src} />
            </PagingItems>
          )
        })}
      </Slick>

      <>
        <PrevButton onClick={onClickPrev(pagingSlickRef)}>
          <PrevIcon />
        </PrevButton>

        <NextButton onClick={onClickNext(pagingSlickRef)}>
          <NextIcon />
        </NextButton>
      </>
    </Inner>
  </Wrap>
);

1.

ref에 해당 객체를 담아줍니다.

asNavFor에 같이 동작할 객체를 저장한 state를 넣어줍니다.

세팅도 넣어줍니다.

 

2.

슬라이드에 표현할 이미지를 반복시킵니다. (당연히 메인이랑 페이지번호부분이 동일한 이미지여야겠죠)

 

3. 

슬라이드 이전, 다음 버튼입니다.

이 부분에서 ref를 넣어줘야 prev, next 함수 실행 시에 어떤 객체를 대상으로 할 지 설정할 수 있습니다.

 

 

메인 슬라이드와 페이지번호 슬라이드는 매우 비슷합니다.

하지만 스타일이나 조금씩 다른 부분이 있어서 하나로 사용하지 못했습니다.

(따로 컴포넌트로 분리하면 가능할 법도 합니다만...)

 

 

완성코드입니다.

import React, { useCallback, useEffect, useRef, useState } from 'react';
import Slick from 'react-slick';
import styled, { css } from 'styled-components';

import { LeftOutlined, RightOutlined } from '@ant-design/icons';

const Wrap = styled.div`
    overflow: hidden;

    & > div + div {
        margin-top: 20px;
    }
`;

const Inner = styled.div`
    position: relative;

    .paging_items {
        filter: grayscale(1);

        &:hover {
            filter: none;
        }
    }

    .slick-current .paging_items {
        filter: none;
    }
`;

const defaultItemStyle = css`
    width: 100%;    
    text-align: center;

    img {
        height: 100%;
        vertical-align: top;
    }
`;

const MainSlickItems = styled.div`
    ${defaultItemStyle}    
    height: 350px;

    img {
        max-width: 100%;
    }
`;

const PagingItems = styled.div`
    ${defaultItemStyle}    
    height: 80px;
    cursor: pointer;
    
    img {
        width: 100%;
    }
`;

const defaultButtonStyle = css`
    position: absolute;
    top: 50%;
    padding: 0;
    width: 30px;
    height: 30px;
    line-height: 1;
    border: none;
    border-radius: 50%;
    background: none;
    outline: none;
    transform:translateY(-50%);
    cursor: pointer;
`;

const PrevButton = styled.button`
    ${defaultButtonStyle}
    left: 0;
`;

const NextButton = styled.button`
    ${defaultButtonStyle}
    right: 0;
`;

const defaultIconStyle = css`
    font-size: 22px;
    color: #dedede;

    &:focus,
    &:hover { 
        color: #666;
    }
`;

const PrevIcon = styled(LeftOutlined)`
    ${defaultIconStyle}
`;

const NextIcon = styled(RightOutlined)`
    ${defaultIconStyle}
`;

const Slide = ({ images }) => {
    const [mainSlick, setMainSlick] = useState(null);
    const [pagingSlick, setPagingSlick] = useState(null);
    const mainSlickRef = useRef(null);
    const pagingSlickRef = useRef(null);

    useEffect(() => {
        setMainSlick(mainSlickRef.current);
        setPagingSlick(pagingSlickRef.current);
    }, []);

    const mainSettings = {
        dots: false,
        arrows: false,
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
    };

    const pagingSettings = {
        dots: false,
        arrows: false,
        centerMode: true,
        slidesToShow: 8,
        swipeToSlide: true,
        focusOnSelect: true,
    };

    const onClickPrev = useCallback((ref) => () => ref.current.slickPrev(), []);
    const onClickNext = useCallback((ref) => () => ref.current.slickNext(), []);

    return (
        <Wrap>
            <Inner>
                <Slick 
                    ref={mainSlickRef} 
                    asNavFor={pagingSlick}
                    {...mainSettings}
                >
                    {images.map((v, i) => {
                        return (
                            <MainSlickItems key={`${v.title}_${i}`}>
                                <img src={v.src} />
                            </MainSlickItems>
                        )
                    })}
                </Slick>

                <>
                    <PrevButton onClick={onClickPrev(mainSlickRef)}>
                        <PrevIcon />
                    </PrevButton>

                    <NextButton onClick={onClickNext(mainSlickRef)}>
                        <NextIcon />
                    </NextButton>
                </>
            </Inner>

            <Inner>
                <Slick
                    ref={pagingSlickRef}
                    asNavFor={mainSlick}
                    {...pagingSettings}
                >
                    {images.map((v, i) => {
                        return (
                            <PagingItems 
                                key={`${v.title}_${i}`}
                                className="paging_items"
                            >
                                <img src={v.src} />
                            </PagingItems>
                        )
                    })}
                </Slick>

                <>
                    <PrevButton onClick={onClickPrev(pagingSlickRef)}>
                        <PrevIcon />
                    </PrevButton>

                    <NextButton onClick={onClickNext(pagingSlickRef)}>
                        <NextIcon />
                    </NextButton>
                </>
            </Inner>
        </Wrap>
    );
};

export default Slide;

 

슬라이드 완성 이미지

이미지 출처: pixabay 

 

 

동작도 확인해보겠습니다.

슬라이드 완성 이미지 gif

 

 

이렇게 하면 많은 이미지로도 깨지지않고 작업할 수 있습니다~

 

 

+ Recent posts