[TIL/React] 2023/11/13

원민관·2023년 11월 13일
0

[TIL]

목록 보기
130/159
post-thumbnail

서론 ✍️

사람은 why에 대한 본인만의 대답이 없을 때 흔들린다. 다들 무엇(what)을 하겠노라고 쉽게 얘기한다. 어떻게(how) 하겠다는 말로 무엇(what)을 강화한다. 그런데 지칠 때면 왜(why) 해야 하는지에 의심을 품기 시작하고, 명확한 답이 없을 때 혹은 잊었을 때 포기한다. 그렇다. 왜(why)는 초심이다.

통신에 대해 학습해야겠다고 생각했다. 왜(why) 통신을 배워야 되는지부터 설명하기 위해 앞에서 무게를 잡았다. 시작하겠다.

왜 통신을 학습해야 할까? ✍️

개인마다 프로그래밍을 정의하는 방식은 다양하지만, 결국 하나의 애플리케이션은 액션, 계산, 데이터로 구성된다. 정육각 프로젝트를 진행하며 '내가 만든 데이터'로 액션, 계산 처리에 대해서는 찍먹을 해봤다. 문제는, 대부분의 경우 데이터를 서버에서 받아오고 목적에 맞게 파싱한다. 서버와 대화할 필요가 생겼기 때문에 통신을 학습해야 되는 것이다. 잊지 말자.

동기(sync) 그리고 비동기(async) ✍️

동기(sync)와 비동기(async)에 대해서는 왜 학습해야 할까? '비효율'이라는 주제로부터 시작된다. 동기는 '요청과 결과가 동시에 O', 비동기는 '요청과 결과가 동시에 X'이다. 배달 로직이 있다고 가정하자.

동기적으로 동작하는 배달 로직에서는 배달원이 짜장면을 배달한 뒤, 손님이 음식을 다 먹고 그릇을 내놓기까지 기다려야 한다. 요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 하는 것이 바로 동기(sync)이다.

그렇다면 비동기는 쉽게 이해할 수 있다. 요청과 결과가 동시에 발생하지 않는 것이 비동기(async)이다. 따라서 배달원은 다른 주문에 대한 배달을 수행할 수 있게 된다.

서버가 클라이언트로 데이터를 전송할 때까지 아무것도 할 수 없는 상태는 그 자체로 비효율적이다. 그렇기에 비동기적인 처리가 코드 내에서 이루어지게 된다. 동기의 비효율, 그리고 그 비효율로부터 파생되는 비동기에 대한 논의, 이 부분이 통신의 시작이기 때문에 동기(sync)와 비동기(async)의 개념을 학습해야 하는 것이다.

콜백 지옥 개념의 등장 ✍️

동기의 비효율이 비동기 코드의 사용을 촉발했다고 이해했다. 문제는, 어쨌든 순서는 있어야 된다는 것이다. 비동기 작업에 대한 여러 함수가 있어도, 작업간에 의존성이 있는 경우에는 당연히 순서를 부여해야 한다. 이때 비동기에 대한 코드는 콜백 함수로 표현된다. 콜백 함수에 콜백 함수가 들어가면서 코드의 depth가 깊어지는 현상, 즉 콜백 지옥의 배경은 바로 이렇게 등장하게 된다. 코드로 표현하면 다음과 같다.

// 주문 받기
const takeOrder = (customer, callback) => {
  setTimeout(() => {
    console.log(`주문 받음: ${customer}`);
    callback();
  }, 1000);
};

// 요리 시작
const startCooking = (callback) => {
  setTimeout(() => {
    console.log("요리 시작");
    callback();
  }, 1000);
};

// 배달 준비
const prepareDelivery = (callback) => {
  setTimeout(() => {
    console.log("배달 준비 완료");
    callback();
  }, 1000);
};

// 배달 진행
const doDelivery = (address, callback) => {
  setTimeout(() => {
    console.log(`배달 중: ${address}`);
    callback();
  }, 1000);
};

// 주문에서 배달까지의 로직
takeOrder("손님 1", function () {
  startCooking(function () {
    prepareDelivery(function () {
      doDelivery("손님 1 집주소", function () {
        console.log("배달 완료");
      });
    });
  });
});

Promise 개념의 등장 ✍️

따라서 Promise는 콜백 지옥에 대한 대안이다. Promise는 객체이다. 코드를 먼저 살펴보자.

// 주문 받기
function takeOrder(customer) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(`주문 받음: ${customer}`);
      resolve();
    }, 1000);
  });
}

// 요리 시작
function startCooking() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("요리 시작");
      resolve();
    }, 1000);
  });
}

// 배달 준비
function prepareDelivery() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("배달 준비 완료");
      resolve();
    }, 1000);
  });
}

// 배달 진행
function doDelivery(address) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(`배달 중: ${address}`);
      resolve();
    }, 1000);
  });
}

// 주문 및 배달 시뮬레이션
takeOrder("고객 A")
  .then(function () {
    return startCooking();
  })
  .then(function () {
    return prepareDelivery();
  })
  .then(function () {
    return doDelivery("고객 A의 집");
  })
  .then(function () {
    console.log("배달 완료");
  });

Promise는 비동기 작업의 완료 또는 실패를 처리하기 위한 하나의 매커니즘이다. Promise 객체가 제공하는 then 메서드를 활용하면, then 메서드 내부에 Promise가 이행되었을 때 실행되는 콜백을 등록할 수 있게 된다. 콜백 지옥 커리를 타지 않아도 순서가 보장되는 것이 Promise의 최대 장점이다.

콜백 지옥으로 작성한 코드와 결과는 동일하지만, 코드가 좀 더 선형적이고 읽기 쉬워진 형태로 변경되었다는 것을 확인할 수 있다.

async/await ✍️

async/await는 한마디로 Promise의 Syntactic sugar, 즉 더 쉬운 버전이다. 어떻게든 더 직관적으로 사용하고 싶어하는 개발자들의 열망이 낳은 산물인 것이다. 코드로 얘기하자.

// 주문 받기
function takeOrder(customer) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(`주문 받음: ${customer}`);
      resolve();
    }, 1000);
  });
}

// 요리 시작
function startCooking() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("요리 시작");
      resolve();
    }, 1000);
  });
}

// 배달 준비
function prepareDelivery() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("배달 준비 완료");
      resolve();
    }, 1000);
  });
}

// 배달 진행
function doDelivery(address) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(`배달 중: ${address}`);
      resolve();
    }, 1000);
  });
}

// 주문 및 배달 시뮬레이션
async function simulateDelivery() {
  await takeOrder("고객 A");
  await startCooking();
  await prepareDelivery();
  await doDelivery("고객 A의 집");
  console.log("배달 완료");
}

// 실행
simulateDelivery();

async/await를 사용하면, 비동기 작업이 마치 '동기 코드'처럼 보이게 된다. 각 함수 앞에 await 키워드를 붙여 해당 작업이 완료될 때까지 기다리고, 그 후 다음 작업을 수행한다. 이로써 코드가 훨씬 더 읽기 쉽고 관리하기 쉬워지는 장점이 있다.

try/catch ✍️

이 정도 했으면 된거 아니야? 싶지만, 아직 try/catch가 남았다. try/catch의 존재 이유를 코드를 통해 살펴보자.

// 주문 받기
function takeOrder(customer) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      try {
        if (customer === "고객 B") {
          throw new Error("주문 오류: 해당 고객에게는 배달하지 않습니다.");
        }
        console.log(`주문 받음: ${customer}`);
        resolve();
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

// 요리 시작
function startCooking() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      try {
        // 요리 중 예외 상황
        throw new Error("요리 중 오류 발생");
        console.log("요리 시작");
        resolve();
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

// 배달 준비
function prepareDelivery() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log("배달 준비 완료");
      resolve();
    }, 1000);
  });
}

// 배달 진행
function doDelivery(address) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(`배달 중: ${address}`);
      resolve();
    }, 1000);
  });
}

// 주문 및 배달 시뮬레이션
async function simulateDelivery() {
  try {
    await takeOrder("고객 A");
    await startCooking();
    await prepareDelivery();
    await doDelivery("고객 A의 집");
    console.log("배달 완료");
  } catch (error) {
    console.error(`에러 발생: ${error.message}`);
  }
}

// 실행
simulateDelivery();

try/catch 블록을 사용하는 이유는 비동기 코드에서 발생할 수 있는 예외(에러)를 처리하고, 프로그램의 안정성을 높이기 위함이다. 비동기 작업 중에 에러가 발생하면 해당 예외는 프로미스 객체의 reject 메서드를 통해 전파된다. 이때, async/await를 사용하면 코드가 '동기적으로 작성'되었지만 실제로는 '비동기적으로 동작'하므로 '예외 처리'가 중요하다.

요컨대, 비동기 코드에서 발생하는 에러를 '블록 단위'로 관리하기 위해 try/catch 개념을 활용한다.

남은 것 ✍️

  1. fetch
  2. res.json
  3. json.stringfy(data)
    에 대한 학습
profile
Write a little every day, without hope, without despair ✍️

0개의 댓글